Files
ewsdr/TxTrace.pas
T
ew8bakandClaude Opus 5 d2096976df fix(tci): всплески на передаче — TX-аудио просилось общим тиком 20 мс
Посреди передачи из MSHV на водопаде появлялись всплески своего сигнала.
Цепочка: очередь DUC пустеет дольше подушки отправителя (DUC_FIFO_THROTTLE
= 2000 отсчётов = 10.4 мс) → FIFO радио сохнет → модуляция обрывается → в
эфире остаётся голая несущая на частоте гетеродина DUC, в стороне от тона
ровно на звуковой сдвиг. Доказано pcap-съёмом: шесть всплесков в дампе —
ровно столько, сколько видел оператор, и каждый стоит за паузой 10.3-23.5 мс,
а паузы 8 мс и короче не дали ни одного.

Виноват не клиент и не блокировка UI, а зернистость НАШЕГО запроса. Слой
первый: PushTxChrono жил на общем тике сервера 20 мс, а просил блок клиента
целиком (2048 отсчётов = 42.7 мс) — маркер выходил через два или три тика,
то есть через 40 или 60 мс. Слой второй: MSHV отвечает пачками по 4-5 блоков
раз в ~44 мс (STREAM_C = 4096 при 96 кГц), и мелкий квант этого не лечит —
нужен запас не меньше пачки.

Сделано:
* квант запроса = один блок TXA (512 отсчётов движка), а не блок клиента;
* свой поток-планировщик TxTickLoop с АБСОЛЮТНЫМИ дедлайнами (опоздание
  одного пробуждения не сдвигает сетку); общий тик маркеров больше не шлёт;
* бухгалтерия Owed/InFlight в кадрах на канал, гасится по k до интерполятора;
  потолок долга обязан быть выше окна в полёте (TCI_TX_OWED_HEADROOM_Q),
  иначе связывающим становится он и подача падает до 58% реального времени
  при полностью исправном клиенте;
* SendBinNow: маркеры пишутся в сокет напрямую под FWriteLock, минуя очередь
  (та выпускается лишь на пробуждении потока клиента, TCI_POLL_MS = 20 мс —
  вдвое больше кванта, и подача снова рвалась);
* FReapLock: планировщик TX — новый поток, а правило «клиента освобождает
  только тик-поток» держалось на том, что им же он и пользуется;
* аванс под зернистость клиента: измеряется по ПЕРИОДУ между пачками (размер
  пачки зависит от того, сколько мы запросили ⇒ положительная обратная связь),
  переживает конец передачи, умеет уменьшаться по выдержке TCI_TX_LEAD_DOWN_MS,
  потолок TCI_TX_LEAD_MAX_MS;
* старт передачи: KickTxTick будит планировщика на фронте PTT, TxPreWarm шлёт
  один маркер ДО SetMOX (41 мс раздумий клиента накладываются на нашу же
  подготовку тракта) под гейтом «передатчик свободен и чужого источника нет»,
  PrimeDUCIQ для источника TCI растянут до Max(6096, аванс×4) — путь микрофона
  радио, CW и web не затронут;
* посев аванса TCI_TX_LEAD_DEF_MS = 50 мс, пока про клиента ничего не известно:
  обучение к первому осушению физически не успевает.

Монотонные часы одного источника для всех потоков — PlatformUtils.MonotonicUs
(абсолютные дедлайны не терпят часов, способных прыгнуть от NTP).

На железе: опасных осушений посреди передачи НОЛЬ (было 12 за 11 с), четыре
передачи из пяти вообще без единого, включая старт; прогон 15:21 чист везде,
в том числе на первой передаче после подключения. Всплесков оператор больше
не видит.

Приборы: TxTrace.pas (EWSDR_TXTRACE=1) и стенд test/hpsdr — кольцевой tcpdump
capture.sh, разбор дампа pcap_tx_scan.py, разбор трассы txtrace_scan.py.
Стенд test/tci: часть F «Пейсинг TX», 260 проверок, провалов нет; живой клиент
с рампой, RTT и потерями — test/tci/tx_chrono_bench.py.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014gmVQnna1i4EbSZ2VGm6KD
2026-08-23 22:06:10 +03:00

140 lines
4.5 KiB
ObjectPascal

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.