diff --git a/AudioOutput.pas b/AudioOutput.pas index bc4b7f8..b6b9df2 100644 --- a/AudioOutput.pas +++ b/AudioOutput.pas @@ -21,8 +21,7 @@ unit AudioOutput; interface uses - Classes, SysUtils, Math, SyncObjs, DynLibs, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + Classes, SysUtils, Math, SyncObjs, DynLibs; const MY_AUDIO_BUFFER_SIZE = 128; // default PA frames per callback — как piHPSDR @@ -629,13 +628,10 @@ 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 193b593..d5114b8 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -19,8 +19,7 @@ uses {$ELSE} Sockets, BaseUnix, {$ENDIF} - HPSDRProtocol, SyncObjs, Settings, RadioBackend, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + HPSDRProtocol, SyncObjs, Settings, RadioBackend; const DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun @@ -1094,14 +1093,11 @@ 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); @@ -1118,11 +1114,9 @@ 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; @@ -1163,7 +1157,6 @@ begin FWBFrameCount := 0; FWBSeqValid := False; end; - PerfAdd(pzNetWideband, T0); // PERF: сборка wideband-кадра + колбэк end; // --------------------------------------------------------------------------- diff --git a/MainForm.pas b/MainForm.pas index c9799a2..5dfc88a 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -37,7 +37,6 @@ uses WidebandView, StatusBar, PlatformUtils, - PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры WinFirewall, BoardUtils, WisdomBuilder, UISync, FlatEdit, FMRepeater, @@ -3169,10 +3168,7 @@ 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; @@ -3226,9 +3222,6 @@ begin else SetStatusText(5, 'TX idle'); end; - finally - PerfAdd(pzUiMeterTick, PerfT0); // PERF: тик метра целиком (10 Гц) - end; end; procedure TMainForm.SpectrumTimerTick(Sender: TObject); @@ -3236,10 +3229,7 @@ var NowTick: QWord; ElapsedTick: QWord; I: Integer; - PerfT0, PerfTz: Int64; // PERF begin - PerfT0 := PerfNow; // PERF - try // Синхронизируем размеры если размер панели изменился (после ресайза). // Важно: проверяем только видимые панели — когда спектр скрыт, // PbSpectrum.Height может быть ненулевым при FSpectrumHeight=0, @@ -3312,16 +3302,13 @@ 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 Гц там не нужно. @@ -3332,7 +3319,6 @@ begin if FSliceFlags[I] <> nil then FSliceFlags[I].UpdateSMeter(FController.SliceSMeter(FSliceFlags[I].SliceId)); end; - PerfAdd(pzUiSliceFlags, PerfTz); // PERF: позиционирование + S-метры слайс-флагов end; // Панорама и водопад теперь рисуются независимо по факту новых кадров. // Это лучше совпадает с Thetis: маленький FFT даёт новые строки чаще. @@ -3399,14 +3385,7 @@ 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 deleted file mode 100644 index eb0278a..0000000 --- a/PerfLog.pas +++ /dev/null @@ -1,315 +0,0 @@ -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! - pzUiComposites // композиты бэндплан/панель/флаги (внутри raw-лока) - ); - -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', - 'ui.composites' - ); - -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 4c7255c..6222a22 100644 --- a/PlutoBackend.pas +++ b/PlutoBackend.pas @@ -26,8 +26,7 @@ interface uses Classes, SysUtils, ctypes, - RadioBackend, IIOBindings, HPSDRProtocol, Settings, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + RadioBackend, IIOBindings, HPSDRProtocol, Settings; type { TPlutoBackend } @@ -196,7 +195,6 @@ var outc: Integer; vi, vq: LongInt; u: LongWord; - T0: Int64; // PERF procedure FlushPkt; begin @@ -222,7 +220,6 @@ 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); @@ -249,7 +246,6 @@ begin Inc(p, step); end; if outc > 0 then FlushPkt; // хвост блока - PerfAdd(pzPlutoRxConv, T0); // PERF: int16→24bit BE + FOnDDCIQ на весь iio-буфер end; end; @@ -266,12 +262,10 @@ 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); @@ -297,7 +291,6 @@ 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 @@ -860,10 +853,8 @@ 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; @@ -902,7 +893,6 @@ 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 41c35ea..4f09e16 100644 --- a/SpectrumView.pas +++ b/SpectrumView.pas @@ -21,8 +21,7 @@ uses Classes, SysUtils, Graphics, GraphType, ExtCtrls, Controls, Math, AppTheme, AlertOverlay, SampleRateOverlay, VfoOverlay, BandPlanOverlay, - WaterfallView, SMeterView, RulerView, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + WaterfallView, SMeterView, RulerView; const SV_CLR_BG = TColor($00101010); @@ -1280,11 +1279,8 @@ 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; @@ -1377,8 +1373,6 @@ begin FGridBitmap.EndUpdate(False); {$ENDIF} end; - PerfAdd(pzUiSpecGrid, PerfTz); // PERF: ребилд кэша сетки (обычно 0) - PerfTz := PerfNow; // PERF // ═══ Фаза 0: чистые вычисления — битмап не трогаем ═══════════════════════ // Дальше битмап обрабатывается тремя непрерывными фазами: raw (ScanLine) → @@ -1431,23 +1425,17 @@ begin end; FSpPts[W] := Point(W-1, H); FSpPts[W+1] := Point(0, H); - PerfAdd(pzUiSpecCurve, PerfTz); // PERF: расчёт точек кривой - PerfTz := PerfNow; // PERF // ═══ Фаза 1: raw — весь кадр одним локом ═════════════════════════════════ {$IFDEF DARWIN} // Cocoa-вариант CopyGridToSpectrum идёт через Canvas.Draw — до RawBegin. CopyGridToSpectrum(W, H); - PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный перенос сетки в битмап - PerfTz := PerfNow; // PERF {$ENDIF} RawBegin; try {$IFNDEF DARWIN} CopyGridToSpectrum(W, H); - PerfAdd(pzUiSpecGrid, PerfTz); // PERF: += полный memcpy сетки в битмап - PerfTz := PerfNow; // PERF {$ENDIF} // Полосы фильтра @@ -1465,15 +1453,11 @@ begin // Полосы фильтра слайсов (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 // Буква главного флага (A) на его полосе — только когда есть слайсы, // иначе одиночный приём не засоряем. @@ -1532,8 +1516,6 @@ begin if FMarkerActive then DrawMarkerLineRaw(W, H); if FBeaconMarkActive or FBeaconDecActive then DrawBeaconMarkersRaw(W, H); - PerfAdd(pzUiSpecOverlays, PerfTz); // PERF: raw-линии/текст/кривая/маркеры - PerfTz := PerfNow; // PERF // Композиты оверлеев — внутри внешнего лока: их собственные BeginUpdate/ // EndUpdate становятся вложенными (только счётчик), иначе каждая пара на @@ -1553,8 +1535,6 @@ begin for i := 0 to FSliceOverlays.Count - 1 do TVfoOverlay(FSliceOverlays[i]).DrawOverlay(FSpectrumBitmap, W, H); - PerfAdd(pzUiComposites, PerfTz); // PERF: композиты бэндплан/панель/флаги - PerfTz := PerfNow; // PERF finally RawEnd; end; @@ -1582,8 +1562,6 @@ begin {$ENDIF} end; - PerfAdd(pzUiSpecAlpha, PerfTz); // PERF: RawEnd (EndUpdate) + редкий ADC-путь - PerfAdd(pzUiDrawSpectrum, PerfT0); // PERF: CPU-рендер кадра спектра end; // ──────────────────────────────────────────────────────────────────────────── @@ -1591,11 +1569,8 @@ end; // ──────────────────────────────────────────────────────────────────────────── procedure TSpectrumView.DrawWaterfall; -var T0: Int64; // PERF begin - T0 := PerfNow; // PERF FWaterfall.DrawWaterfall; - PerfAdd(pzUiDrawWaterfall, T0); // PERF: CPU-рендер строк водопада end; procedure TSpectrumView.DrawRuler; @@ -1610,20 +1585,14 @@ end; procedure TSpectrumView.PaintSpectrum(Sender: TObject); 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); diff --git a/SpectrumViewOpengl.pas b/SpectrumViewOpengl.pas index dc4f18a..a432df5 100644 --- a/SpectrumViewOpengl.pas +++ b/SpectrumViewOpengl.pas @@ -17,8 +17,7 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, Types, OpenGLContext, GL, - AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + AppTheme, SpectrumView, VfoOverlay, BandPlanOverlay, WaterfallViewOpengl; type TGLTextureCache = record @@ -937,12 +936,10 @@ 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; @@ -974,10 +971,7 @@ 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 0daac90..52aebca 100644 --- a/WDSPEngine.pas +++ b/WDSPEngine.pas @@ -26,7 +26,6 @@ interface uses Classes, SysUtils, Math, SyncObjs, BeaconDecoder, - PerfLog, // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры WDSP; const @@ -1365,12 +1364,10 @@ 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 не вызывается). @@ -1404,11 +1401,9 @@ 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; @@ -1418,9 +1413,7 @@ 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; @@ -1444,14 +1437,12 @@ 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 @@ -1462,7 +1453,6 @@ begin // DSP обработка Err := 0; fexchange0(RXA_CHAN, @FRXIn[0], @FRXOut[0], @Err); - PerfAdd(pzDspFexMain, T0); // PERF: главный RX-канал WDSP // S-meter: читаем ОДИН раз в DSP-колбэке и кешируем в FSMeter. // GetRXAMeter сбрасывает аккумулятор после вызова, поэтому второй @@ -1484,14 +1474,12 @@ 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-каналы. @@ -1598,10 +1586,8 @@ 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 @@ -1615,9 +1601,7 @@ 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; @@ -1631,7 +1615,6 @@ begin finally FSliceLock.Leave; end; - PerfAdd(pzDspSlices, T0); // PERF: fan-out всех слайсов целиком end; function TWDSPEngine.AddSlice(Id, Mode, FilterLo, FilterHi: Integer; @@ -2215,10 +2198,8 @@ 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); @@ -2254,7 +2235,6 @@ 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); @@ -2552,10 +2532,8 @@ 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 @@ -2578,7 +2556,6 @@ 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 a5c9967..00a2a4d 100644 --- a/WaterfallViewOpengl.pas +++ b/WaterfallViewOpengl.pas @@ -16,8 +16,7 @@ interface uses Classes, SysUtils, Graphics, Controls, Math, OpenGLContext, GL, - AppTheme, WaterfallView, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + AppTheme, WaterfallView; type TWaterfallViewOpenGL = class(TWaterfallView) @@ -462,11 +461,9 @@ 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; @@ -494,10 +491,7 @@ 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 460fe64..3ddf318 100644 --- a/WidebandView.pas +++ b/WidebandView.pas @@ -18,8 +18,7 @@ interface uses Classes, SysUtils, Math, Graphics, Controls, ExtCtrls, - OpenGLContext, GL, AppTheme, WDSP, - PerfLog; // ВРЕМЕННО (perf/cpu-profiling): зонные CPU-таймеры + OpenGLContext, GL, AppTheme, WDSP; type TWidebandView = class @@ -1121,14 +1120,11 @@ 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 deleted file mode 100644 index b58065a..0000000 --- a/doc/PERF_PROFILING.md +++ /dev/null @@ -1,85 +0,0 @@ -# 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 (full-raw рендер): grid=ребилд+memcpy сетки, bands=raw-заливки полос, curve=точки+градиент, overlays=raw-линии/текст/кривая/маркеры, composites=бэндплан/панель/флаги, alpha=RawEnd/EndUpdate (+редкий canvas-путь ADC overload) | -| 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 0b17dae..1e17ac1 100644 --- a/ewsdr.lpi +++ b/ewsdr.lpi @@ -17,9 +17,9 @@ - + - + @@ -82,6 +82,7 @@ +