Files
SDR-Rover/experiments/lab051_receiver_profile.py
2026-09-08 16:04:09 +03:00

247 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Lab051: воспроизводимое профилирование приёмника, без оборудования.
Измерение времени отделено от медленного наблюдения массивов/tracemalloc.
Классификация valid/false происходит только после штатного PHY/FEC/CRC.
"""
from __future__ import annotations
from collections import defaultdict
from contextlib import contextmanager
from dataclasses import asdict, dataclass
import ctypes
from ctypes import wintypes
import json
from pathlib import Path
import statistics
import sys
import time
import tracemalloc
import weakref
if __package__ in (None, ""):
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import numpy as np
from PIL import Image
from experiments import lab046c_offline_recovery as offline
from experiments import lab050_video_stream as video
from experiments.lab050_streaming_receiver import streaming_mode
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "data/processed/lab051"
lab045 = video.phy.lab045
lab043 = lab045.lab044.lab043
def process_memory() -> dict:
"""Working set Windows без psutil; чужие процессы не перечисляются."""
if sys.platform != "win32":
return {"rss_bytes": None, "peak_rss_bytes": None}
class Counters(ctypes.Structure):
_fields_ = [("cb", wintypes.DWORD), ("PageFaultCount", wintypes.DWORD)] + [
(name, ctypes.c_size_t) for name in (
"PeakWorkingSetSize", "WorkingSetSize", "QuotaPeakPagedPoolUsage",
"QuotaPagedPoolUsage", "QuotaPeakNonPagedPoolUsage", "QuotaNonPagedPoolUsage",
"PagefileUsage", "PeakPagefileUsage")]
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
psapi = ctypes.WinDLL("psapi", use_last_error=True)
kernel.GetCurrentProcess.restype = wintypes.HANDLE
psapi.GetProcessMemoryInfo.argtypes = [wintypes.HANDLE, ctypes.POINTER(Counters), wintypes.DWORD]
counters = Counters()
counters.cb = ctypes.sizeof(counters)
if not psapi.GetProcessMemoryInfo(kernel.GetCurrentProcess(), ctypes.byref(counters), counters.cb):
return {"rss_bytes": None, "peak_rss_bytes": None, "error": ctypes.get_last_error()}
return {"rss_bytes": counters.WorkingSetSize, "peak_rss_bytes": counters.PeakWorkingSetSize}
@dataclass
class Stage:
calls: int = 0
seconds: float = 0.0
exclusive_seconds: float = 0.0
class StageProfiler:
"""Только последовательное профилирование; подмены возвращаются в finally."""
def __init__(self):
self.rows = []
self.current = None
self.shared = defaultdict(Stage)
self.stack = []
self.examples = {}
@contextmanager
def measure(self, *, optimized: bool = False):
patches = []
targets = (
(offline, "_correlation_scores", "coarse_localization"),
(lab043, "_refine_frame_start", "refinement"),
(lab043, "_matched_waveform", "matched_filter"),
(lab043, "_preamble_timing_offset", "timing"),
(lab043, "_timing_offsets", "timing_fallback"),
(lab043, "_estimate_cfo_gain", "cfo_gain"),
(lab043, "_evaluate_symbols", "phase_diagnostics"),
(lab045, "decode_phy_header_soft", "phy_header"),
(lab045, "analyze_post_preamble_soft", "body_pipeline"),
(lab045.conv, "viterbi_decode_soft", "viterbi"),
(lab045, "parse_packet", "crc_parser"),
)
if optimized:
from experiments import lab051_streaming_receiver as fast
targets += ((fast.BlockFrontEnd, "refine", "refinement"),
(fast.BlockFrontEnd, "coarse_scores", "coarse_localization"),
(fast.BlockFrontEnd, "matched_window", "matched_filter"),
(fast.BlockFrontEnd, "timing", "timing"),
(fast, "fast_viterbi", "viterbi"))
def wrap(function, name):
def measured(*args, **kwargs):
scope = self.shared if self.current is None else self.current
stage = scope[name]
started = time.perf_counter()
entry = [0.0]
self.stack.append(entry)
try:
return function(*args, **kwargs)
finally:
elapsed = time.perf_counter() - started
self.stack.pop()
if self.stack:
self.stack[-1][0] += elapsed
stage.calls += 1
stage.seconds += elapsed
stage.exclusive_seconds += elapsed - entry[0]
return measured
for owner, name, stage in targets:
original = getattr(owner, name)
patches.append((owner, name, original))
setattr(owner, name, wrap(original, stage))
original_decode = offline.decode_candidate_from_memmap
def candidate(raw, sample_count, coarse, number, **kwargs):
self.current = defaultdict(Stage)
started = time.perf_counter()
try:
outcome = original_decode(raw, sample_count, coarse, number, **kwargs)
elapsed = time.perf_counter() - started
kind = "valid" if outcome.accepted else "false"
self.rows.append({"kind": kind, "seconds": elapsed, "reason": outcome.reason,
"offset": coarse.sample_offset, "refined": outcome.refined_start_sample,
"stages": {name: asdict(row) for name, row in self.current.items()}})
# По одному примеру после решения, только для отдельного replay.
if kind not in self.examples and (kind == "valid" or outcome.reason.startswith("PHY_HEADER_MAGIC")):
self.examples[kind] = (raw.copy(), sample_count, coarse)
return outcome
finally:
self.current = None
offline.decode_candidate_from_memmap = candidate
try:
yield self
finally:
offline.decode_candidate_from_memmap = original_decode
for owner, name, original in reversed(patches):
setattr(owner, name, original)
def summary(self) -> dict:
result = {"shared_stages": {name: asdict(row) for name, row in self.shared.items()}}
for kind in ("valid", "false"):
selected = [row for row in self.rows if row["kind"] == kind]
stages = defaultdict(Stage)
for row in selected:
for name, stage in row["stages"].items():
stages[name].calls += stage["calls"]
stages[name].seconds += stage["seconds"]
stages[name].exclusive_seconds += stage["exclusive_seconds"]
times = [row["seconds"] for row in selected]
result[kind] = {"count": len(times), "mean_seconds": statistics.mean(times) if times else None,
"median_seconds": statistics.median(times) if times else None,
"total_seconds": sum(times),
"stages": {name: asdict(row) for name, row in stages.items()}}
return result
def array_observation(function) -> dict:
"""Нижняя граница числа массивов в локальных переменных проектного Python.
Временные выражения и внутренности C/SciPy не видны line tracer-у.
tracemalloc отдельно считает пик отслеживаемой памяти, включая NumPy.
Эти проходы НИКОГДА не используются как измерение производительности.
"""
seen = {}
counters = {"observed_array_objects": 0, "observed_owned_arrays": 0, "observed_owned_bytes_total": 0}
def trace(frame, event, argument):
filename = frame.f_code.co_filename.replace("\\", "/")
if not ("/experiments/" in filename or "/protocol/" in filename):
return None
if event in ("line", "return"):
for value in frame.f_locals.values():
if not isinstance(value, np.ndarray):
continue
identity = id(value)
if identity in seen and seen[identity]() is value:
continue
seen[identity] = weakref.ref(value)
counters["observed_array_objects"] += 1
if value.flags.owndata:
counters["observed_owned_arrays"] += 1
counters["observed_owned_bytes_total"] += value.nbytes
return trace
previous = sys.gettrace()
try:
sys.settrace(trace)
function()
finally:
sys.settrace(previous)
tracemalloc.start()
try:
function()
current, peak = tracemalloc.get_traced_memory()
finally:
tracemalloc.stop()
return {**counters, "tracemalloc_retained_bytes": current, "tracemalloc_peak_bytes": peak,
"array_count_is_lower_bound": True}
def run_functional_tests() -> None:
assert video.phy.PhyMode(500_000).samples_per_symbol == 16
def main() -> None:
OUTPUT.mkdir(parents=True, exist_ok=True)
with Image.open(ROOT / "data/raw/lab011_scene.jpg") as source:
sequence = video.encode_sequence(source, video.PROFILES[2], 3)
profiler = StageProfiler()
with profiler.measure():
benchmark = video.benchmark_receiver(sequence, 500_000)
result = {"benchmark": benchmark, "profile": profiler.summary(), "rf_tx": 0}
(OUTPUT / "baseline_profile.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result["profile"], indent=2), flush=True)
# Небольшие ограниченные окна, не полная запись. В Git не включать.
metadata = {}
for kind, (raw, count, coarse) in profiler.examples.items():
np.save(OUTPUT / f"baseline_{kind}_window.npy", raw)
metadata[kind] = {"sample_count": count, "offset": coarse.sample_offset, "score": coarse.score}
with streaming_mode(video.phy.PhyMode(500_000)):
call = lambda: offline.decode_candidate_from_memmap(
raw, count, coarse, 1,
read_iq_window=lambda data, start, end: data[2*start:2*end].view(np.complex64))
result.setdefault("array_observation", {})[kind] = array_observation(call)
(OUTPUT / "baseline_examples.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
result["process_memory_after_profiling"] = process_memory()
(OUTPUT / "baseline_profile.json").write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result["array_observation"]), flush=True)
if __name__ == "__main__":
main()