mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-08-09 02:12:53 +02:00
feat: add memory_debug endpoint for memory leak diagnostics and improve SSL context handling for GitHub requests
This commit is contained in:
@@ -728,6 +728,8 @@ class SQLiteHandler:
|
||||
"""Return CRC error records within the given time window (chronological)."""
|
||||
try:
|
||||
cutoff = time.time() - (hours * 3600)
|
||||
if limit is None:
|
||||
limit = 1000
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
query = """
|
||||
@@ -735,10 +737,9 @@ class SQLiteHandler:
|
||||
FROM crc_errors
|
||||
WHERE timestamp > ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
if limit:
|
||||
query += f" LIMIT {int(limit)}"
|
||||
rows = conn.execute(query, (cutoff,)).fetchall()
|
||||
rows = conn.execute(query, (cutoff, int(limit))).fetchall()
|
||||
return [{"timestamp": r["timestamp"], "count": r["count"]} for r in reversed(rows)]
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get CRC error history: {e}")
|
||||
@@ -1116,21 +1117,21 @@ class SQLiteHandler:
|
||||
try:
|
||||
cutoff = time.time() - (hours * 3600)
|
||||
|
||||
if limit is None:
|
||||
limit = 1000
|
||||
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
# Build query with optional limit
|
||||
query = """
|
||||
SELECT timestamp, noise_floor_dbm
|
||||
FROM noise_floor
|
||||
WHERE timestamp > ?
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
|
||||
if limit:
|
||||
query += f" LIMIT {int(limit)}"
|
||||
|
||||
measurements = conn.execute(query, (cutoff,)).fetchall()
|
||||
measurements = conn.execute(query, (cutoff, int(limit))).fetchall()
|
||||
|
||||
# Reverse to get chronological order (oldest to newest)
|
||||
result = [
|
||||
@@ -1348,6 +1349,9 @@ class SQLiteHandler:
|
||||
) -> List[dict]:
|
||||
|
||||
try:
|
||||
if limit is None:
|
||||
limit = 500
|
||||
|
||||
with sqlite3.connect(self.sqlite_path) as conn:
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
|
||||
@@ -1197,6 +1197,70 @@ class APIEndpoints:
|
||||
logger.error(f"Error getting hardware stats: {e}")
|
||||
return self._error(e)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def memory_debug(self):
|
||||
"""Diagnostic endpoint: show what is actually holding memory.
|
||||
|
||||
First call starts tracemalloc and takes a baseline snapshot.
|
||||
Subsequent calls compare against the baseline and return the top
|
||||
allocations that have *grown* since startup — i.e. the real leaks.
|
||||
"""
|
||||
import tracemalloc
|
||||
|
||||
if not tracemalloc.is_tracing():
|
||||
tracemalloc.start(10)
|
||||
# Store baseline snapshot for future diffs
|
||||
self._tracemalloc_baseline = tracemalloc.take_snapshot()
|
||||
return self._success({
|
||||
"status": "started",
|
||||
"message": "tracemalloc started — call again after some time to see growth",
|
||||
})
|
||||
|
||||
current = tracemalloc.take_snapshot()
|
||||
baseline = getattr(self, "_tracemalloc_baseline", None)
|
||||
|
||||
# Top 20 allocations right now (grouped by file + line)
|
||||
top_current = current.statistics("lineno")[:20]
|
||||
current_stats = []
|
||||
for stat in top_current:
|
||||
current_stats.append({
|
||||
"file": str(stat.traceback),
|
||||
"size_kb": round(stat.size / 1024, 1),
|
||||
"count": stat.count,
|
||||
})
|
||||
|
||||
result = {"current_top_20": current_stats}
|
||||
|
||||
# If we have a baseline, show what GREW (the actual leaks)
|
||||
if baseline:
|
||||
diff = current.compare_to(baseline, "lineno")
|
||||
growth = [d for d in diff if d.size_diff > 0]
|
||||
growth.sort(key=lambda d: d.size_diff, reverse=True)
|
||||
growth_stats = []
|
||||
for stat in growth[:20]:
|
||||
growth_stats.append({
|
||||
"file": str(stat.traceback),
|
||||
"size_diff_kb": round(stat.size_diff / 1024, 1),
|
||||
"count_diff": stat.count_diff,
|
||||
"current_size_kb": round(stat.size / 1024, 1),
|
||||
})
|
||||
result["growth_since_baseline"] = growth_stats
|
||||
|
||||
# Also include process-level memory for context
|
||||
try:
|
||||
import resource
|
||||
rusage = resource.getrusage(resource.RUSAGE_SELF)
|
||||
result["rss_mb"] = round(rusage.ru_maxrss / 1024, 1) # macOS=bytes, Linux=KB
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
traced_current, traced_peak = tracemalloc.get_traced_memory()
|
||||
result["traced_current_mb"] = round(traced_current / (1024 * 1024), 2)
|
||||
result["traced_peak_mb"] = round(traced_peak / (1024 * 1024), 2)
|
||||
|
||||
return self._success(result)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
def hardware_processes(self):
|
||||
|
||||
@@ -19,6 +19,7 @@ import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import ssl
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
@@ -43,6 +44,15 @@ PACKAGE_NAME = "pymc_repeater"
|
||||
# How long (seconds) before a cached check result expires
|
||||
CHECK_CACHE_TTL = 600 # 10 minutes
|
||||
|
||||
_github_ssl_ctx: Optional[ssl.SSLContext] = None
|
||||
|
||||
|
||||
def _get_github_ssl_context() -> ssl.SSLContext:
|
||||
global _github_ssl_ctx
|
||||
if _github_ssl_ctx is None:
|
||||
_github_ssl_ctx = ssl.create_default_context()
|
||||
return _github_ssl_ctx
|
||||
|
||||
|
||||
class _RateLimitError(Exception):
|
||||
"""Raised when GitHub returns HTTP 403 due to rate limiting."""
|
||||
@@ -371,6 +381,8 @@ class _UpdateState:
|
||||
def append_line(self, line: str) -> None:
|
||||
with self._lock:
|
||||
self.progress_lines.append(line)
|
||||
if len(self.progress_lines) > 500:
|
||||
self.progress_lines = self.progress_lines[-500:]
|
||||
|
||||
|
||||
_state = _UpdateState()
|
||||
@@ -394,7 +406,8 @@ def _fetch_url(url: str, timeout: int = 10) -> str:
|
||||
headers["Authorization"] = f"Bearer {token}"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
ctx = _get_github_ssl_context() if url.startswith("https") else None
|
||||
with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
|
||||
return resp.read().decode("utf-8", errors="replace")
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code == 403:
|
||||
|
||||
Reference in New Issue
Block a user