Skip to content

Commit 098024b

Browse files
committed
Download/Upload speed improvement
1 parent c57f286 commit 098024b

3 files changed

Lines changed: 227 additions & 94 deletions

File tree

pyrogram/client.py

Lines changed: 153 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import shutil
2727
import sys
2828
import time
29+
import math
30+
import tempfile
2931
from concurrent.futures.thread import ThreadPoolExecutor
3032
from datetime import datetime, timedelta
3133
from hashlib import sha256
@@ -198,6 +200,10 @@ class Client(Methods):
198200
Defaults to False, because ``getpass`` (the library used) is known to be problematic in some
199201
terminal environments.
200202
203+
crypto_executor_workers (``int``, *optional*):
204+
Set the maximum crypto executor workers value for a session.
205+
Defaults to ``workers if workers < 5 else 5``.
206+
201207
max_concurrent_transmissions (``int``, *optional*):
202208
Set the maximum amount of concurrent transmissions (uploads & downloads).
203209
A value that is too high may result in network related issues.
@@ -260,6 +266,7 @@ class Client(Methods):
260266
UPGRADED_GIFT_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:nft/|\+))([\w-]+)$")
261267
CHANNEL_MESSAGE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:c/)?)([\w]+)(?:.+)?$")
262268
WORKERS = min(32, (os.cpu_count() or 0) + 4) # os.cpu_count() can be None
269+
CRYPTO_EXECUTOR_WORKERS = WORKERS if WORKERS < 5 else 5
263270
WORKDIR = PARENT_DIR
264271

265272
# Interval of seconds in which the updates watchdog will kick in
@@ -301,6 +308,7 @@ def __init__(
301308
takeout: Optional[bool] = None,
302309
sleep_threshold: int = Session.SLEEP_THRESHOLD,
303310
hide_password: Optional[bool] = False,
311+
crypto_executor_workers: int = CRYPTO_EXECUTOR_WORKERS,
304312
max_concurrent_transmissions: int = MAX_CONCURRENT_TRANSMISSIONS,
305313
max_message_cache_size: int = MAX_MESSAGE_CACHE_SIZE,
306314
max_topic_cache_size: int = MAX_TOPIC_CACHE_SIZE,
@@ -345,6 +353,7 @@ def __init__(
345353
self.takeout = takeout
346354
self.sleep_threshold = sleep_threshold
347355
self.hide_password = hide_password
356+
self.crypto_executor_workers = crypto_executor_workers
348357
self.max_concurrent_transmissions = max_concurrent_transmissions
349358
self.max_message_cache_size = max_message_cache_size
350359
self.max_topic_cache_size = max_topic_cache_size
@@ -1072,12 +1081,13 @@ def load_plugins(self):
10721081
async def handle_download(self, packet):
10731082
file_id, directory, file_name, in_memory, file_size, progress, progress_args = packet
10741083

1084+
max_workers = self.crypto_executor_workers
10751085
os.makedirs(directory, exist_ok=True) if not in_memory else None
10761086
temp_file_path = os.path.abspath(re.sub("\\\\", "/", os.path.join(directory, file_name))) + ".temp"
10771087
file = BytesIO() if in_memory else open(temp_file_path, "wb")
10781088

10791089
try:
1080-
async for chunk in self.get_file(file_id, file_size, 0, 0, progress, progress_args):
1090+
async for chunk in self.get_file(file_id, file_size, 0, 0, progress, progress_args, max_workers, in_memory):
10811091
file.write(chunk)
10821092
except BaseException as e:
10831093
if not in_memory:
@@ -1108,7 +1118,9 @@ async def get_file(
11081118
limit: int = 0,
11091119
offset: int = 0,
11101120
progress: Callable = None,
1111-
progress_args: tuple = ()
1121+
progress_args: tuple = (),
1122+
max_workers: int = 1,
1123+
in_memory: bool = False
11121124
) -> AsyncGenerator[bytes, None]:
11131125
async with self.get_file_semaphore:
11141126
file_type = file_id.file_type
@@ -1151,15 +1163,39 @@ async def get_file(
11511163
)
11521164

11531165
current = 0
1166+
temporary = True
11541167
total = abs(limit) or (1 << 31) - 1
11551168
chunk_size = 1024 * 1024
11561169
offset_bytes = abs(offset) * chunk_size
11571170

11581171
dc_id = file_id.dc_id
11591172

1173+
async def handle_progress(downloaded_bytes):
1174+
if not progress:
1175+
return
1176+
func = functools.partial(
1177+
progress,
1178+
min(downloaded_bytes, file_size) if file_size != 0 else downloaded_bytes,
1179+
file_size,
1180+
*progress_args
1181+
)
1182+
if inspect.iscoroutinefunction(progress):
1183+
asyncio.create_task(func())
1184+
else:
1185+
self.loop.run_in_executor(self.executor, func)
1186+
11601187
try:
1161-
session = await self.get_session(dc_id, is_media=True)
1162-
1188+
try:
1189+
session = await self.get_session(dc_id, is_media=True, temporary=True)
1190+
except Exception as e:
1191+
temporary = False
1192+
try:
1193+
await session.stop()
1194+
except:
1195+
pass
1196+
log.info(f"Reusing cached media session due to {e}")
1197+
session = await self.get_session(dc_id, is_media=True)
1198+
11631199
r = await session.invoke(
11641200
raw.functions.upload.GetFile(
11651201
location=location,
@@ -1170,40 +1206,119 @@ async def get_file(
11701206
)
11711207

11721208
if isinstance(r, raw.types.upload.File):
1173-
while True:
1209+
if file_size and file_size > 0:
1210+
total_parts = math.ceil(file_size / chunk_size)
1211+
else:
1212+
if limit:
1213+
total_parts = abs(limit)
1214+
else:
1215+
total_parts = None
1216+
if total_parts is None:
11741217
chunk = r.bytes
1175-
11761218
yield chunk
1177-
11781219
current += 1
11791220
offset_bytes += chunk_size
1221+
await handle_progress(offset_bytes)
11801222

1181-
if progress:
1182-
func = functools.partial(
1183-
progress,
1184-
min(offset_bytes, file_size)
1185-
if file_size != 0
1186-
else offset_bytes,
1187-
file_size,
1188-
*progress_args
1223+
while True:
1224+
if len(chunk) < chunk_size or current >= total:
1225+
break
1226+
1227+
r = await session.invoke(
1228+
raw.functions.upload.GetFile(
1229+
location=location,
1230+
offset=offset_bytes,
1231+
limit=chunk_size
1232+
),
1233+
sleep_threshold=30
11891234
)
1190-
1191-
if inspect.iscoroutinefunction(progress):
1192-
await func()
1235+
if not isinstance(r, raw.types.upload.File):
1236+
break
1237+
chunk = r.bytes
1238+
yield chunk
1239+
current += 1
1240+
offset_bytes += chunk_size
1241+
await handle_progress(offset_bytes)
1242+
else:
1243+
concurrency = min(max_workers, total_parts)
1244+
sem = asyncio.Semaphore(concurrency)
1245+
downloaded_bytes = 0
1246+
exceptions: list[Exception] = []
1247+
parts_lock = asyncio.Lock()
1248+
1249+
parts: list[dict] = []
1250+
for i in range(total_parts):
1251+
part = {
1252+
"part_no": i,
1253+
"is_completed": asyncio.Future(),
1254+
"bytes": None
1255+
}
1256+
parts.append(part)
1257+
1258+
if not in_memory:
1259+
tempdir = tempfile.mkdtemp(prefix=f"pyro_getfile_{self.rnd_id()}")
1260+
for part in parts:
1261+
part["bytes"] = os.path.join(tempdir, f"part_{part['part_no']}")
1262+
1263+
def write_file(data: bytes, path: str):
1264+
with open(path, "wb") as f:
1265+
f.write(data)
1266+
1267+
def cleanup_tempdir():
1268+
for part in parts:
1269+
try:
1270+
os.remove(part["bytes"])
1271+
except:
1272+
pass
1273+
try:
1274+
os.rmdir(tempdir)
1275+
except:
1276+
pass
1277+
1278+
async def download_part(part: dict):
1279+
nonlocal downloaded_bytes
1280+
part_no = part["part_no"]
1281+
local_offset = offset_bytes + part_no * chunk_size
1282+
async with sem:
1283+
try:
1284+
r = await session.invoke(
1285+
raw.functions.upload.GetFile(location=location, offset=local_offset, limit=chunk_size),
1286+
sleep_threshold=30
1287+
)
1288+
if not isinstance(r, raw.types.upload.File):
1289+
raise RuntimeError(f"Unexpected response for part {part_no}: {type(r)}")
1290+
1291+
if in_memory:
1292+
part["bytes"] = r.bytes
1293+
else:
1294+
await self.loop.run_in_executor(self.executor, write_file, r.bytes, part["bytes"])
1295+
1296+
async with parts_lock:
1297+
downloaded_bytes += len(r.bytes)
1298+
await handle_progress(min(downloaded_bytes, file_size))
1299+
1300+
part["is_completed"].set_result(True)
1301+
except Exception as e:
1302+
if not part["is_completed"].done():
1303+
part["is_completed"].set_exception(e)
1304+
exceptions.append(e)
1305+
1306+
tasks = [asyncio.create_task(download_part(part)) for part in parts]
1307+
1308+
for part in parts:
1309+
await part["is_completed"]
1310+
if in_memory:
1311+
yield part["bytes"]
11931312
else:
1194-
await self.loop.run_in_executor(self.executor, func)
1313+
data = await self.loop.run_in_executor(self.executor, lambda p=part["bytes"]: open(p, "rb").read())
1314+
yield data
11951315

1196-
if len(chunk) < chunk_size or current >= total:
1197-
break
1316+
await asyncio.gather(*tasks, return_exceptions=True)
1317+
if not in_memory:
1318+
await self.loop.run_in_executor(self.executor, cleanup_tempdir)
11981319

1199-
r = await session.invoke(
1200-
raw.functions.upload.GetFile(
1201-
location=location,
1202-
offset=offset_bytes,
1203-
limit=chunk_size
1204-
),
1205-
sleep_threshold=30
1206-
)
1320+
if exceptions:
1321+
raise exceptions[0]
12071322

12081323
elif isinstance(r, raw.types.upload.FileCdnRedirect):
12091324

@@ -1291,6 +1406,9 @@ def _check_all_hashes():
12911406
raise
12921407
except Exception as e:
12931408
log.exception(e)
1409+
finally:
1410+
if temporary:
1411+
await session.stop()
12941412

12951413
async def get_session(
12961414
self,
@@ -1367,6 +1485,10 @@ async def get_session(
13671485

13681486
if is_media:
13691487
auth_key = (await self.get_session(dc_id)).auth_key
1488+
1489+
if temporary:
1490+
await self.get_session(dc_id, is_media=True)
1491+
13701492
else:
13711493
if not is_current_dc:
13721494
auth_key = await Auth(
@@ -1386,7 +1508,8 @@ async def get_session(
13861508
port,
13871509
auth_key,
13881510
await self.storage.test_mode(),
1389-
is_media=is_media
1511+
is_media=is_media,
1512+
crypto_executor_workers=self.crypto_executor_workers
13901513
)
13911514

13921515
if not temporary:

0 commit comments

Comments
 (0)