mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 17:27:32 +00:00
Посреди передачи из 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
140 lines
4.5 KiB
ObjectPascal
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.
|