chore(tx): сняты зонды трассировки — пейсинг проверен на железе

Всплесков на передаче нет, инструмент своё отработал и больше не нужен в
горячем пути. Снято ровно то, что ставил d209697, и ничего сверх:

* удалён юнит TxTrace.pas (кольцо + сброс дампа + TraceInit);
* TCIAdapter: точки съёма маркеров и TX-аудио, поля FTxTraceLast/FTxTraceAudio;
* HPSDRNetwork: DUC-DRY/DUC-RUN и переменная DryFrom (жила только для них),
  вместе с ними ушли из uses TxTrace и PlatformUtils — их добавлял тот же
  коммит, MonotonicUs в этом файле больше нигде не встречается;
* RadioController: PTT-метка в SetMOX и сброс дампа на снятии PTT;
* TraceInit из ewsdr.lpr и ewsdrd.lpr.

Каждая удалённая строка стояла под `if TraceOn` — вычислений, от которых
зависит тракт, в них не было. ★PlatformUtils.MonotonicUs НЕ тронут: на нём
абсолютные дедлайны планировщика TX, это не отладка.

Скрипт разбора test/hpsdr/txtrace_scan.py оставлен, формат дампа не менялся;
в README стенда записан рецепт возврата зондов из d209697.

Проверено: lazbuild --ws=qt6 и build-ewsdrd.sh собираются, test/tci 260/260,
test/cat 50/50 — те же числа, что и с зондами.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
This commit is contained in:
2026-08-24 17:52:23 +03:00
co-authored by Claude Opus 5
parent d2096976df
commit 166fc65fc8
7 changed files with 17 additions and 185 deletions
+1 -15
View File
@@ -19,7 +19,7 @@ uses
{$ELSE} {$ELSE}
Sockets, BaseUnix, Sockets, BaseUnix,
{$ENDIF} {$ENDIF}
HPSDRProtocol, SyncObjs, Settings, RadioBackend, TxTrace, PlatformUtils; HPSDRProtocol, SyncObjs, Settings, RadioBackend;
const const
DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun
@@ -509,7 +509,6 @@ var
NowTick: QWord; NowTick: QWord;
ElapsedMs: QWord; ElapsedMs: QWord;
WaitMs: Integer; WaitMs: Integer;
DryFrom: Int64; // приборы: начало простоя очереди, мкс
{$ENDIF} {$ENDIF}
{$IFDEF WINDOWS} {$IFDEF WINDOWS}
@@ -581,18 +580,12 @@ begin
{$ELSE} {$ELSE}
VirtualSamples := 0; VirtualSamples := 0;
LastTick := GetTickCount64; LastTick := GetTickCount64;
DryFrom := 0;
while not Terminated do while not Terminated do
begin begin
if not FNet.DequeueDUCIQ(Pkt) then if not FNet.DequeueDUCIQ(Pkt) then
begin begin
DrainVirtualFIFO; DrainVirtualFIFO;
if TraceOn and FNet.FIsTransmitting and (DryFrom = 0) then
begin
DryFrom := MonotonicUs;
TR(trkDucDry, VirtualSamples, FNet.FDUCIQCount);
end;
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо // Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как // в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём // dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
@@ -611,13 +604,6 @@ begin
if not FNet.FIsTransmitting then if not FNet.FIsTransmitting then
Continue; Continue;
if TraceOn and (DryFrom <> 0) then
begin
TR(trkDucRun, Integer(MonotonicUs - DryFrom), VirtualSamples,
FNet.FDUCIQCount);
DryFrom := 0;
end;
FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ));
FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress,
FNet.FPortDUCIQ); FNet.FPortDUCIQ);
+1 -5
View File
@@ -51,7 +51,7 @@ uses
HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings, HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings,
WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder, WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder,
Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer, Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer,
CWDecoder, TxTrace; CWDecoder;
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для // Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1). // движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
@@ -6303,9 +6303,6 @@ begin
// прекратить — ключ обязан отпуститься немедленно. // прекратить — ключ обязан отпуститься немедленно.
if (not On_) and CWXBusy then CWXAbort; if (not On_) and CWXBusy then CWXAbort;
FTransmitting := On_; FTransmitting := On_;
if TraceOn and (On_ <> WasTransmitting) then
if Assigned(FDSPEngine) then TR(trkPtt, Ord(On_), Ord(FDSPEngine.TXMicSource))
else TR(trkPtt, Ord(On_), -1);
// 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник. // 2TON: прямое снятие MOX (кнопкой/CAT/web) гасит и двухтональник.
if (not On_) and FTwoTone then if (not On_) and FTwoTone then
begin begin
@@ -6446,7 +6443,6 @@ begin
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой. // (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
SyncCWKeyer; SyncCWKeyer;
Changed(rfTransmitting); Changed(rfTransmitting);
if TraceOn and WasTransmitting and (not On_) then TraceDump('tx');
end; end;
procedure TRadioController.SetTune(On_: Boolean); procedure TRadioController.SetTune(On_: Boolean);
+1 -21
View File
@@ -44,7 +44,7 @@ interface
uses uses
Classes, SysUtils, DateUtils, Math, SyncObjs, Classes, SysUtils, DateUtils, Math, SyncObjs,
RadioController, RadioBackend, WDSPEngine, Settings, PlatformUtils, RadioController, RadioBackend, WDSPEngine, Settings, PlatformUtils,
DXSpotStore, TCIProtocol, TCIServer, TCIStreams, TxTrace; DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
const const
// ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА // ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА
@@ -242,8 +242,6 @@ type
FTxLeadKeep: Integer; // ★он же, но ПЕРЕЖИВАЮЩИЙ конец передачи FTxLeadKeep: Integer; // ★он же, но ПЕРЕЖИВАЮЩИЙ конец передачи
FTxLeadRate: Integer; // частота, при которой аванс измерен FTxLeadRate: Integer; // частота, при которой аванс измерен
FTxLeadLowUs: Int64; // с какого момента оценка держится НИЖЕ аванса FTxLeadLowUs: Int64; // с какого момента оценка держится НИЖЕ аванса
FTxTraceLast: Int64; // приборы: время прошлого маркера
FTxTraceAudio: Int64; // приборы: время прошлого блока аудио
FTxLastRxUs: Int64; // когда пришёл прошлый блок (границы пачек) FTxLastRxUs: Int64; // когда пришёл прошлый блок (границы пачек)
// Рабочие буферы разбора TX-блока. Полем, а не на стеке: развёрнутый в // Рабочие буферы разбора TX-блока. Полем, а не на стеке: развёрнутый в
// 48 кГц блок — это сотни килобайт, и класть их в стек потока клиента // 48 кГц блок — это сотни килобайт, и класть их в стек потока клиента
@@ -3754,7 +3752,6 @@ begin
TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans); TCIFillHeader(H, tstTXChrono, Rx, Rate, ST, Q, Chans);
Client.SendBinNow(H); Client.SendBinNow(H);
if TraceOn then TR(trkNote, -4, Q, Rx);
end; end;
procedure TTCIAdapter.TxPublishLead(Rate: Integer); procedure TTCIAdapter.TxPublishLead(Rate: Integer);
@@ -3843,7 +3840,6 @@ begin
try try
if not Active then if not Active then
begin begin
if TraceOn and FTxRunning then TR(trkNote, -3, Round(FTxInFlight), 0);
FTxRunning := False; FTxRunning := False;
FServer.TxTickPeriodMs := 0; FServer.TxTickPeriodMs := 0;
Exit; Exit;
@@ -3886,7 +3882,6 @@ begin
(TCI_TX_LEAD_MAX_MS * Rate) div 1000); (TCI_TX_LEAD_MAX_MS * Rate) div 1000);
if LeadWant > FTxLead then if LeadWant > FTxLead then
begin begin
if TraceOn then TR(trkNote, -2, LeadWant, FTxLead);
FTxLead := LeadWant; FTxLead := LeadWant;
FTxLeadLowUs := 0; FTxLeadLowUs := 0;
TxPublishLead(Rate); TxPublishLead(Rate);
@@ -3899,7 +3894,6 @@ begin
if FTxLeadLowUs = 0 then FTxLeadLowUs := NowUs if FTxLeadLowUs = 0 then FTxLeadLowUs := NowUs
else if NowUs - FTxLeadLowUs > TCI_TX_LEAD_DOWN_MS * 1000 then else if NowUs - FTxLeadLowUs > TCI_TX_LEAD_DOWN_MS * 1000 then
begin begin
if TraceOn then TR(trkNote, -2, LeadWant, FTxLead);
FTxLead := LeadWant; FTxLead := LeadWant;
FTxLeadLowUs := NowUs; FTxLeadLowUs := NowUs;
TxPublishLead(Rate); TxPublishLead(Rate);
@@ -3946,7 +3940,6 @@ begin
begin begin
// Прощаем ровно один квант и начинаем выдержку заново: поздний ответ // Прощаем ровно один квант и начинаем выдержку заново: поздний ответ
// безопасен, он уведёт долг в минус и сам притормозит выдачу. // безопасен, он уведёт долг в минус и сам притормозит выдачу.
if TraceOn then TR(trkNote, -1, Round(FTxInFlight), FTxWindowQ);
FTxInFlight := Max(0, FTxInFlight - Q); FTxInFlight := Max(0, FTxInFlight - Q);
// ★Срок сдвигаем НА выдержку, а не на «сейчас». Зависших кредитов может // ★Срок сдвигаем НА выдержку, а не на «сейчас». Зависших кредитов может
// быть несколько, и прощение по одному за выдержку живого времени // быть несколько, и прощение по одному за выдержку живого времени
@@ -3984,13 +3977,6 @@ begin
FTxInFlight := FTxInFlight + Q; FTxInFlight := FTxInFlight + Q;
TxNoteRequest(NowUs); TxNoteRequest(NowUs);
Inc(Sent); Inc(Sent);
if TraceOn then
begin
if FTxTraceLast = 0 then FTxTraceLast := NowUs;
TR(trkChrono, Q, Round(FTxOwed - FTxInFlight),
Integer(NowUs - FTxTraceLast));
FTxTraceLast := NowUs;
end;
end; end;
finally finally
FTxLock.Leave; FTxLock.Leave;
@@ -4104,12 +4090,6 @@ begin
FTxProgressUs := NowUs; FTxProgressUs := NowUs;
FTxArmed := True; FTxArmed := True;
TxNoteReply(NowUs); TxNoteReply(NowUs);
if TraceOn then
begin
if FTxTraceAudio = 0 then FTxTraceAudio := NowUs;
TR(trkTxAudio, Integer(NowUs - FTxTraceAudio), Integer(H.DataLength), k);
FTxTraceAudio := NowUs;
end;
if (FTxInterp = nil) or (FTxInRate <> Rate) then if (FTxInterp = nil) or (FTxInRate <> Rate) then
begin begin
-139
View File
@@ -1,139 +0,0 @@
unit TxTrace;
{
TxTrace.pas кольцевая трассировка TX-тракта.
★Временный диагностический юнит. Выключен по умолчанию, включается
переменной окружения EWSDR_TXTRACE=1; при выключенной трассировке TR()
одна проверка Boolean. Никакого ввода-вывода в горячем пути: кольцо
сбрасывается на диск в конце каждой передачи.
Зачем он снова здесь: всплески на передаче вернулись после того, как первая
редакция зондов была удалена, и без них видно только то, что видит глаз на
водопаде. Прибор смотрит ровно на подачу TX-аудио по TCI и на очередь DUC.
}
{$IFDEF FPC}
{$MODE Delphi}
{$ENDIF}
interface
const
trkPtt = 0; // A=1 старт передачи / 0 конец; B=источник модуляции
trkChrono = 7; // A=запрошено кадров; B=непокрытый долг; C=пауза от прошлого маркера, мкс
trkTxAudio = 8; // A=пауза от прошлого блока, мкс; B=length в шапке; C=кадров после интерполяции
trkDucDry = 10; // A=виртуальный уровень FIFO радио; B=глубина очереди
trkDucRun = 11; // A=длительность простоя очереди, мкс; B=уровень FIFO; C=глубина очереди
trkNote = 12; // A=-1 прощение кредита, -2 смена аванса, -3 в полёте на снятии PTT, -4 предварительный маркер
var
TraceOn: Boolean = False;
procedure TraceInit;
procedure TR(Kind: Integer; A: Integer = 0; B: Integer = 0; C: Integer = 0);
function TraceDump(const Reason: string): string;
implementation
uses
SysUtils, Math, PlatformUtils;
const
TRACE_SIZE = 16384;
TRACE_MASK = TRACE_SIZE - 1;
type
TTraceRec = record
T: Int64;
Kind: Integer;
A, B, C: Integer;
end;
var
Ring: array[0..TRACE_SIZE - 1] of TTraceRec;
RingSeq: LongInt = 0;
Epoch: Int64 = 0;
const
TRACE_NAMES: array[0..12] of string = (
'PTT', 'RECV-GAP', 'HANDLER', 'HP-SYNC', 'WIDEBAND', 'TX-TICK',
'TX-PHASE', 'CHRONO', 'TX-AUDIO', 'PULL-MIC', 'DUC-DRY', 'DUC-RUN', 'NOTE');
procedure TraceInit;
var
S: string;
begin
S := LowerCase(Trim(GetEnvironmentVariable('EWSDR_TXTRACE')));
TraceOn := (S = '1') or (S = 'true') or (S = 'yes') or (S = 'on');
Epoch := MonotonicUs;
end;
procedure TR(Kind: Integer; A: Integer = 0; B: Integer = 0; C: Integer = 0);
var
Idx: LongInt;
P: ^TTraceRec;
begin
if not TraceOn then Exit;
Idx := InterlockedIncrement(RingSeq) - 1;
P := @Ring[Idx and TRACE_MASK];
P^.T := MonotonicUs;
P^.Kind := Kind;
P^.A := A;
P^.B := B;
P^.C := C;
end;
function Slug(const S: string): string;
var i: Integer;
begin
Result := '';
for i := 1 to Length(S) do
if S[i] in ['a'..'z', 'A'..'Z', '0'..'9', '-', '_'] then
Result := Result + S[i];
if Result <> '' then Result := '-' + Copy(Result, 1, 24);
end;
function TraceDump(const Reason: string): string;
var
F: TextFile;
i, First: LongInt;
Total: LongInt;
R: TTraceRec;
Name: string;
begin
Result := '';
if not TraceOn then Exit;
Total := RingSeq;
if Total <= 0 then Exit;
Result := GetAppCfgDir + 'txtrace-' +
FormatDateTime('yyyymmdd-hhnnss', Now) + Slug(Reason) + '.txt';
AssignFile(F, Result);
try
Rewrite(F);
try
WriteLn(F, '# ewsdr TX trace, повод: ', Reason);
WriteLn(F, '# всего событий: ', Total, ', в кольце: ',
Min(Total, TRACE_SIZE));
WriteLn(F, '# t_ms событие A B C');
First := Total - Min(Total, TRACE_SIZE);
for i := First to Total - 1 do
begin
R := Ring[i and TRACE_MASK];
if (R.Kind < Low(TRACE_NAMES)) or (R.Kind > High(TRACE_NAMES)) then
Name := '?'
else
Name := TRACE_NAMES[R.Kind];
WriteLn(F, Format('%12.3f %-9s %9d %9d %9d',
[(R.T - Epoch) / 1000.0, Name, R.A, R.B, R.C]));
end;
finally
CloseFile(F);
end;
except
on E: Exception do Result := '';
end;
end;
end.
+1 -2
View File
@@ -18,12 +18,11 @@ uses
Forms, LazOpenGLContextEx, Forms, LazOpenGLContextEx,
MainForm, AudioOutput, DeviceForm, FlatButton, FreqDisplay, VfoOverlay, MainForm, AudioOutput, DeviceForm, FlatButton, FreqDisplay, VfoOverlay,
WDSP, WDSPEngine, WebPageHtml, WebServer, WebUtils, WinFirewall, WsClient, WDSP, WDSPEngine, WebPageHtml, WebServer, WebUtils, WinFirewall, WsClient,
Settings, RadioController, TxTrace; Settings, RadioController;
{$R *.res} {$R *.res}
begin begin
TraceInit;
EnableProcessDpiAwareness; EnableProcessDpiAwareness;
RequireDerivedFormResource := True; RequireDerivedFormResource := True;
Application.Title:='EWSDR'; Application.Title:='EWSDR';
+1 -2
View File
@@ -28,7 +28,7 @@ uses
{$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF} {$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF}
Classes, SysUtils, Math, Classes, SysUtils, Math,
RadioController, WebServer, WebAdapter, WDSPEngine, RadioController, WebServer, WebAdapter, WDSPEngine,
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils, TxTrace; HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils;
const const
PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер) PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер)
@@ -490,7 +490,6 @@ begin
end; end;
begin begin
TraceInit;
OnGetApplicationName := @DaemonAppName; // ДО создания контроллера/настроек! OnGetApplicationName := @DaemonAppName; // ДО создания контроллера/настроек!
Log('EWSDR daemon starting (headless).'); Log('EWSDR daemon starting (headless).');
GHost := THeadlessHost.Create; GHost := THeadlessHost.Create;
+12 -1
View File
@@ -107,8 +107,19 @@ TX голодала, в эфире дыры. Тут же диапазон глу
## Приборы внутри процесса: кольцевая трассировка TX-тракта ## Приборы внутри процесса: кольцевая трассировка TX-тракта
★**Сняты из кода 2026-08-24** — пейсинг TX проверен на железе, всплесков нет, и
инструмент больше не нужен в горячем пути. Последняя редакция зондов лежит в
коммите `d209697`; вернуть их целиком:
git show d209697 -- TxTrace.pas | git apply # сам юнит
git show <коммит снятия> | git apply -R # точки съёма и TraceInit
Скрипт разбора (`txtrace_scan.py`) оставлен: формат дампа не менялся, и при
возврате зондов он работает как прежде. Ниже — описание прибора на случай
возврата.
Дамп на проводе видит ПАУЗУ в потоке DUC, но не видит, кто её сделал. Для этого Дамп на проводе видит ПАУЗУ в потоке DUC, но не видит, кто её сделал. Для этого
в код вписано кольцо трассировки (`TxTrace.pas`), выключенное по умолчанию. в код вписывалось кольцо трассировки (`TxTrace.pas`), выключенное по умолчанию.
Включение — переменной окружения, кольцо сбрасывается на диск в конце КАЖДОЙ Включение — переменной окружения, кольцо сбрасывается на диск в конце КАЖДОЙ
передачи (после снятия PTT, чтобы файл не задержал выход из эфира): передачи (после снятия PTT, чтобы файл не задержал выход из эфира):