247 lines
11 KiB
Python
247 lines
11 KiB
Python
"""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()
|