From 0b4d126dac3ecab641c7d308eb4574670f8519b0 Mon Sep 17 00:00:00 2001 From: Vladimir Date: Thu, 2 Jul 2026 22:07:36 +0300 Subject: [PATCH] perf: zone CPU profiler (PerfLog) + phase-restructured CPU spectrum render MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Инструментация (ВРЕМЕННАЯ, не мержить в 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 --- AudioOutput.pas | 6 +- HPSDRNetwork.pas | 9 +- MainForm.pas | 21 +++ PerfLog.pas | 313 ++++++++++++++++++++++++++++++++++++++++ PlutoBackend.pas | 12 +- SpectrumView.pas | 273 +++++++++++++++++++++++++---------- SpectrumViewOpengl.pas | 8 +- WDSPEngine.pas | 23 +++ WaterfallViewOpengl.pas | 8 +- WidebandView.pas | 6 +- doc/PERF_PROFILING.md | 85 +++++++++++ ewsdr.lpi | 11 +- 12 files changed, 692 insertions(+), 83 deletions(-) create mode 100644 PerfLog.pas create mode 100644 doc/PERF_PROFILING.md diff --git a/AudioOutput.pas b/AudioOutput.pas index b6b9df2..bc4b7f8 100644 --- a/AudioOutput.pas +++ b/AudioOutput.pas @@ -21,7 +21,8 @@ unit AudioOutput; interface uses - Classes, SysUtils, Math, SyncObjs, DynLibs; + Classes, SysUtils, Math, SyncObjs, DynLibs, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры const MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR @@ -628,10 +629,13 @@ procedure TAudioOutput.Write(const Left, Right: array of Single; Count: Integer) // Порядок: сначала пишем данные в FBuf, потом обновляем FInPt (memory barrier) var i: Integer; + T0: Int64; // PERF begin if not FOpen then Exit; + T0 := PerfNow; // PERF for i := 0 to Count - 1 do WriteDouble(Left[i], Right[i]); + PerfAdd(pzAudioWrite, T0); // PERF: запись блока в PA ring (все экземпляры) end; end. diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index d5114b8..193b593 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -19,7 +19,8 @@ uses {$ELSE} Sockets, BaseUnix, {$ENDIF} - HPSDRProtocol, SyncObjs, Settings, RadioBackend; + HPSDRProtocol, SyncObjs, Settings, RadioBackend, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры const DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun @@ -1093,11 +1094,14 @@ procedure THPSDRNetwork.HandleDDCIQ(const Buf: array of Byte; Len: Integer; DDCIdx: Integer); var Pkt: TDDCIQPacket; + T0: Int64; // PERF begin if not Assigned(FOnDDCIQ) then Exit; + T0 := PerfNow; // PERF FillChar(Pkt, SizeOf(Pkt), 0); Move(Buf[0], Pkt, Min(Len, SizeOf(Pkt))); FOnDDCIQ(DDCIdx, Pkt); // прямой вызов из потока — UI не трогает + PerfAdd(pzNetDDCIQ, T0); // PERF: копия пакета + постановка в DSP-очередь end; procedure THPSDRNetwork.HandleMicData(const Buf: array of Byte; Len: Integer); @@ -1114,9 +1118,11 @@ procedure THPSDRNetwork.HandleWidebandData(const Buf: array of Byte; Len: Intege var Seq: LongWord; I, SamplesInPacket, TargetCount, S: Integer; + T0: Int64; // PERF begin if (not FWidebandEnabled) or (ADCIdx <> FWidebandADC) or (FWBSampleBits <> 16) or (Len < 6) then Exit; + T0 := PerfNow; // PERF SamplesInPacket := (Len - 4) div 2; if SamplesInPacket <= 0 then Exit; @@ -1157,6 +1163,7 @@ begin FWBFrameCount := 0; FWBSeqValid := False; end; + PerfAdd(pzNetWideband, T0); // PERF: сборка wideband-кадра + колбэк end; // --------------------------------------------------------------------------- diff --git a/MainForm.pas b/MainForm.pas index 5dfc88a..c9799a2 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -37,6 +37,7 @@ uses WidebandView, StatusBar, PlatformUtils, + PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры WinFirewall, BoardUtils, WisdomBuilder, UISync, FlatEdit, FMRepeater, @@ -3168,7 +3169,10 @@ var PeakTarget, MinTarget: Double; PeakDiff, MinDiff: Double; PeakAlpha, MinAlpha: Double; + PerfT0: Int64; // PERF begin + PerfT0 := PerfNow; // PERF + try if not FController.FRunning then begin if PbSMeterRight <> nil then PbSMeterRight.Invalidate; @@ -3222,6 +3226,9 @@ begin else SetStatusText(5, 'TX idle'); end; + finally + PerfAdd(pzUiMeterTick, PerfT0); // PERF: тик метра целиком (10 Гц) + end; end; procedure TMainForm.SpectrumTimerTick(Sender: TObject); @@ -3229,7 +3236,10 @@ var NowTick: QWord; ElapsedTick: QWord; I: Integer; + PerfT0, PerfTz: Int64; // PERF begin + PerfT0 := PerfNow; // PERF + try // Синхронизируем размеры если размер панели изменился (после ресайза). // Важно: проверяем только видимые панели — когда спектр скрыт, // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0, @@ -3302,13 +3312,16 @@ begin // Обновляем оверлей если видим if Assigned(FVfoOverlay) and FVfoOverlay.Visible then begin + PerfTz := PerfNow; // PERF FVfoOverlay.UpdateSMeter(FController.FLastSMeter); FVfoOverlay.UpdateVfo(FController.FVfoA); PushVfoOverlayExtState; PositionVfoOverlay; + PerfAdd(pzUiVfoOverlay, PerfTz); // PERF: обновление главного VFO-флага end; if Length(FSliceFlags) > 0 then begin + PerfTz := PerfNow; // PERF PositionSliceFlags; // держим слайс-флаги на частоте // S-метр слайсов троттлим до ~10 Гц: каждое изменение метра = софтовый // ре-рендер флага (дорого), 60 Гц там не нужно. @@ -3319,6 +3332,7 @@ begin if FSliceFlags[I] <> nil then FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId)); end; + PerfAdd(pzUiSliceFlags, PerfTz); // PERF: позиционирование + S-метры слайс-флагов end; // Панорама и водопад теперь рисуются независимо по факту новых кадров. // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. @@ -3385,7 +3399,14 @@ begin // Исходящее зеркало состояния + спектр/водопад в web — в адаптере (всё из // FController, включая последний кадр пикселей). Сервер сам гейтит по FRunning. if Assigned(FWebAdapter) then + begin + PerfTz := PerfNow; // PERF FWebAdapter.PushState; + PerfAdd(pzUiWebPush, PerfTz); // PERF: web-зеркало состояния + кадры клиентам + end; + finally + PerfAdd(pzUiSpectrumTick, PerfT0); // PERF: тик спектра целиком (кадровый таймер) + end; end; // =========================================================================== diff --git a/PerfLog.pas b/PerfLog.pas new file mode 100644 index 0000000..28281ca --- /dev/null +++ b/PerfLog.pas @@ -0,0 +1,313 @@ +unit PerfLog; + +{ =========================================================================== + PerfLog — лёгкий зонный CPU-профилировщик для поиска горячих точек. + + ВРЕМЕННЫЙ ОТЛАДОЧНЫЙ МОДУЛЬ (ветка perf/cpu-profiling) — не мержить в main. + + Использование в горячем коде: + var t0: Int64; + ... + t0 := PerfNow; + <работа> + PerfAdd(pzDspFexMain, t0); + + Накладные расходы: два чтения монотонных часов + несколько сложений. + Никаких строк/аллокаций/локов в горячем пути. Каждая зона пишется из + одного потока (single-writer), поэтому обычные Int64-инкременты безопасны; + репортер читает значения racy — для отладки это приемлемо. + + Отчёт раз в PERF_REPORT_SEC секунд в StdErr (и в файл, если задан + EWSDR_PERF_FILE). Отключение: переменная окружения EWSDR_PERF=0. + + Колонки отчёта: + calls — вызовов за интервал + c/s — вызовов в секунду + avg — среднее время на вызов, мкс + max — максимум за интервал, мкс + total — суммарно за интервал, мс + core% — доля одного ядра CPU (total / интервал * 100) + =========================================================================== } + +{$mode objfpc}{$H+} + +interface + +type + TPerfZone = ( + // Сетевой поток (приём UDP от радио) + pzNetDDCIQ, // HandleDDCIQ: разбор IQ-пакета + постановка в очередь + pzNetWideband, // HandleWidebandData: wideband-пакеты ADC + + // Pluto (libiio, свои RX/TX потоки — мимо HPSDRNetwork) + pzPlutoRxConv, // RX: конверсия iio-буфера int16→24bit BE + FOnDDCIQ + pzPlutoTxResamp, // TX: полифазный интерполятор 192k→device-rate + FIFO + pzPlutoTxFill, // TX: наполнение iio-буфера из FIFO (без блокирующего push) + + // DSP-поток (WDSPEngine) + pzDspPushIQ, // PushIQItemToDSP целиком (декод 24-бит + акк + всё ниже) + pzDspSpectrum0, // Spectrum0() — скармливание display-блока анализатору + pzDspFexMain, // fexchange0 главного RX-канала + pzDspAudioCb, // копирование аудио + FOnAudio (маршрут аудио) + pzDspSlices, // ProcessSlices целиком (все слайсы) + pzDspSliceFex, // fexchange0 одного слайс-канала + pzDspTxBlock, // ProcessTXBlock (TX-тракт) + + // Display-поток (WDSPEngine.UpdateSpectrum) + pzDispGetPixels, // GetPixels спектр+водопад из анализатора WDSP + + // Аудио-выход (PortAudio ring, из DSP-потока) + pzAudioWrite, // TAudioOutput.Write (все экземпляры: главный + слайсы) + + // UI-поток (главная форма, таймеры, отрисовка) + pzUiSpectrumTick, // SpectrumTimerTick целиком + pzUiMeterTick, // MeterTimerTick целиком + pzUiDrawSpectrum, // SpectrumView.DrawSpectrum (CPU или GL) + pzUiDrawWaterfall, // SpectrumView.DrawWaterfall + pzUiPaintSpectrum, // PaintSpectrum: блит битмапа на канву (Qt6!) + pzUiPaintWaterfall,// PaintWaterfall: блит битмапа на канву (Qt6!) + pzUiSliceFlags, // обновление S-метров слайс-флагов (ре-рендер флага) + pzUiVfoOverlay, // обновление VFO-оверлея (S-метр/частота/позиция) + pzUiWidebandPaint, // отрисовка wideband-панорамы + pzUiWebPush, // WebAdapter.PushState (зеркало состояния + кадры в web) + + // Разбивка CPU-рендера спектра (внутри ui.draw_spectrum) + pzUiSpecGrid, // ребилд кэша сетки + полный memcpy сетки в битмап + pzUiSpecBands, // полоса фильтра + слайс-маркеры + AGC-линии (BlendBand) + pzUiSpecCurve, // расчёт точек кривой + градиент под кривой + Polyline + pzUiSpecOverlays, // края/VFO/маркеры/бэндплан/флаги (DrawOverlay в битмап) + pzUiSpecAlpha, // финальный альфа-OR проход по всему битмапу + pzUiGlSwap // SwapBuffers (GL) — включает ожидание vsync! + ); + +const + PERF_REPORT_SEC = 5; + + PerfZoneName: array[TPerfZone] of string = ( + 'net.ddc_iq', + 'net.wideband', + 'pluto.rx_conv', + 'pluto.tx_resamp', + 'pluto.tx_fill', + 'dsp.push_iq', + 'dsp.spectrum0', + 'dsp.fex_main', + 'dsp.audio_cb', + 'dsp.slices', + 'dsp.slice_fex', + 'dsp.tx_block', + 'disp.get_pixels', + 'audio.write', + 'ui.spectrum_tick', + 'ui.meter_tick', + 'ui.draw_spectrum', + 'ui.draw_waterfall', + 'ui.paint_spectrum', + 'ui.paint_waterfall', + 'ui.slice_flags', + 'ui.vfo_overlay', + 'ui.wideband_paint', + 'ui.web_push', + 'ui.spec_grid', + 'ui.spec_bands', + 'ui.spec_curve', + 'ui.spec_overlays', + 'ui.spec_alpha', + 'ui.gl_swap' + ); + +var + PerfEnabled: Boolean = True; + +// Монотонные микросекунды. +function PerfNow: Int64; + +// Прибавить (PerfNow - StartUs) к зоне. Вызывать в конце измеряемого участка. +procedure PerfAdd(Zone: TPerfZone; StartUs: Int64); + +implementation + +uses + SysUtils, Classes + {$IFDEF WINDOWS}, Windows{$ENDIF} + {$IFDEF LINUX}, BaseUnix, Linux{$ENDIF} + {$IF DEFINED(UNIX) AND NOT DEFINED(LINUX)}, BaseUnix, Unix{$ENDIF}; + +type + TZoneAcc = record + Count: Int64; + TotalUs: Int64; + MaxUs: Int64; + end; + +var + Zones: array[TPerfZone] of TZoneAcc; + +{$IFDEF WINDOWS} +var + QPCFreq: Int64 = 0; + +function PerfNow: Int64; +var C: Int64; +begin + if QPCFreq = 0 then QueryPerformanceFrequency(QPCFreq); + QueryPerformanceCounter(C); + Result := (C * 1000000) div QPCFreq; +end; +{$ELSE} +{$IFDEF LINUX} +function PerfNow: Int64; +var ts: TTimeSpec; +begin + clock_gettime(CLOCK_MONOTONIC, @ts); + Result := Int64(ts.tv_sec) * 1000000 + ts.tv_nsec div 1000; +end; +{$ELSE} +function PerfNow: Int64; +var tv: TTimeVal; +begin + fpGetTimeOfDay(@tv, nil); + Result := Int64(tv.tv_sec) * 1000000 + tv.tv_usec; +end; +{$ENDIF} +{$ENDIF} + +procedure PerfAdd(Zone: TPerfZone; StartUs: Int64); +var El: Int64; +begin + if not PerfEnabled then Exit; + El := PerfNow - StartUs; + if El < 0 then El := 0; + Inc(Zones[Zone].Count); + Inc(Zones[Zone].TotalUs, El); + if El > Zones[Zone].MaxUs then Zones[Zone].MaxUs := El; +end; + +// --------------------------------------------------------------------------- +// Репортер +// --------------------------------------------------------------------------- + +type + TPerfReporter = class(TThread) + private + FFile: TextFile; + FFileOpen: Boolean; + procedure Emit(const S: string); + procedure Report(ElapsedUs: Int64); + protected + procedure Execute; override; + public + constructor Create; + end; + +var + Reporter: TPerfReporter = nil; + +constructor TPerfReporter.Create; +var FN: string; +begin + FFileOpen := False; + FN := GetEnvironmentVariable('EWSDR_PERF_FILE'); + if FN <> '' then + begin + AssignFile(FFile, FN); + try + Rewrite(FFile); + FFileOpen := True; + except + FFileOpen := False; + end; + end; + FreeOnTerminate := False; + inherited Create(False); +end; + +procedure TPerfReporter.Emit(const S: string); +begin + try + WriteLn(StdErr, S); + except + end; + if FFileOpen then + begin + WriteLn(FFile, S); + Flush(FFile); + end; +end; + +procedure TPerfReporter.Report(ElapsedUs: Int64); +var + Z: TPerfZone; + Snap: array[TPerfZone] of TZoneAcc; + Sec: Double; + Any: Boolean; +begin + // Снимок + сброс (racy, но для отладки достаточно). + for Z := Low(TPerfZone) to High(TPerfZone) do + begin + Snap[Z] := Zones[Z]; + Zones[Z].Count := 0; + Zones[Z].TotalUs := 0; + Zones[Z].MaxUs := 0; + end; + + Any := False; + for Z := Low(TPerfZone) to High(TPerfZone) do + if Snap[Z].Count > 0 then begin Any := True; Break; end; + if not Any then Exit; + + Sec := ElapsedUs / 1000000.0; + Emit(Format('[PERF] ===== interval %.2f s =====', [Sec])); + Emit(Format('[PERF] %-18s %8s %8s %8s %8s %9s %6s', + ['zone', 'calls', 'c/s', 'avg_us', 'max_us', 'total_ms', 'core%'])); + for Z := Low(TPerfZone) to High(TPerfZone) do + if Snap[Z].Count > 0 then + Emit(Format('[PERF] %-18s %8d %8.0f %8.1f %8d %9.1f %6.1f', + [PerfZoneName[Z], + Snap[Z].Count, + Snap[Z].Count / Sec, + Snap[Z].TotalUs / Snap[Z].Count, + Snap[Z].MaxUs, + Snap[Z].TotalUs / 1000.0, + Snap[Z].TotalUs / ElapsedUs * 100.0])); +end; + +procedure TPerfReporter.Execute; +var + LastUs, NowUs: Int64; + Waited: Integer; +begin + LastUs := PerfNow; + while not Terminated do + begin + // Спим мелкими шагами, чтобы быстро завершаться. + Waited := 0; + while (Waited < PERF_REPORT_SEC * 1000) and not Terminated do + begin + Sleep(100); + Inc(Waited, 100); + end; + if Terminated then Break; + NowUs := PerfNow; + Report(NowUs - LastUs); + LastUs := NowUs; + end; +end; + +initialization + if GetEnvironmentVariable('EWSDR_PERF') = '0' then + PerfEnabled := False; + if PerfEnabled then + Reporter := TPerfReporter.Create; + +finalization + if Reporter <> nil then + begin + Reporter.Terminate; + Reporter.WaitFor; + if Reporter.FFileOpen then + CloseFile(Reporter.FFile); + Reporter.Free; + end; + +end. diff --git a/PlutoBackend.pas b/PlutoBackend.pas index 6222a22..4c7255c 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -26,7 +26,8 @@ interface uses Classes, SysUtils, ctypes, - RadioBackend, IIOBindings, HPSDRProtocol, Settings; + RadioBackend, IIOBindings, HPSDRProtocol, Settings, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры type { TPlutoBackend } @@ -195,6 +196,7 @@ var outc: Integer; vi, vq: LongInt; u: LongWord; + T0: Int64; // PERF procedure FlushPkt; begin @@ -220,6 +222,7 @@ begin if Terminated then Break; if n <= 0 then begin Sleep(2); Continue; end; + T0 := PerfNow; // PERF: только конверсия, без блокирующего refill step := iio_buffer_step(FBk.FRxBuf); pFirst := iio_buffer_first(FBk.FRxBuf, FBk.FRxChI); pEnd := iio_buffer_end(FBk.FRxBuf); @@ -246,6 +249,7 @@ begin Inc(p, step); end; if outc > 0 then FlushPkt; // хвост блока + PerfAdd(pzPlutoRxConv, T0); // PERF: int16→24bit BE + FOnDDCIQ на весь iio-буфер end; end; @@ -262,10 +266,12 @@ var pFirst, pEnd, p: PByte; h: Integer; n: ptrint; + T0: Int64; // PERF begin while not Terminated do begin if FBk.FTxBuf = nil then begin Sleep(5); Continue; end; + T0 := PerfNow; // PERF step := iio_buffer_step(FBk.FTxBuf); pFirst := iio_buffer_first(FBk.FTxBuf, FBk.FTxChI); pEnd := iio_buffer_end(FBk.FTxBuf); @@ -291,6 +297,7 @@ begin Inc(p, step); end; LeaveCriticalSection(FBk.FTxLock); + PerfAdd(pzPlutoTxFill, T0); // PERF: наполнение буфера (без блокирующего push) if Terminated then Break; n := iio_buffer_push(FBk.FTxBuf); // блокирует → пейсинг на device-rate @@ -853,8 +860,10 @@ procedure TPlutoBackend.SendDUCIQ(const IData, QData: array of Integer); var i, p, k, t, L, taps, base, n: Integer; ai, aq: Double; + T0: Int64; // PERF begin if (not FTxStreaming) or (FTxRatio < 1) or (Length(FTxCoef) = 0) then Exit; + T0 := PerfNow; // PERF n := Length(IData); if Length(QData) < n then n := Length(QData); L := FTxRatio; @@ -893,6 +902,7 @@ begin finally LeaveCriticalSection(FTxLock); end; + PerfAdd(pzPlutoTxResamp, T0); // PERF: полифазный интерполятор + 24→16 + FIFO end; procedure TPlutoBackend.ClearDUCIQQueue; diff --git a/SpectrumView.pas b/SpectrumView.pas index d937438..7477034 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -21,7 +21,8 @@ uses Classes, SysUtils, Graphics, ExtCtrls, Controls, Math, AppTheme, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, - WaterfallView, SMeterView, RulerView; + WaterfallView, SMeterView, RulerView, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры const SV_CLR_BG = TColor($00101010); @@ -109,10 +110,15 @@ type procedure SetFillSpectrum(V: Boolean); procedure DrawSpectrumGradient(const SpPts: array of TPoint; W, H: Integer); procedure DrawMarkerLine(C: TCanvas; W, H: Integer); - procedure DrawSliceFilterMarkers(C: TCanvas; W, H: Integer); + // Слайс-маркеры разделены на raw-часть (полосы, ScanLine) и canvas-часть + // (линии/буквы): DrawSpectrum группирует raw- и canvas-операции, чтобы + // не гонять Qt6-битмап между режимами доступа (дорогая синхронизация). + procedure DrawSliceFilterBands(W, H: Integer); + procedure DrawSliceFilterLines(C: TCanvas; W, H: Integer); procedure DrawBandLetter(C: TCanvas; X1, X2: Integer; L: Char; Clr: TColor); procedure DrawBeaconMarkers(C: TCanvas; W, H: Integer); procedure BlendBand(X1, X2, H: Integer; R, G, B, Alpha: Byte); + procedure FillBandRaw(X1, X2, H: Integer; Color: TColor); procedure CopyGridToSpectrum(W, H: Integer); procedure DrawADCOverloadOverlay(C: TCanvas; W, H: Integer); procedure CalcFilterBandX(VfoFreq: Double; W: Integer; @@ -267,6 +273,11 @@ type implementation +var + // ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 → пропустить альфа-OR + // проход в DrawSpectrum (проверка, нужен ли он на Qt6). + GSkipAlphaFix: Boolean = False; + // ════════════════════════════════════════════════════════════════════════════ // Вспомогательные функции // ════════════════════════════════════════════════════════════════════════════ @@ -618,13 +629,53 @@ begin end; end; +// Непрозрачная вертикальная полоса через raw-доступ (ScanLine) — замена +// Canvas.FillRect для полосы фильтра в DrawSpectrum: полоса рисуется в +// raw-фазе (между memcpy сетки и градиентом), а FillRect дёргал бы Canvas +// и форсил лишнюю синхронизацию битмапа на Qt6. +procedure TSpectrumView.FillBandRaw(X1, X2, H: Integer; Color: TColor); +var + Y, W: Integer; + Row: PLongWord; + Px: LongWord; + R, G, B: Byte; +begin + if FSpectrumBitmap = nil then Exit; + W := FSpectrumBitmap.Width; + if X1 < 0 then X1 := 0; + if X2 > W then X2 := W; + if X2 <= X1 then Exit; + R := Color and $FF; G := (Color shr 8) and $FF; B := (Color shr 16) and $FF; +{$IFDEF DARWIN} + // ARGB в памяти (см. BlendBand): байт 0 = альфа, дальше R,G,B. + Px := LongWord($FF) or (LongWord(R) shl 8) or (LongWord(G) shl 16) or + (LongWord(B) shl 24); +{$ELSE} + // BGRA в памяти: байты B,G,R,A. + Px := LongWord(B) or (LongWord(G) shl 8) or (LongWord(R) shl 16) or $FF000000; +{$ENDIF} + FSpectrumBitmap.BeginUpdate(False); + try + for Y := 0 to H - 1 do + begin + Row := PLongWord(FSpectrumBitmap.ScanLine[Y]); + if Row = nil then Continue; + Inc(Row, X1); + FillDWord(Row^, X2 - X1, Px); + end; + finally + FSpectrumBitmap.EndUpdate(False); + end; +end; + // Маркер фильтра каждого слайса (B+) на спектре — полупрозрачная полоса // пропускания + края + центральная несущая. Янтарный цвет, как в GL-вьюхе -// (SpectrumViewOpengl.DrawSliceFilterMarkers). Рисуется под кривой спектра. -procedure TSpectrumView.DrawSliceFilterMarkers(C: TCanvas; W, H: Integer); +// (SpectrumViewOpengl.DrawSliceFilterMarkers). Полосы (raw ScanLine-блендинг) +// и линии/буквы (canvas) разнесены по разным процедурам — их зовут из разных +// фаз DrawSpectrum, чтобы не чередовать режимы доступа к битмапу. +procedure TSpectrumView.DrawSliceFilterBands(W, H: Integer); const SLICE_R = $F0; SLICE_G = $A8; SLICE_B = $10; // янтарный (совпадает с SLICE_CLR в GL) - SLICE_CLR = TColor($0010A8F0); var i, SX1, SX2, SVfoX: Integer; O: TVfoOverlay; @@ -635,6 +686,21 @@ begin if (O = nil) or (not O.Visible) then Continue; CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX); if SX2 > SX1 then BlendBand(SX1, SX2, H, SLICE_R, SLICE_G, SLICE_B, 60); + end; +end; + +procedure TSpectrumView.DrawSliceFilterLines(C: TCanvas; W, H: Integer); +const + SLICE_CLR = TColor($0010A8F0); +var + i, SX1, SX2, SVfoX: Integer; + O: TVfoOverlay; +begin + for i := 0 to FSliceOverlays.Count - 1 do + begin + O := TVfoOverlay(FSliceOverlays[i]); + if (O = nil) or (not O.Visible) then Continue; + CalcFilterBandXFor(O.DisplayVfoHz, O.DisplayMode, O.DisplayBW, W, SX1, SX2, SVfoX); C.Pen.Color := SLICE_CLR; C.Pen.Width := 1; C.Pen.Style := psSolid; C.MoveTo(SX1, 0); C.LineTo(SX1, H); C.MoveTo(SX2, 0); C.LineTo(SX2, H); @@ -901,8 +967,11 @@ var LabelBandW, SrcCount: Integer; GridFreqS, GridLine, pixPerStep: Double; N, gridMult: Integer; + PerfT0, PerfTz: Int64; // PERF begin if FSpectrumBitmap = nil then Exit; + PerfT0 := PerfNow; // PERF + PerfTz := PerfT0; // PERF W := FSpectrumBitmap.Width; H := FSpectrumBitmap.Height; if (W <= 0) or (H <= 0) then Exit; C := FSpectrumBitmap.Canvas; @@ -978,68 +1047,27 @@ begin end; end; end; - CopyGridToSpectrum(W, H); + PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0) + PerfTz := PerfNow; // PERF - // ── 2. Полоса фильтра ───────────────────────────────────────────────────── + // ═══ Фаза 0: чистые вычисления — битмап не трогаем ═══════════════════════ + // Дальше битмап обрабатывается тремя непрерывными фазами: raw (ScanLine) → + // canvas (QPainter) → raw. Чередование режимов доступа на Qt6 форсит + // дорогую синхронизацию всего битмапа на каждом переходе (замерено: полоса + // фильтра в пару тысяч пикселей стоила 4× дороже полного memcpy кадра), + // поэтому порядок операций здесь важнее «логичной» группировки по смыслу. + + // Полоса фильтра (X-координаты главного и TX-VFO) if FActiveVfo = 0 then CalcFilterBandX(FVfoA, W, X1, X2, VfoX) else CalcFilterBandX(FVfoB, W, X1, X2, VfoX); - if FTXOverlay then + TXX1 := 0; TXX2 := 0; TXVfoX := 0; + if FTXOverlay and (FTXVfoIndex <> FActiveVfo) then begin - if FTXVfoIndex <> FActiveVfo then - begin - if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100); - if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX) - else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX); - if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100); - end else - if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100); - end else - begin - C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; C.Pen.Style := psClear; - if X2 > X1 then C.FillRect(Rect(X1, 0, X2, H)); - C.Pen.Style := psSolid; + if FTXVfoIndex = 0 then CalcFilterBandX(FVfoA, W, TXX1, TXX2, TXVfoX) + else CalcFilterBandX(FVfoB, W, TXX1, TXX2, TXVfoX); end; - // Буква главного флага (A) на его полосе — только когда есть слайсы, - // иначе одиночный приём не засоряем. - if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and - (FActiveVfo = 0) and Assigned(FVfoOverlay) then - DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter)); - - // Полосы фильтра слайсов (B+) — под кривой спектра, как в GL-вьюхе. - DrawSliceFilterMarkers(C, W, H); - - // ── 3. AGC линии ────────────────────────────────────────────────────────── - if FWDSPReady then - begin - AGCy := Round((DBmax - FAGCThresh) * InvRange * H); - if (AGCy >= 0) and (AGCy < H) then - begin - C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; - C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; - C.TextOut(4, AGCy - 9, 'AGC T'); - end; - AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H); - if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then - begin - C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot; - C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); - C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6; - C.TextOut(4, AGCHangY + 2, 'AGC H'); - end; - end else begin - AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H); - if (AGCy >= 0) and (AGCy < H) then - begin - C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; - C.MoveTo(0, AGCy); C.LineTo(W, AGCy); - C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; - C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); - end; - end; - - // ── 4. Кривая спектра ───────────────────────────────────────────────────── + // Точки кривой спектра if FSpPtsLen <> W + 2 then begin SetLength(FSpPts, W + 2); FSpPtsLen := W + 2; end; SrcCount := EnsureRange(FSpectrumBufCount, 2, 1024); @@ -1073,18 +1101,86 @@ begin end; FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H); - // ── 5. Градиент (заливка под кривой) ────────────────────────────────────── - if FFillSpectrum then + PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой + PerfTz := PerfNow; // PERF + + // ═══ Фаза 1: raw (ScanLine) — сетка, полосы, градиент ════════════════════ + CopyGridToSpectrum(W, H); + PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап + PerfTz := PerfNow; // PERF + + if FTXOverlay then begin - C.Brush.Style := bsSolid; C.Pen.Style := psClear; + if FTXVfoIndex <> FActiveVfo then + begin + if X2 > X1 then BlendBand(X1, X2, H, $30, $C0, $30, 100); + if TXX2 > TXX1 then BlendBand(TXX1, TXX2, H, $E0, $30, $30, 100); + end else + if X2 > X1 then BlendBand(X1, X2, H, $E0, $30, $30, 100); + end else + if X2 > X1 then FillBandRaw(X1, X2, H, FTheme.SpecFilter); + + // Полосы фильтра слайсов (B+) — под градиентом/кривой, как в GL-вьюхе. + DrawSliceFilterBands(W, H); + + PerfAdd(pzUiSpecBands, PerfTz); // PERF: полосы фильтра (raw-заливки) + PerfTz := PerfNow; // PERF + + // Градиент (заливка под кривой) + if FFillSpectrum then DrawSpectrumGradient(FSpPts, W, H); + + PerfAdd(pzUiSpecCurve, PerfTz); // PERF: += градиент под кривой + PerfTz := PerfNow; // PERF + + // ═══ Фаза 2: canvas (QPainter) — линии, текст, кривая, маркеры ═══════════ + // Буква главного флага (A) на его полосе — только когда есть слайсы, + // иначе одиночный приём не засоряем. + if (FSliceOverlays <> nil) and (FSliceOverlays.Count > 0) and + (FActiveVfo = 0) and Assigned(FVfoOverlay) then + DrawBandLetter(C, X1, X2, FVfoOverlay.SliceLetter, SliceColor(FVfoOverlay.SliceLetter)); + + // Края/несущие/буквы слайсов (raw-полосы уже нарисованы в фазе 1). + DrawSliceFilterLines(C, W, H); + + // AGC линии. Кисть выставляем явно: TextOut подписей рисует фон кистью, и + // до перестройки фаз здесь всегда была bsSolid/SpecFilter (от FillRect + // полосы) — сохраняем тот же вид, не полагаясь на остаточное состояние. + C.Brush.Color := FTheme.SpecFilter; C.Brush.Style := bsSolid; + if FWDSPReady then + begin + AGCy := Round((DBmax - FAGCThresh) * InvRange * H); + if (AGCy >= 0) and (AGCy < H) then + begin + C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; + C.MoveTo(0, AGCy); C.LineTo(W, AGCy); + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; + C.TextOut(4, AGCy - 9, 'AGC T'); + end; + AGCHangY := Round((DBmax - FAGCHangLevel) * InvRange * H); + if (AGCHangY >= 0) and (AGCHangY < H) and (Abs(AGCHangY - AGCy) > 4) then + begin + C.Pen.Color := FTheme.SpecAgcHangColor; C.Pen.Width := 1; C.Pen.Style := psDot; + C.MoveTo(0, AGCHangY); C.LineTo(W, AGCHangY); + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcHangColor; C.Font.Size := 6; + C.TextOut(4, AGCHangY + 2, 'AGC H'); + end; + end else begin + AGCy := Round((DBmax - (-FAGCTop)) * InvRange * H); + if (AGCy >= 0) and (AGCy < H) then + begin + C.Pen.Color := FTheme.SpecAgcColor; C.Pen.Width := 1; C.Pen.Style := psDash; + C.MoveTo(0, AGCy); C.LineTo(W, AGCy); + C.Pen.Style := psSolid; C.Font.Color := FTheme.SpecAgcColor; C.Font.Size := 6; + C.TextOut(4, AGCy - 9, Format('AGC -%ddB', [FAGCTop])); + end; end; - // ── 6. Линия спектра ────────────────────────────────────────────────────── + // Линия спектра C.Pen.Style := psSolid; C.Pen.Color := FTheme.SpecLine; C.Pen.Width := 1; C.Polyline(Slice(FSpPts, W)); - // ── 7. Края фильтра + VFO ───────────────────────────────────────────────── + // Края фильтра + VFO C.Pen.Color := FTheme.SpecFilterEdge; C.Pen.Width := 1; C.Pen.Style := psSolid; C.MoveTo(X1, 0); C.LineTo(X1, H); C.MoveTo(X2, 0); C.LineTo(X2, H); @@ -1106,6 +1202,11 @@ begin if FMarkerActive then DrawMarkerLine(C, W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkers(C, W, H); DrawADCOverloadOverlay(C, W, H); + + PerfAdd(pzUiSpecOverlays, PerfTz); // PERF: canvas-фаза (линии/текст/кривая) + PerfTz := PerfNow; // PERF + + // ═══ Фаза 3: raw — композиты оверлеев + альфа-проход ═════════════════════ // Бэндплан QO-100 — полоска внизу спектра (над ruler), под панелями оверлеев. if Assigned(FBandPlanOverlay) then FBandPlanOverlay.DrawOverlay(FSpectrumBitmap, W, H); @@ -1121,20 +1222,29 @@ begin // (Canvas-операции сохраняют альфу $FF), поэтому там его не делаем. // На Qt6/win32 бэкенд другой и сглаженные края могут занулять альфу — // оставляем быстрый dword-OR до проверки на этих платформах. + // ВРЕМЕННО (perf/cpu-profiling): EWSDR_ALPHA_FIX=0 пропускает проход — + // для визуальной проверки, нужен ли он на Qt6 вообще (подозрение: нет, + // SourceOver поверх непрозрачного фона альфу не занижает). Если артефактов + // не будет — проход можно удалить совсем. {$IFNDEF DARWIN} - FSpectrumBitmap.BeginUpdate(False); - for i := 0 to H - 1 do + if not GSkipAlphaFix then begin - RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]); - if RowLW = nil then Continue; - for Yp := 0 to W - 1 do + FSpectrumBitmap.BeginUpdate(False); + for i := 0 to H - 1 do begin - RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3 - Inc(RowLW); + RowLW := PLongWord(FSpectrumBitmap.ScanLine[i]); + if RowLW = nil then Continue; + for Yp := 0 to W - 1 do + begin + RowLW^ := RowLW^ or $FF000000; // BGRA: альфа в байте 3 + Inc(RowLW); + end; end; + FSpectrumBitmap.EndUpdate(False); end; - FSpectrumBitmap.EndUpdate(False); {$ENDIF} + PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: композиты флагов + альфа-OR проход + PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра end; // ──────────────────────────────────────────────────────────────────────────── @@ -1142,8 +1252,11 @@ end; // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawWaterfall; +var T0: Int64; // PERF begin + T0 := PerfNow; // PERF FWaterfall.DrawWaterfall; + PerfAdd(pzUiDrawWaterfall, T0); // PERF: CPU-рендер строк водопада end; procedure TSpectrumView.DrawRuler; @@ -1156,15 +1269,22 @@ end; // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.PaintSpectrum(Sender: TObject); -var PB: TPaintBox; +var + PB: TPaintBox; + T0: Int64; // PERF begin + T0 := PerfNow; // PERF PB := TPaintBox(Sender); PB.Canvas.Draw(0, 0, FSpectrumBitmap); + PerfAdd(pzUiPaintSpectrum, T0); // PERF: блит битмапа спектра (Qt6 canvas) end; procedure TSpectrumView.PaintWaterfall(Sender: TObject); +var T0: Int64; // PERF begin + T0 := PerfNow; // PERF FWaterfall.PaintWaterfall(Sender); + PerfAdd(pzUiPaintWaterfall, T0); // PERF: блит битмапа водопада (Qt6 canvas) end; procedure TSpectrumView.PaintRuler(Sender: TObject); @@ -1177,4 +1297,7 @@ begin FSMeter.PaintSMeterRight(Sender); end; +initialization + GSkipAlphaFix := GetEnvironmentVariable('EWSDR_ALPHA_FIX') = '0'; + end. diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index a432df5..dc4f18a 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -17,7 +17,8 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, Types, OpenGLContext, GL, - AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl; + AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры type TGLTextureCache = record @@ -936,10 +937,12 @@ procedure TSpectrumViewOpenGL.PaintSpectrum(Sender: TObject); var W, H: Integer; DBmax, DBmin, InvRange: Double; + T0, Tsw: Int64; // PERF begin if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender); if (FGLControl = nil) or (not FGLControl.MakeCurrent) then Exit; + T0 := PerfNow; // PERF W := FGLControl.Width; H := FGLControl.Height; if (W <= 0) or (H <= 0) then Exit; @@ -971,7 +974,10 @@ begin DrawCachedOverlays(W, H); FLastADCVisible := FADCOverloadVisible; + Tsw := PerfNow; // PERF FGLControl.SwapBuffers; + PerfAdd(pzUiGlSwap, Tsw); // PERF: SwapBuffers (в т.ч. ожидание vsync) + PerfAdd(pzUiPaintSpectrum, T0); // PERF: GL-кадр спектра (включая SwapBuffers) end; end. diff --git a/WDSPEngine.pas b/WDSPEngine.pas index 52aebca..0daac90 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -26,6 +26,7 @@ interface uses Classes, SysUtils, Math, SyncObjs, BeaconDecoder, + PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры WDSP; const @@ -1364,10 +1365,12 @@ procedure TWDSPEngine.PushIQItemToDSP(const Item: TIQQueueItem); var i, Pos: Integer; IR, QR: LongInt; + T0: Int64; // PERF const SCALE = 1.0 / 8388608.0; begin if FTXActive and not FKeepRXDuringTX then Exit; + T0 := PerfNow; // PERF // Post-TX hold: дропаем входящие RX-IQ пакеты пока активно окно мьюта. // Это закрывает ОБА канала сразу — и водопад/спектр (анализатор не // кормится), и аудио (RXA не запускается, FOnAudio не вызывается). @@ -1401,9 +1404,11 @@ begin if FRXAccPos >= FBufSize then ProcessRXBlock; end; + PerfAdd(pzDspPushIQ, T0); // PERF: весь путь пакета через DSP-поток end; procedure TWDSPEngine.FeedDisplaySample(const I, Q: Double); +var T0: Int64; // PERF begin if not FAnalyzerOpen then Exit; @@ -1413,7 +1418,9 @@ begin if FDispPos >= DISPLAY_BLOCK_SIZE then begin + T0 := PerfNow; // PERF Spectrum0(1, RX_DISP_ID, 0, 0, @FDispBuf[0]); + PerfAdd(pzDspSpectrum0, T0); // PERF: скармливание блока анализатору FDispPos := 0; end; end; @@ -1437,12 +1444,14 @@ procedure TWDSPEngine.ProcessRXBlock; var i: Integer; Err: Integer; + T0: Int64; // PERF // OutL/OutR теперь поля класса — не аллоцируем каждый раз begin FRXAccPos := 0; if not FInitialized then Exit; try + T0 := PerfNow; // PERF // Упаковываем в interleaved буфер для fexchange0 for i := 0 to FBufSize - 1 do begin @@ -1453,6 +1462,7 @@ begin // DSP обработка Err := 0; fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); + PerfAdd(pzDspFexMain, T0); // PERF: главный RX-канал WDSP // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй @@ -1474,12 +1484,14 @@ begin and (not FTXActive or (FKeepRXDuringTX and FMonitorRXDuringTX)) and (FPostTXMuteUntil = 0) then begin + T0 := PerfNow; // PERF for i := 0 to FAudioBufSize - 1 do begin FOutL[i] := FRXOut[i * 2] * FVolume; FOutR[i] := FRXOut[i * 2 + 1] * FVolume; end; FOnAudio(FOutL, FOutR, FAudioBufSize); + PerfAdd(pzDspAudioCb, T0); // PERF: маршрут RX-аудио (web/радио/звуковуха) end; // Софтовые слайсы: fan-out того же IQ-блока на доп. WDSP-каналы. @@ -1586,8 +1598,10 @@ procedure TWDSPEngine.ProcessSlices; var s, i, k, Err: Integer; V: Double; + T0, TS: Int64; // PERF begin if not Assigned(FOnSliceAudio) then Exit; + T0 := PerfNow; // PERF FSliceLock.Enter; try for s := 0 to MAX_SLICES - 1 do @@ -1601,7 +1615,9 @@ begin FSliceIn[i * 2 + 1] := FRXAccQ[i]; end; Err := 0; + TS := PerfNow; // PERF fexchange0(FSlices[s].Chan, @FSliceIn[0], @FSliceOut[0], @Err); + PerfAdd(pzDspSliceFex, TS); // PERF: fexchange0 одного слайса // S-метр канала читаем здесь (DSP-поток) — GetRXAMeter сбрасывает аккумулятор. FSlices[s].SMeter := GetRXAMeter(FSlices[s].Chan, RXA_S_AV); V := FSlices[s].Volume; @@ -1615,6 +1631,7 @@ begin finally FSliceLock.Leave; end; + PerfAdd(pzDspSlices, T0); // PERF: fan-out всех слайсов целиком end; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; @@ -2198,8 +2215,10 @@ var i, tail: Integer; Err: Integer; Avail: Integer; + T0: Int64; // PERF begin if not FInitialized then Exit; + T0 := PerfNow; // PERF Avail := (FTXMicHead - FTXMicTail + TX_MIC_RING) and (TX_MIC_RING - 1); @@ -2235,6 +2254,7 @@ begin // FTXOut содержит FTXOutBufSize IQ пар @ FTXSampleRate (interleaved double) if Assigned(FOnTXIQ) then FOnTXIQ(FTXOut, FTXOutBufSize); + PerfAdd(pzDspTxBlock, T0); // PERF: TX-блок целиком (fexchange0 + analyzer + IQ out) end; procedure TWDSPEngine.PushTXMicSamples16(const Src: array of SmallInt; N: Integer); @@ -2532,8 +2552,10 @@ var Flag: Integer; i, N: Integer; DispID: Integer; + T0: Int64; // PERF begin if not FInitialized or not FAnalyzerOpen then Exit; + T0 := PerfNow; // PERF N := EnsureRange(FDisplayPixelCount, 1, SPECTRUM_PIXELS); DispID := FActiveDisplayID; // RX_DISP_ID или TX_DISP_ID, зависит от FTXActive @@ -2556,6 +2578,7 @@ begin if Assigned(FOnWaterfall) then FOnWaterfall(FWaterfallPixels, N); end; + PerfAdd(pzDispGetPixels, T0); // PERF: GetPixels + колбэки спектр/водопад end; procedure TWDSPEngine.GetSpectrumData(var Pixels: array of Single; diff --git a/WaterfallViewOpengl.pas b/WaterfallViewOpengl.pas index 00a2a4d..a5c9967 100644 --- a/WaterfallViewOpengl.pas +++ b/WaterfallViewOpengl.pas @@ -16,7 +16,8 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, OpenGLContext, GL, - AppTheme, WaterfallView; + AppTheme, WaterfallView, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры type TWaterfallViewOpenGL = class(TWaterfallView) @@ -461,9 +462,11 @@ procedure TWaterfallViewOpenGL.PaintWaterfall(Sender: TObject); var W, H: Integer; R, G, B: GLFloat; + T0, Tsw: Int64; // PERF begin if Sender is TOpenGLControl then FGLControl := TOpenGLControl(Sender); if (FGLControl = nil) or (not FGLControl.MakeCurrent) then Exit; + T0 := PerfNow; // PERF W := FGLControl.Width; H := FGLControl.Height; if (W <= 0) or (H <= 0) then Exit; @@ -491,7 +494,10 @@ begin end; DrawTextureWrapped(W, H); DrawMarker(W, H); + Tsw := PerfNow; // PERF FGLControl.SwapBuffers; + PerfAdd(pzUiGlSwap, Tsw); // PERF: SwapBuffers (в т.ч. ожидание vsync) + PerfAdd(pzUiPaintWaterfall, T0); // PERF: GL-кадр водопада (включая SwapBuffers) end; procedure TWaterfallViewOpenGL.SetTheme(const T: TAppTheme); diff --git a/WidebandView.pas b/WidebandView.pas index 3ddf318..460fe64 100644 --- a/WidebandView.pas +++ b/WidebandView.pas @@ -18,7 +18,8 @@ interface uses Classes, SysUtils, Math, Graphics, Controls, ExtCtrls, - OpenGLContext, GL, AppTheme, WDSP; + OpenGLContext, GL, AppTheme, WDSP, + PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры type TWidebandView = class @@ -1120,11 +1121,14 @@ begin end; procedure TWidebandView.Paint(Sender: TObject); +var T0: Int64; // PERF begin + T0 := PerfNow; // PERF if Sender is TOpenGLControl then PaintGL(TOpenGLControl(Sender)) else if Sender is TPaintBox then PaintCPU(TPaintBox(Sender).Canvas); + PerfAdd(pzUiWidebandPaint, T0); // PERF: кадр wideband-панорамы (CPU или GL) end; procedure TWidebandView.PaintRuler(Sender: TObject); diff --git a/doc/PERF_PROFILING.md b/doc/PERF_PROFILING.md new file mode 100644 index 0000000..43449f9 --- /dev/null +++ b/doc/PERF_PROFILING.md @@ -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 + 3–4 слайса (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. diff --git a/ewsdr.lpi b/ewsdr.lpi index ac97e97..4dd4370 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -70,6 +70,13 @@ + + + + + + +