perf: zone CPU profiler (PerfLog) + phase-restructured CPU spectrum render

Инструментация (ВРЕМЕННАЯ, не мержить в main):
- PerfLog.pas: зонные таймеры, отчёт раз в 5с в stderr (core% = доля ядра),
  EWSDR_PERF=0 выкл, EWSDR_PERF_FILE=путь — дублировать в файл
- Замеры // PERF: сеть (DDC IQ/wideband), Pluto (rx_conv/tx_resamp/tx_fill),
  DSP (push_iq/fexchange/slices/tx), аудио, UI (тики/draw/paint/gl_swap,
  разбивка draw_spectrum по фазам)
- doc/PERF_PROFILING.md: карта зон, методика, выводы

Оптимизация CPU-рендера спектра (по результатам замеров на эфире):
диагноз — дорога не пиксельная работа, а чередование raw(ScanLine)/
canvas(QPainter) доступа к битмапу на Qt6 (~6 переходов за кадр, каждый
синкает весь битмап; полоса фильтра в тысячи пикселей стоила 4x дороже
полного memcpy кадра). DrawSpectrum перестроен в фазы calc->raw->canvas->raw
(2 перехода): FillBandRaw вместо Canvas.FillRect, DrawSliceFilterMarkers
разделён на raw-полосы + canvas-линии, композиты флагов и альфа-OR в
финальной raw-фазе. Кадр 8.3мс -> 6.1мс; spec_bands 1.87мс -> 0.03мс.
Альфа-OR проход на Qt6 подтверждён нужным (без него canvas-пиксели теряют
альфу и пропадают при блите), сам цикл ~0.08мс — оставлен; тумблер
EWSDR_ALPHA_FIX=0 только для экспериментов. Кисть перед AGC-подписями
теперь ставится явно (была остаточной после перестройки фаз).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 22:07:36 +03:00
co-authored by Claude Fable 5
parent b2fa2756cf
commit 0b4d126dac
12 changed files with 692 additions and 83 deletions
+85
View File
@@ -0,0 +1,85 @@
# CPU-профилирование (ветка perf/cpu-profiling)
Временная инструментация для поиска самых дорогих по CPU мест.
Модуль `PerfLog.pas` + зонные замеры, помеченные комментарием `// PERF`.
**Не мержить в main** — после выводов ветка выбрасывается или чистится.
## Как запустить
```sh
lazbuild -B --ws=qt6 ewsdr.lpr
./bin/x86_64-linux/ewsdr-debug # отчёт в stderr каждые 5 с
EWSDR_PERF_FILE=/tmp/ewsdr-perf.log ./bin/x86_64-linux/ewsdr-debug # + в файл
EWSDR_PERF=0 ./bin/x86_64-linux/ewsdr-debug # выключить замеры
```
## Как читать отчёт
```
[PERF] ===== interval 5.00 s =====
[PERF] zone calls c/s avg_us max_us total_ms core%
[PERF] dsp.push_iq 2929 586 55.1 412 161.4 3.2
```
`core%` — доля одного ядра CPU, потраченная в зоне за интервал. Это главная
колонка: сортируем по ней и смотрим, куда уходит время. `max_us` показывает
выбросы (джиттер), `avg_us` — стоимость одного вызова.
Зоны вложены: `dsp.push_iq` включает `dsp.spectrum0` + `dsp.fex_main` +
`dsp.audio_cb` + `dsp.slices`; `dsp.slices` включает `dsp.slice_fex` и
`audio.write` слайсов; `ui.spectrum_tick` включает `ui.draw_*`, `ui.slice_flags`,
`ui.vfo_overlay`, `ui.web_push`; `dsp.tx_block` включает `pluto.tx_resamp`
(SendDUCIQ зовётся из FOnTXIQ). Разница «родитель минус дети» = ненаблюдаемый
остаток (циклы копирования, мелочь).
Бэкенды: на openHPSDR работает `net.*`, на Pluto вместо них `pluto.*`
(RX-поток Pluto конвертит iio-буфер и кладёт в ту же DSP-очередь).
## Карта зон
| Зона | Поток | Что меряет |
|---|---|---|
| net.ddc_iq | receive | копия IQ-пакета + постановка в DSP-очередь (openHPSDR) |
| net.wideband | receive | сборка wideband-кадра (openHPSDR) |
| pluto.rx_conv | Pluto RX | конверсия iio-буфера int16→24bit BE + отдача в DSP-очередь |
| pluto.tx_resamp | TX DSP | полифазный интерполятор 192k→device-rate + 24→16 + FIFO |
| pluto.tx_fill | Pluto TX | наполнение iio-буфера из FIFO (без блокирующего push) |
| dsp.push_iq | DSP | весь путь пакета: декод 24-бит, аккумуляция, всё ниже |
| dsp.spectrum0 | DSP | скармливание display-блока анализатору WDSP |
| dsp.fex_main | DSP | fexchange0 главного RX-канала |
| dsp.audio_cb | DSP | маршрут RX-аудио (web/радио/звуковуха) |
| dsp.slices | DSP | fan-out всех слайсов целиком |
| dsp.slice_fex | DSP | fexchange0 одного слайс-канала |
| dsp.tx_block | TX DSP | TX-блок целиком |
| disp.get_pixels | display | GetPixels спектр+водопад |
| audio.write | DSP | запись в PortAudio ring (главный + все слайсы) |
| ui.spectrum_tick | UI | кадровый таймер целиком |
| ui.meter_tick | UI | 10 Гц метр-таймер |
| ui.draw_spectrum / ui.draw_waterfall | UI | CPU-рендер кадра в битмап |
| ui.paint_spectrum / ui.paint_waterfall | UI | блит на канву (Qt6) или GL-кадр со SwapBuffers |
| ui.slice_flags | UI | позиционирование + S-метры слайс-флагов |
| ui.vfo_overlay | UI | обновление главного VFO-флага |
| ui.wideband_paint | UI | кадр wideband-панорамы |
| ui.web_push | UI | WebAdapter.PushState |
| ui.spec_grid…spec_alpha | UI | разбивка ui.draw_spectrum ПО ФАЗАМ (после перестройки): grid=ребилд+memcpy сетки, bands=raw-заливки полос, curve=точки+градиент, overlays=canvas-фаза (линии/текст/кривая/маркеры), alpha=композиты флагов+альфа-OR |
| ui.gl_swap | UI | SwapBuffers отдельно — включает ожидание vsync, НЕ чистый CPU |
## Чего зоны НЕ видят
- Общесистемное: Qt event loop, композитор, PortAudio callback-поток
(сам колбэк не инструментирован — он читает ring без работы).
- Для сравнения с общим CPU процесса: `pidstat -p $(pidof ewsdr-debug) 5 -t`
(покажет и раскладку по потокам — сопоставляется с зонами по потокам).
## Методика измерений
Снять отчёты в фиксированных сценариях по ~30 с каждый и сравнить core%:
1. RX, 1 слайс (только VFO A), спектр CPU-рендер.
2. То же + OpenGL (`ewsdr-debug --opengl` или как включается GL-свитч).
3. RX + 34 слайса (Ctrl+ЛКМ) — рост dsp.slices / dsp.slice_fex / audio.write.
4. Разные FPS дисплея и размеры FFT.
5. Открытый web-клиент (ui.web_push, dsp.audio_cb с Opus).
6. Wideband включён/выключен (openHPSDR).
7. Pluto: разные sample rate (576k/1536k/3072k) — pluto.rx_conv и dsp.push_iq
растут линейно с рейтом; TX (MOX/TUN) — pluto.tx_resamp vs dsp.tx_block.