Spaces:
Running
Running
File size: 9,236 Bytes
58f00c9 a00abaf 58f00c9 a00abaf 58f00c9 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 529b973 a00abaf 58f00c9 a00abaf 58f00c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | """Uptime / ping / resource numbers for /status and /stats.
Inside a container psutil reports the *host's* CPU and RAM, so the cgroup files
are read first and psutil is only the fallback.
"""
from __future__ import annotations
import asyncio
import os
import platform
import shutil
import time
from typing import Any, List, Optional, Tuple
import psutil
from . import __version__
from .fasttransfer import BIG_FILE_THRESHOLD
from .utils import clip, esc, human_size, human_uptime
START_TIME = time.time()
CGROUP_ROOT = "/sys/fs/cgroup"
_NET_BASE = psutil.net_io_counters()
def _read(path: str) -> Optional[str]:
try:
with open(path, "r", encoding="utf-8") as fh:
return fh.read().strip()
except OSError:
return None
def parse_cpu_max(text: Optional[str]) -> Optional[float]:
"""cgroup v2 ``cpu.max`` -> number of cores, ``None`` when unlimited."""
if not text:
return None
parts = text.split()
if len(parts) != 2 or parts[0] == "max":
return None
try:
quota, period = int(parts[0]), int(parts[1])
except ValueError:
return None
return quota / period if quota > 0 and period > 0 else None
def parse_mem_limit(text: Optional[str]) -> Optional[int]:
"""cgroup ``memory.max`` / v1 ``limit_in_bytes`` -> bytes, ``None`` when unlimited."""
if not text or text.strip() == "max":
return None
try:
value = int(text.strip())
except ValueError:
return None
return value if value < 2 ** 60 else None
def parse_stat_value(text: Optional[str], key: str) -> Optional[int]:
for line in (text or "").splitlines():
parts = line.split()
if len(parts) == 2 and parts[0] == key:
try:
return int(parts[1])
except ValueError:
return None
return None
def cpu_cores() -> float:
quota = parse_cpu_max(_read(f"{CGROUP_ROOT}/cpu.max"))
return quota if quota else float(psutil.cpu_count() or 1)
def _cgroup_cpu_usec() -> Optional[int]:
return parse_stat_value(_read(f"{CGROUP_ROOT}/cpu.stat"), "usage_usec")
async def cpu_percent(interval: float = 0.5) -> float:
"""CPU use of this container as a percentage of its own limit."""
first = _cgroup_cpu_usec()
if first is None:
return psutil.cpu_percent(interval=None)
t0 = time.monotonic()
await asyncio.sleep(interval)
second = _cgroup_cpu_usec()
elapsed = time.monotonic() - t0
if second is None or elapsed <= 0:
return psutil.cpu_percent(interval=None)
return max(0.0, min(100.0, (second - first) / (elapsed * 1_000_000) / cpu_cores() * 100))
def memory_usage() -> Tuple[int, int]:
"""(used, total) bytes for this container."""
current = _read(f"{CGROUP_ROOT}/memory.current")
limit = parse_mem_limit(_read(f"{CGROUP_ROOT}/memory.max"))
if current is None: # cgroup v1
current = _read(f"{CGROUP_ROOT}/memory/memory.usage_in_bytes")
limit = parse_mem_limit(_read(f"{CGROUP_ROOT}/memory/memory.limit_in_bytes"))
host_total = psutil.virtual_memory().total
if current is None or not current.isdigit():
vm = psutil.virtual_memory()
return vm.used, vm.total
used = int(current)
inactive = parse_stat_value(_read(f"{CGROUP_ROOT}/memory.stat"), "inactive_file") or 0
used = max(0, used - inactive) # do not count reclaimable page cache
return used, min(limit, host_total) if limit else host_total
def disk_usage(path: str) -> Tuple[int, int, int]:
os.makedirs(path, exist_ok=True)
du = shutil.disk_usage(path)
return du.used, du.total, du.free
def net_since_start() -> Tuple[int, int]:
now = psutil.net_io_counters()
return max(0, now.bytes_recv - _NET_BASE.bytes_recv), max(0, now.bytes_sent - _NET_BASE.bytes_sent)
def uptime() -> float:
return time.time() - START_TIME
def crypto_backend() -> str:
"""What Telethon encrypts and decrypts with: ``cryptg`` or ``libssl`` (both native code, fast), else ``python`` (slow)."""
from telethon.crypto import aes
if aes.cryptg is not None:
return "cryptg"
return "libssl" if aes.libssl.encrypt_ige and aes.libssl.decrypt_ige else "python"
def loop_backend() -> str:
"""``uvloop`` when the running event loop is uvloop's, ``asyncio`` for the standard one, ``none`` outside a loop."""
try:
loop = asyncio.get_running_loop()
except RuntimeError:
return "none"
return "uvloop" if type(loop).__module__.split(".")[0] == "uvloop" else "asyncio"
def speed_checks(parallel: bool, connections: int, stream: Optional[bool] = None) -> List[Tuple[str, str, bool]]:
"""``(short name, detail, fine)`` for the things transfer speed depends on; ``stream`` (when given) adds the streaming rename."""
crypto, loop = crypto_backend(), loop_backend()
if crypto == "cryptg":
encryption = ("cryptg", "cryptg", True)
elif crypto == "libssl":
encryption = ("libssl", "libssl (cryptg is not installed)", True)
else:
encryption = ("slow Python crypto", "pure Python, slow: cryptg is missing", False)
event_loop = ("uvloop", "uvloop", True) if loop == "uvloop" else ("no uvloop", f"{loop} loop: uvloop is not running", False)
transfers = (f"parallel Γ{connections}", f"on, {connections} connections", True) if parallel else ("parallel off", "off (FAST_TRANSFER=0)", False)
checks = [encryption, event_loop, transfers]
if stream is not None:
if not parallel:
checks.append(("no streaming", "off: it needs the parallel transfer", False))
elif stream:
checks.append(("streaming", "on: big files are uploaded while they download", True))
else:
checks.append(("no streaming", "off (STREAM_RENAME=0)", False))
return checks
def speed_summary(parallel: bool, connections: int, stream: Optional[bool] = None) -> str:
"""One line for the start-up notice: ``cryptg β
Β· uvloop β
Β· parallel Γ8 β
Β· streaming β
``."""
return " Β· ".join(f"{short} {'β
' if fine else 'β οΈ'}" for short, _detail, fine in speed_checks(parallel, connections, stream))
def format_speed(parallel: bool, connections: int, stats: Any, stream: Optional[bool] = None) -> str:
"""The Speed section of /status: what is switched on, and how the transfers since the start went."""
labels = ("π Encryption", "π Event loop", "π Parallel transfer", "β¬β¬ Streaming rename")
lines = ["β‘ <b>Speed</b>"]
for label, (_short, detail, fine) in zip(labels, speed_checks(parallel, connections, stream)):
lines.append(f"{label}: {esc(detail)} {'β
' if fine else 'β οΈ'}")
small = f"{BIG_FILE_THRESHOLD // 2 ** 20} MB"
lines.append(f"β¬ <b>Downloads:</b> {stats.fast_down} parallel Β· {stats.standard_down} standard")
lines.append(f"β¬ <b>Uploads:</b> {stats.fast_up} parallel Β· {stats.small_up} up to {small} Β· {stats.standard_up} standard")
if stream is not None:
lines.append(f"β¬β¬ <b>Streamed renames:</b> {stats.streamed}")
if stats.fallbacks:
lines.append(
f"β οΈ The parallel transfer failed {stats.fallbacks}Γ and the standard, slower one took over. "
f"Last: <code>{esc(clip(stats.last_fallback, 160))}</code>"
)
if stats.stream_fallbacks:
lines.append(
f"β οΈ Streaming failed {stats.stream_fallbacks}Γ and those files were saved first and then uploaded. "
f"Last: <code>{esc(clip(stats.last_stream_fallback, 160))}</code>"
)
return "\n".join(lines)
def format_status(ping_ms: Optional[float], dc_id: Optional[int], telethon_version: str, speed: str = "") -> str:
ping = f"{ping_ms:.0f} ms" if ping_ms is not None else "n/a"
dc = f" (DC {dc_id})" if dc_id else ""
return (
"π’ <b>Bot status</b>\n\n"
f"β± <b>Uptime:</b> {human_uptime(uptime())}\n"
f"π <b>Ping:</b> {ping}{dc}\n"
f"π€ <b>Version:</b> v{__version__}\n"
f"π Python {platform.python_version()} Β· Telethon {telethon_version}"
+ (f"\n\n{speed}" if speed else "")
)
async def format_stats(users: int, files: int, total_bytes: int, running: int, waiting: int, temp_dir: str) -> str:
cpu = await cpu_percent()
used, total = memory_usage()
d_used, d_total, d_free = disk_usage(temp_dir)
rx, tx = net_since_start()
rss = psutil.Process().memory_info().rss
ram_pct = used / total * 100 if total else 0.0
disk_pct = d_used / d_total * 100 if d_total else 0.0
return (
"π <b>System stats</b>\n\n"
f"π₯ <b>Users:</b> {users}\n"
f"π <b>Files renamed:</b> {files} ({human_size(total_bytes)})\n"
f"π§΅ <b>Queue:</b> {running} running Β· {waiting} waiting\n\n"
f"π₯ <b>CPU:</b> {cpu:.1f}% of {cpu_cores():g} core(s)\n"
f"π§ <b>RAM:</b> {human_size(used)} / {human_size(total)} ({ram_pct:.1f}%) Β· bot {human_size(rss)}\n"
f"πΎ <b>Disk:</b> {human_size(d_used)} / {human_size(d_total)} ({disk_pct:.1f}%) Β· {human_size(d_free)} free\n"
f"π <b>Network since start:</b> β {human_size(rx)} Β· β {human_size(tx)}"
)
|