mirror of
https://git.vladimir.cc/vladimir/ewsdr.git
synced 2026-08-25 20:27:33 +00:00
chore(tx): детектор голодания снят — всплесков не видно, вернём при нужде
Прибор из b173b7f отработал своё на этом заходе: всплески не появляются, и
держать его в тракте нет причин. Снято ровно то, что тот коммит поставил:
юнит TxHealth.pas, замеры простоев в отправителе DUC (обе ветки), публикация
контекста пейсинга из планировщика TCI, счёт бита HPS_FIFO_EMPTY, вызовы
TxHealthService в таймере UI и цикле демона, индикатор «·сухо N» в статусной
строке и секция G стенда.
Вернуть — реверсом этого коммита; опора детектора и правило его устройства
(в тракте передачи ничего, кроме записи в память) записаны в
test/hpsdr/README.md, чтобы не восстанавливать рассуждение заново.
Проверено: test/tci 262/262, test/cat 50/50, GUI (--ws=qt6) и демон собираются.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkwwyj7xVRrqnSVEseTRfV
This commit is contained in:
+1
-22
@@ -19,7 +19,7 @@ uses
|
|||||||
{$ELSE}
|
{$ELSE}
|
||||||
Sockets, BaseUnix,
|
Sockets, BaseUnix,
|
||||||
{$ENDIF}
|
{$ENDIF}
|
||||||
HPSDRProtocol, SyncObjs, Settings, RadioBackend, TxHealth, 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
|
||||||
@@ -497,7 +497,6 @@ const
|
|||||||
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
|
DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter
|
||||||
var
|
var
|
||||||
Pkt: TDUCIQPacket;
|
Pkt: TDUCIQPacket;
|
||||||
DryFrom: Int64; // детектор: начало простоя очереди, мкс (см. TxHealth)
|
|
||||||
{$IFDEF WINDOWS}
|
{$IFDEF WINDOWS}
|
||||||
QPCFreq: Int64;
|
QPCFreq: Int64;
|
||||||
QPCValue: Int64;
|
QPCValue: Int64;
|
||||||
@@ -552,15 +551,11 @@ begin
|
|||||||
if not QueryPerformanceFrequency(QPCFreq) or (QPCFreq <= 0) then
|
if not QueryPerformanceFrequency(QPCFreq) or (QPCFreq <= 0) then
|
||||||
QPCFreq := 1000;
|
QPCFreq := 1000;
|
||||||
NextDueUs := ClockUs;
|
NextDueUs := ClockUs;
|
||||||
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
|
||||||
// Детектор голодания: очередь пуста на передаче. Стоимость — одно чтение
|
|
||||||
// монотонных часов на ПЕРЕХОД, ни файлов, ни локов (см. TxHealth.pas).
|
|
||||||
if FNet.FIsTransmitting and (DryFrom = 0) then DryFrom := MonotonicUs;
|
|
||||||
RTLEventWaitFor(FNet.FDUCIQSem, 20);
|
RTLEventWaitFor(FNet.FDUCIQSem, 20);
|
||||||
NowUs := ClockUs;
|
NowUs := ClockUs;
|
||||||
if NextDueUs < NowUs - DUC_PACKET_INTERVAL_US then
|
if NextDueUs < NowUs - DUC_PACKET_INTERVAL_US then
|
||||||
@@ -573,12 +568,6 @@ begin
|
|||||||
if not FNet.FIsTransmitting then
|
if not FNet.FIsTransmitting then
|
||||||
Continue;
|
Continue;
|
||||||
|
|
||||||
if DryFrom <> 0 then
|
|
||||||
begin
|
|
||||||
TxHealthDrought(DryFrom, MonotonicUs, 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);
|
||||||
@@ -591,16 +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;
|
||||||
// Детектор голодания: очередь пуста на передаче. Стоимость — одно чтение
|
|
||||||
// монотонных часов на ПЕРЕХОД, ни файлов, ни локов (см. TxHealth.pas).
|
|
||||||
if FNet.FIsTransmitting and (DryFrom = 0) then DryFrom := MonotonicUs;
|
|
||||||
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
// Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо
|
||||||
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
// в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как
|
||||||
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
// dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём
|
||||||
@@ -619,12 +604,6 @@ begin
|
|||||||
if not FNet.FIsTransmitting then
|
if not FNet.FIsTransmitting then
|
||||||
Continue;
|
Continue;
|
||||||
|
|
||||||
if DryFrom <> 0 then
|
|
||||||
begin
|
|
||||||
TxHealthDrought(DryFrom, MonotonicUs, 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);
|
||||||
|
|||||||
+5
-17
@@ -40,7 +40,7 @@ uses
|
|||||||
PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu,
|
PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu,
|
||||||
WidebandView,
|
WidebandView,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
PlatformUtils, TxHealth,
|
PlatformUtils,
|
||||||
RadioModes,
|
RadioModes,
|
||||||
WinFirewall,
|
WinFirewall,
|
||||||
BoardUtils, WisdomBuilder, UISync,
|
BoardUtils, WisdomBuilder, UISync,
|
||||||
@@ -3822,14 +3822,9 @@ var
|
|||||||
PeakTarget, MinTarget: Double;
|
PeakTarget, MinTarget: Double;
|
||||||
PeakDiff, MinDiff: Double;
|
PeakDiff, MinDiff: Double;
|
||||||
PeakAlpha, MinAlpha: Double;
|
PeakAlpha, MinAlpha: Double;
|
||||||
TxhSuffix: string;
|
|
||||||
begin
|
begin
|
||||||
// Затухание спотов не зависит от того, крутится ли приём.
|
// Затухание спотов не зависит от того, крутится ли приём.
|
||||||
ServiceDXCluster;
|
ServiceDXCluster;
|
||||||
// Детектор голодания очереди TX: сводки за прошедшие передачи → журнал.
|
|
||||||
// ★Это ЕДИНСТВЕННОЕ место, где прибор трогает диск, и оно намеренно вынесено
|
|
||||||
// из тракта передачи в таймер UI (см. TxHealth.pas).
|
|
||||||
TxHealthService;
|
|
||||||
|
|
||||||
if not FController.FRunning then
|
if not FController.FRunning then
|
||||||
begin
|
begin
|
||||||
@@ -3882,24 +3877,17 @@ begin
|
|||||||
|
|
||||||
if FController.FWDSPReady then
|
if FController.FWDSPReady then
|
||||||
begin
|
begin
|
||||||
// Опасные простои очереди TX за сеанс: столько раз очередь пустела дольше
|
|
||||||
// подушки отправителя, то есть столько раз в эфир могла уйти голая несущая.
|
|
||||||
// Ноль — значит всплеск, если он был виден, родился НЕ в нашем тракте.
|
|
||||||
if TxHealthDangerTotal > 0 then
|
|
||||||
TxhSuffix := Format(' ·сухо %d', [TxHealthDangerTotal])
|
|
||||||
else
|
|
||||||
TxhSuffix := '';
|
|
||||||
if FController.FTransmitting then
|
if FController.FTransmitting then
|
||||||
begin
|
begin
|
||||||
case FController.FDSPEngine.TXMicSource of
|
case FController.FDSPEngine.TXMicSource of
|
||||||
txmsSoundCard: SetStatusText(5, 'TX sound card' + TxhSuffix);
|
txmsSoundCard: SetStatusText(5, 'TX sound card');
|
||||||
txmsWeb: SetStatusText(5, 'TX web mic' + TxhSuffix);
|
txmsWeb: SetStatusText(5, 'TX web mic');
|
||||||
else
|
else
|
||||||
SetStatusText(5, 'TX radio mic' + TxhSuffix);
|
SetStatusText(5, 'TX radio mic');
|
||||||
end;
|
end;
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
SetStatusText(5, 'TX idle' + TxhSuffix);
|
SetStatusText(5, 'TX idle');
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
|||||||
+1
-23
@@ -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, TxHealth;
|
CWDecoder;
|
||||||
|
|
||||||
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
// Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для
|
||||||
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
// движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1).
|
||||||
@@ -1864,11 +1864,6 @@ begin
|
|||||||
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
|
FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0;
|
||||||
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0;
|
||||||
FHPpOverload := Status.ADCOverload;
|
FHPpOverload := Status.ADCOverload;
|
||||||
// ★Жалоба самого радио: бит «DUC FIFO пуст». Поле ГЛУБИНЫ FIFO (байты 35-36)
|
|
||||||
// на этой прошивке статично и веры ему нет, а бит мы не читали вовсе —
|
|
||||||
// если прошивка его заполняет, это независимый свидетель голодания.
|
|
||||||
if FTransmitting and ((Status.StatusBits and HPS_FIFO_EMPTY) <> 0) then
|
|
||||||
TxHealthFifoEmpty;
|
|
||||||
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
// Состояние ключа/манипулятора в разъёме трансивера (сетевой поток).
|
||||||
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
|
// Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit»
|
||||||
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
|
// останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по
|
||||||
@@ -6297,7 +6292,6 @@ end;
|
|||||||
procedure TRadioController.SetMOX(On_: Boolean);
|
procedure TRadioController.SetMOX(On_: Boolean);
|
||||||
var
|
var
|
||||||
WasTransmitting: Boolean;
|
WasTransmitting: Boolean;
|
||||||
TxhSrc: Integer;
|
|
||||||
begin
|
begin
|
||||||
// DMR support is receive-only. This guard also covers CAT, web and hardware
|
// DMR support is receive-only. This guard also covers CAT, web and hardware
|
||||||
// PTT. FM RAW is a flat external-modem transmit path.
|
// PTT. FM RAW is a flat external-modem transmit path.
|
||||||
@@ -6432,19 +6426,6 @@ begin
|
|||||||
if FTransmitting and FDisplayDuplex then
|
if FTransmitting and FDisplayDuplex then
|
||||||
FDSPEngine.SetDisplaySourceTX(False);
|
FDSPEngine.SetDisplaySourceTX(False);
|
||||||
end;
|
end;
|
||||||
// Детектор голодания очереди TX: фронт «в эфир» открывает передачу для
|
|
||||||
// счётчиков. Стоит ПОСЛЕ блока движка — источник модуляции решается там.
|
|
||||||
// Ввода-вывода тут нет и быть не может: журнал пишет потребитель (таймер UI /
|
|
||||||
// цикл демона), см. TxHealth.pas.
|
|
||||||
if FTransmitting and (not WasTransmitting) then
|
|
||||||
begin
|
|
||||||
if FTCIMicActive then TxhSrc := txhSrcTCI
|
|
||||||
else if FWebMicActive then TxhSrc := txhSrcWeb
|
|
||||||
else if Assigned(FDSPEngine) and (FDSPEngine.TXMicSource = txmsSoundCard) then
|
|
||||||
TxhSrc := txhSrcCard
|
|
||||||
else TxhSrc := txhSrcRadio;
|
|
||||||
TxHealthPtt(True, TxhSrc);
|
|
||||||
end;
|
|
||||||
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
|
// Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX.
|
||||||
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then
|
||||||
begin
|
begin
|
||||||
@@ -6462,9 +6443,6 @@ begin
|
|||||||
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
|
// (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой.
|
||||||
SyncCWKeyer;
|
SyncCWKeyer;
|
||||||
Changed(rfTransmitting);
|
Changed(rfTransmitting);
|
||||||
// Передача кончилась — складываем сводку в память. Диск не трогаем (★прошлый
|
|
||||||
// прибор писал дамп ровно здесь и тем самым прикрывал гонку в пейсинге).
|
|
||||||
if WasTransmitting and (not FTransmitting) then TxHealthPtt(False, 0);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TRadioController.SetTune(On_: Boolean);
|
procedure TRadioController.SetTune(On_: Boolean);
|
||||||
|
|||||||
+1
-7
@@ -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, TxHealth;
|
DXSpotStore, TCIProtocol, TCIServer, TCIStreams;
|
||||||
|
|
||||||
const
|
const
|
||||||
// ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА
|
// ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА
|
||||||
@@ -3997,12 +3997,6 @@ begin
|
|||||||
TxNoteRequest(NowUs);
|
TxNoteRequest(NowUs);
|
||||||
Inc(Sent);
|
Inc(Sent);
|
||||||
end;
|
end;
|
||||||
// Снимок пейсинга для детектора голодания: поток отправителя DUC приложит
|
|
||||||
// его к очередному опасному простою и тем ответит, чей это простой —
|
|
||||||
// аванса, зернистости клиента или планировщика. Простые записи, без лока и
|
|
||||||
// без ввода-вывода (см. TxHealth.pas).
|
|
||||||
TxHealthPublishTci(FTxLead, Round(FTxOwed), Round(FTxInFlight), FTxQuantum,
|
|
||||||
Integer(PeriodUs), Round(FTxGapUs));
|
|
||||||
finally
|
finally
|
||||||
FTxLock.Leave;
|
FTxLock.Leave;
|
||||||
end;
|
end;
|
||||||
|
|||||||
-358
@@ -1,358 +0,0 @@
|
|||||||
unit TxHealth;
|
|
||||||
|
|
||||||
{
|
|
||||||
TxHealth.pas — тихий детектор голодания очереди TX.
|
|
||||||
|
|
||||||
ЗАЧЕМ. Всплеск своего сигнала, рождённый НАШИМ трактом, физически невозможен
|
|
||||||
без одного события: очередь DUC простаивает дольше подушки отправителя
|
|
||||||
(DUC_FIFO_THROTTLE = 2000 отсчётов при 192 кГц = 10.42 мс). Доказано pcap-
|
|
||||||
съёмом: шесть всплесков в дампе — шесть простоев 10.3-23.5 мс, а паузы 8 мс и
|
|
||||||
короче не дали ни одного. Значит счётчик таких простоев — ПОЛНЫЙ детектор:
|
|
||||||
молчит он — всплеск не наш (смотреть приёмный тракт, PA, реле, спуры),
|
|
||||||
сработал — наш, и момент известен с точностью до микросекунд.
|
|
||||||
|
|
||||||
★ГЛАВНОЕ ПРАВИЛО, оплаченное регрессом. Прошлый прибор (TxTrace) писал дамп
|
|
||||||
СИНХРОННО в хвосте SetMOX — десятки миллисекунд ровно в том окне, где жила
|
|
||||||
гонка FTxRunning, и тем самым ЧИНИЛ дефект собой: с зондами всплесков не было,
|
|
||||||
без них они возвращались. Поэтому здесь в тракте передачи не делается НИЧЕГО,
|
|
||||||
кроме записи в память:
|
|
||||||
* потоки передачи (отправитель DUC, сетевой поток статуса, планировщик TCI)
|
|
||||||
только пишут в кольцо и в счётчики — ни файлов, ни локов, ни форматирования;
|
|
||||||
* ВСЮ работу с диском делает ПОТРЕБИТЕЛЬ (таймер UI 100 мс / цикл демона),
|
|
||||||
вызывая TxHealthService. Своих потоков юнит не заводит.
|
|
||||||
|
|
||||||
Детектор включён ВСЕГДА: переменной окружения нет намеренно — прибор, который
|
|
||||||
надо не забыть включить, к моменту дефекта выключен.
|
|
||||||
}
|
|
||||||
|
|
||||||
{$IFDEF FPC}
|
|
||||||
{$MODE Delphi}
|
|
||||||
{$ENDIF}
|
|
||||||
|
|
||||||
interface
|
|
||||||
|
|
||||||
const
|
|
||||||
// Подушка отправителя: DUC_FIFO_THROTTLE = 2000 отсчётов при DUC_SAMPLE_RATE
|
|
||||||
// = 192000 (копия из HPSDRNetwork.pas — юнит намеренно ни от чего не зависит).
|
|
||||||
// Простой ДЛИННЕЕ неё гарантированно осушает FIFO радио: в эфире остаётся
|
|
||||||
// голая несущая гетеродина DUC.
|
|
||||||
TXH_CUSHION_US = 10417;
|
|
||||||
|
|
||||||
// Источник модуляции в сводке (что именно кормило передатчик).
|
|
||||||
txhSrcRadio = 0;
|
|
||||||
txhSrcCard = 1;
|
|
||||||
txhSrcWeb = 2;
|
|
||||||
txhSrcTCI = 3;
|
|
||||||
|
|
||||||
type
|
|
||||||
// Один опасный простой очереди — с контекстом пейсинга TCI на тот момент.
|
|
||||||
TTxDrought = record
|
|
||||||
Session: LongInt; // к какой передаче относится
|
|
||||||
AtMs: Integer; // от фронта PTT
|
|
||||||
LenUs: Integer; // длительность простоя
|
|
||||||
Queue: Integer; // глубина очереди DUC на выходе из простоя
|
|
||||||
Lead: Integer; // аванс, кадров (0 — модуляция не из TCI)
|
|
||||||
Owed: Integer; // долг, кадров
|
|
||||||
InFlight: Integer; // в полёте, кадров
|
|
||||||
Quantum: Integer; // квант запроса, кадров
|
|
||||||
StepUs: Integer; // шаг планировщика
|
|
||||||
GapUs: Integer; // период между пачками клиента
|
|
||||||
end;
|
|
||||||
|
|
||||||
// Сводка по одной передаче.
|
|
||||||
TTxSession = record
|
|
||||||
Id: LongInt;
|
|
||||||
StartedAt: TDateTime;
|
|
||||||
LenMs: Integer;
|
|
||||||
Source: Integer; // txhSrc*
|
|
||||||
Droughts: Integer; // простоев всего
|
|
||||||
Dangerous: Integer; // из них длиннее подушки
|
|
||||||
LongestUs: Integer;
|
|
||||||
FirstDangerMs: Integer; // -1 — опасных не было
|
|
||||||
FifoEmpty: Integer; // HP-статусов с битом «DUC FIFO пуст»
|
|
||||||
end;
|
|
||||||
|
|
||||||
// ── Пишут потоки передачи (только память) ───────────────────────────────────
|
|
||||||
procedure TxHealthPtt(On_: Boolean; Source: Integer);
|
|
||||||
procedure TxHealthDrought(FromUs, ToUs: Int64; QueueDepth: Integer);
|
|
||||||
procedure TxHealthFifoEmpty;
|
|
||||||
procedure TxHealthPublishTci(Lead, Owed, InFlight, Quantum, StepUs,
|
|
||||||
GapUs: Integer);
|
|
||||||
|
|
||||||
// ── Зовёт потребитель (UI-таймер / цикл демона) ─────────────────────────────
|
|
||||||
{ Забрать готовые сводки и дописать их в ~/.config/ewsdr/txhealth.log.
|
|
||||||
Возвращает число записанных передач. Работа с диском — только здесь. }
|
|
||||||
function TxHealthService: Integer;
|
|
||||||
{ Снять следующую сводку, не трогая диск (Service стоит на этих двух; стенду и
|
|
||||||
любому иному потребителю — тоже). }
|
|
||||||
function TxHealthTake(out S: TTxSession): Boolean;
|
|
||||||
{ Index-й опасный простой этой передачи, пока он ещё жив в кольце. }
|
|
||||||
function TxHealthDetail(Id: LongInt; Index: Integer; out D: TTxDrought): Boolean;
|
|
||||||
{ Опасных простоев за весь сеанс — для индикации в UI. }
|
|
||||||
function TxHealthDangerTotal: Integer;
|
|
||||||
{ Последняя записанная сводка (для UI/стенда), False — сводок ещё не было. }
|
|
||||||
function TxHealthLastSession(out S: TTxSession): Boolean;
|
|
||||||
{ Путь журнала. }
|
|
||||||
function TxHealthLogPath: string;
|
|
||||||
|
|
||||||
implementation
|
|
||||||
|
|
||||||
uses
|
|
||||||
SysUtils, Math, PlatformUtils;
|
|
||||||
|
|
||||||
const
|
|
||||||
RING_SIZE = 256; // опасных простоев в памяти (хвост важнее)
|
|
||||||
RING_MASK = RING_SIZE - 1;
|
|
||||||
SESS_SIZE = 32; // сводок, ждущих потребителя
|
|
||||||
SESS_MASK = SESS_SIZE - 1;
|
|
||||||
MAX_DETAIL = 24; // строк подробностей на передачу в журнале
|
|
||||||
|
|
||||||
var
|
|
||||||
// Кольцо опасных простоев: пишет поток отправителя DUC, читает потребитель.
|
|
||||||
Ring: array[0..RING_SIZE - 1] of TTxDrought;
|
|
||||||
RingSeq: LongInt = 0;
|
|
||||||
|
|
||||||
// Кольцо сводок: пишет поток, снимающий PTT, читает потребитель.
|
|
||||||
Sess: array[0..SESS_SIZE - 1] of TTxSession;
|
|
||||||
SessSeq: LongInt = 0; // сколько сводок сложено
|
|
||||||
SessRead: LongInt = 0; // сколько забрал потребитель
|
|
||||||
|
|
||||||
// Текущая передача.
|
|
||||||
CurId: LongInt = 0;
|
|
||||||
CurStartUs: Int64 = 0;
|
|
||||||
CurStartAt: TDateTime = 0;
|
|
||||||
CurSource: Integer = txhSrcRadio;
|
|
||||||
CurOn: Boolean = False;
|
|
||||||
CurDroughts, CurDanger, CurLongest, CurFirstMs, CurFifoEmpty: Integer;
|
|
||||||
|
|
||||||
// Снимок пейсинга TCI — публикует планировщик, читает поток отправителя.
|
|
||||||
// Простые записи без лока: числа диагностические, рвань в них безвредна, а
|
|
||||||
// лок в потоке отправителя DUC — недопустимый риск для тайминга.
|
|
||||||
PubLead, PubOwed, PubFlight, PubQuantum, PubStep, PubGap: Integer;
|
|
||||||
|
|
||||||
DangerTotal: Integer = 0;
|
|
||||||
LastSess: TTxSession;
|
|
||||||
HaveLast: Boolean = False;
|
|
||||||
|
|
||||||
procedure TxHealthPtt(On_: Boolean; Source: Integer);
|
|
||||||
// Фронт PTT. Только память: несколько записей, никакого ввода-вывода.
|
|
||||||
var
|
|
||||||
S: TTxSession;
|
|
||||||
Idx: LongInt;
|
|
||||||
begin
|
|
||||||
if On_ then
|
|
||||||
begin
|
|
||||||
if CurOn then Exit;
|
|
||||||
InterlockedIncrement(CurId);
|
|
||||||
CurStartUs := MonotonicUs;
|
|
||||||
CurStartAt := Now;
|
|
||||||
CurSource := Source;
|
|
||||||
CurDroughts := 0;
|
|
||||||
CurDanger := 0;
|
|
||||||
CurLongest := 0;
|
|
||||||
CurFirstMs := -1;
|
|
||||||
CurFifoEmpty := 0;
|
|
||||||
CurOn := True;
|
|
||||||
Exit;
|
|
||||||
end;
|
|
||||||
|
|
||||||
if not CurOn then Exit;
|
|
||||||
CurOn := False;
|
|
||||||
S.Id := CurId;
|
|
||||||
S.StartedAt := CurStartAt;
|
|
||||||
S.LenMs := Integer((MonotonicUs - CurStartUs) div 1000);
|
|
||||||
S.Source := CurSource;
|
|
||||||
S.Droughts := CurDroughts;
|
|
||||||
S.Dangerous := CurDanger;
|
|
||||||
S.LongestUs := CurLongest;
|
|
||||||
S.FirstDangerMs := CurFirstMs;
|
|
||||||
S.FifoEmpty := CurFifoEmpty;
|
|
||||||
Idx := InterlockedIncrement(SessSeq) - 1;
|
|
||||||
Sess[Idx and SESS_MASK] := S;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TxHealthDrought(FromUs, ToUs: Int64; QueueDepth: Integer);
|
|
||||||
// Поток отправителя DUC, на выходе очереди из простоя. Дороже всего здесь одно
|
|
||||||
// вычитание: время простоя измерил вызывающий, он же держит его границы.
|
|
||||||
var
|
|
||||||
LenUs, AtMs: Integer;
|
|
||||||
Idx: LongInt;
|
|
||||||
P: ^TTxDrought;
|
|
||||||
begin
|
|
||||||
if not CurOn then Exit;
|
|
||||||
LenUs := Integer(ToUs - FromUs);
|
|
||||||
if LenUs < 0 then Exit;
|
|
||||||
Inc(CurDroughts);
|
|
||||||
if LenUs > CurLongest then CurLongest := LenUs;
|
|
||||||
if LenUs <= TXH_CUSHION_US then Exit;
|
|
||||||
|
|
||||||
Inc(CurDanger);
|
|
||||||
InterlockedIncrement(DangerTotal);
|
|
||||||
AtMs := Integer((FromUs - CurStartUs) div 1000);
|
|
||||||
if CurFirstMs < 0 then CurFirstMs := AtMs;
|
|
||||||
|
|
||||||
Idx := InterlockedIncrement(RingSeq) - 1;
|
|
||||||
P := @Ring[Idx and RING_MASK];
|
|
||||||
P^.Session := CurId;
|
|
||||||
P^.AtMs := AtMs;
|
|
||||||
P^.LenUs := LenUs;
|
|
||||||
P^.Queue := QueueDepth;
|
|
||||||
P^.Lead := PubLead;
|
|
||||||
P^.Owed := PubOwed;
|
|
||||||
P^.InFlight := PubFlight;
|
|
||||||
P^.Quantum := PubQuantum;
|
|
||||||
P^.StepUs := PubStep;
|
|
||||||
P^.GapUs := PubGap;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TxHealthFifoEmpty;
|
|
||||||
// Сетевой поток, разбор HP-статуса: радио говорит, что его DUC FIFO пуст.
|
|
||||||
// ★На этой прошивке поле ГЛУБИНЫ FIFO статично (всегда 256) и веры ему нет —
|
|
||||||
// а вот бит в байте статуса мы раньше не читали вовсе. Если прошивка его
|
|
||||||
// заполняет, получаем жалобу самого железа как независимого свидетеля.
|
|
||||||
begin
|
|
||||||
if CurOn then Inc(CurFifoEmpty);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TxHealthPublishTci(Lead, Owed, InFlight, Quantum, StepUs,
|
|
||||||
GapUs: Integer);
|
|
||||||
// Планировщик TCI. Публикация состояния пейсинга для контекста простоев.
|
|
||||||
begin
|
|
||||||
PubLead := Lead;
|
|
||||||
PubOwed := Owed;
|
|
||||||
PubFlight := InFlight;
|
|
||||||
PubQuantum := Quantum;
|
|
||||||
PubStep := StepUs;
|
|
||||||
PubGap := GapUs;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthDangerTotal: Integer;
|
|
||||||
begin
|
|
||||||
Result := DangerTotal;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthLastSession(out S: TTxSession): Boolean;
|
|
||||||
begin
|
|
||||||
Result := HaveLast;
|
|
||||||
if Result then S := LastSess;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthLogPath: string;
|
|
||||||
begin
|
|
||||||
Result := GetAppCfgDir + 'txhealth.log';
|
|
||||||
end;
|
|
||||||
|
|
||||||
function SourceName(Src: Integer): string;
|
|
||||||
begin
|
|
||||||
case Src of
|
|
||||||
txhSrcTCI: Result := 'TCI';
|
|
||||||
txhSrcWeb: Result := 'web';
|
|
||||||
txhSrcCard: Result := 'звуковая карта';
|
|
||||||
else
|
|
||||||
Result := 'микрофон радио';
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthTake(out S: TTxSession): Boolean;
|
|
||||||
var Have: LongInt;
|
|
||||||
begin
|
|
||||||
Have := SessSeq;
|
|
||||||
if Have <= SessRead then Exit(False);
|
|
||||||
// Сводки, которые кольцо успело затереть, пропускаем: журнал честно отражает
|
|
||||||
// хвост, а отстать на SESS_SIZE передач потребитель может лишь простояв
|
|
||||||
// секунды — чего с таймером 100 мс не бывает.
|
|
||||||
if Have - SessRead > SESS_SIZE then SessRead := Have - SESS_SIZE;
|
|
||||||
S := Sess[SessRead and SESS_MASK];
|
|
||||||
Inc(SessRead);
|
|
||||||
LastSess := S;
|
|
||||||
HaveLast := True;
|
|
||||||
Result := True;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthDetail(Id: LongInt; Index: Integer; out D: TTxDrought): Boolean;
|
|
||||||
var
|
|
||||||
i, Seen, Total: LongInt;
|
|
||||||
begin
|
|
||||||
Result := False;
|
|
||||||
Total := RingSeq;
|
|
||||||
Seen := 0;
|
|
||||||
for i := Max(0, Total - RING_SIZE) to Total - 1 do
|
|
||||||
begin
|
|
||||||
if Ring[i and RING_MASK].Session <> Id then Continue;
|
|
||||||
if Seen = Index then
|
|
||||||
begin
|
|
||||||
D := Ring[i and RING_MASK];
|
|
||||||
Exit(True);
|
|
||||||
end;
|
|
||||||
Inc(Seen);
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure WriteSession(var F: TextFile; const S: TTxSession);
|
|
||||||
// Одна передача: строка сводки плюс подробности по опасным простоям.
|
|
||||||
var
|
|
||||||
Shown: Integer;
|
|
||||||
R: TTxDrought;
|
|
||||||
Verdict: string;
|
|
||||||
begin
|
|
||||||
if S.Dangerous > 0 then
|
|
||||||
Verdict := 'ОПАСНО'
|
|
||||||
else
|
|
||||||
Verdict := 'чисто';
|
|
||||||
if S.FirstDangerMs >= 0 then
|
|
||||||
Verdict := Verdict + Format(' (первое на %d мс)', [S.FirstDangerMs]);
|
|
||||||
WriteLn(F, Format('%s %s передача %.1f с, источник %s: осушений %d, ' +
|
|
||||||
'опасных %d, самое длинное %.1f мс, FIFO-EMPTY %d',
|
|
||||||
[FormatDateTime('yyyy-mm-dd hh:nn:ss', S.StartedAt), Verdict,
|
|
||||||
S.LenMs / 1000.0, SourceName(S.Source), S.Droughts, S.Dangerous,
|
|
||||||
S.LongestUs / 1000.0, S.FifoEmpty]));
|
|
||||||
if S.Dangerous <= 0 then Exit;
|
|
||||||
|
|
||||||
Shown := 0;
|
|
||||||
while TxHealthDetail(S.Id, Shown, R) do
|
|
||||||
begin
|
|
||||||
Inc(Shown);
|
|
||||||
if Shown > MAX_DETAIL then
|
|
||||||
begin
|
|
||||||
WriteLn(F, ' … остальные опущены');
|
|
||||||
Break;
|
|
||||||
end;
|
|
||||||
WriteLn(F, Format(' опасный #%d: на %d мс от PTT, %.1f мс, очередь %d, ' +
|
|
||||||
'аванс %d, долг %d, в полёте %d, квант %d, шаг %d мкс, ' +
|
|
||||||
'пачка клиента %.1f мс',
|
|
||||||
[Shown, R.AtMs, R.LenUs / 1000.0, R.Queue, R.Lead, R.Owed,
|
|
||||||
R.InFlight, R.Quantum, R.StepUs, R.GapUs / 1000.0]));
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TxHealthService: Integer;
|
|
||||||
// ★Единственное место с диском, и зовут его только потребители (таймер UI,
|
|
||||||
// цикл демона) — никогда не поток передачи.
|
|
||||||
var
|
|
||||||
F: TextFile;
|
|
||||||
S: TTxSession;
|
|
||||||
begin
|
|
||||||
Result := 0;
|
|
||||||
if SessSeq <= SessRead then Exit;
|
|
||||||
|
|
||||||
AssignFile(F, TxHealthLogPath);
|
|
||||||
try
|
|
||||||
if FileExists(TxHealthLogPath) then Append(F) else Rewrite(F);
|
|
||||||
try
|
|
||||||
while TxHealthTake(S) do
|
|
||||||
begin
|
|
||||||
WriteSession(F, S);
|
|
||||||
Inc(Result);
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
CloseFile(F);
|
|
||||||
end;
|
|
||||||
except
|
|
||||||
// Журнал — вспомогательный: не пишется, так и ладно. Сводки при этом
|
|
||||||
// считаем забранными, иначе они копились бы и пытались писаться каждые
|
|
||||||
// 100 мс.
|
|
||||||
on E: Exception do SessRead := SessSeq;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
end.
|
|
||||||
+1
-4
@@ -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, TxHealth;
|
HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils;
|
||||||
|
|
||||||
const
|
const
|
||||||
PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер)
|
PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер)
|
||||||
@@ -470,9 +470,6 @@ begin
|
|||||||
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и
|
// Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и
|
||||||
// отпустить реле T/R после выдержки больше некому).
|
// отпустить реле T/R после выдержки больше некому).
|
||||||
FCtrl.ServiceCWKeyed;
|
FCtrl.ServiceCWKeyed;
|
||||||
// Детектор голодания очереди TX: сводки за прошедшие передачи → журнал.
|
|
||||||
// Единственное место, где прибор трогает диск (см. TxHealth.pas).
|
|
||||||
TxHealthService;
|
|
||||||
lastBeacon := nowMs;
|
lastBeacon := nowMs;
|
||||||
end;
|
end;
|
||||||
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
if nowMs - lastPush >= PUSH_INTERVAL_MS then
|
||||||
|
|||||||
+20
-43
@@ -105,54 +105,31 @@ TX голодала, в эфире дыры. Тут же диапазон глу
|
|||||||
* `capture.sh` — кольцевой съём tcpdump + лог потерь ядра.
|
* `capture.sh` — кольцевой съём tcpdump + лог потерь ядра.
|
||||||
* `pcap_tx_scan.py` — разбор pcap, чистый Python без зависимостей (~40 МБ/с).
|
* `pcap_tx_scan.py` — разбор pcap, чистый Python без зависимостей (~40 МБ/с).
|
||||||
|
|
||||||
## Тихий детектор голодания очереди TX (включён всегда)
|
## Тихий детектор голодания очереди TX (снят, вернуть из `b173b7f`)
|
||||||
|
|
||||||
Всплеск, рождённый НАШИМ трактом, невозможен без одного события: очередь DUC
|
Опора детектора: всплеск, рождённый НАШИМ трактом, невозможен без простоя
|
||||||
простаивает дольше подушки отправителя (`DUC_FIFO_THROTTLE` = 2000 отсчётов при
|
очереди DUC длиннее подушки отправителя (`DUC_FIFO_THROTTLE` = 2000 отсчётов при
|
||||||
192 кГц = **10.42 мс**). Это доказано съёмом 2026-08-21: шесть всплесков — шесть
|
192 кГц = **10.42 мс**) — съём 2026-08-21 дал шесть всплесков на шесть простоев
|
||||||
простоев 10.3–23.5 мс, паузы 8 мс и короче не дали ни одного. Поэтому счётчик
|
10.3–23.5 мс, а паузы 8 мс и короче не дали ни одного. Значит счётчик таких
|
||||||
таких простоев — **полный** детектор, и он теперь живёт в самой программе
|
простоев отвечает на вопрос «наш всплеск или не наш» полностью.
|
||||||
(`TxHealth.pas`), без переменных окружения: прибор, который надо не забыть
|
|
||||||
включить, к моменту дефекта выключен.
|
|
||||||
|
|
||||||
Что делает:
|
Прибор (`TxHealth.pas`) был включён всегда, без переменных окружения: поток
|
||||||
|
отправителя DUC мерил простои, планировщик TCI публиковал рядом контекст
|
||||||
|
пейсинга (аванс, долг, в полёте, квант, шаг, период пачек клиента), сетевой
|
||||||
|
поток считал бит `HPS_FIFO_EMPTY`. ★Журнал (`~/.config/ewsdr/txhealth.log`,
|
||||||
|
строка на передачу: «чисто» / «ОПАСНО (первое на N мс)» плюс подробности) писал
|
||||||
|
ТОЛЬКО потребитель — таймер UI и цикл демона; в тракте передачи диска не было по
|
||||||
|
построению. В статусной строке показывался счётчик `·сухо N` за сеанс.
|
||||||
|
|
||||||
* поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение
|
Вернуть целиком:
|
||||||
монотонных часов на переход, ни файлов, ни локов;
|
|
||||||
* планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте,
|
|
||||||
квант, шаг, период пачек клиента) — простыми записями;
|
|
||||||
* сетевой поток считает биты «DUC FIFO пуст» из HP-статуса радио (поле ГЛУБИНЫ
|
|
||||||
на этой прошивке статично и веры ему нет, а бит раньше не читался вовсе);
|
|
||||||
* журнал пишет **потребитель** — таймер UI (100 мс) или цикл демона, никогда не
|
|
||||||
тракт передачи.
|
|
||||||
|
|
||||||
★Последнее — правило, оплаченное регрессом: прошлый прибор (`TxTrace`) сбрасывал
|
git revert <коммит снятия> # он же и есть реверс b173b7f
|
||||||
дамп синхронно в хвосте `SetMOX`, и эти десятки миллисекунд закрывали гонку в
|
|
||||||
пейсинге TCI. С зондами всплесков не было, без них они возвращались —
|
|
||||||
инструмент чинил дефект собой (лечение: коммит `fa38fa7`).
|
|
||||||
|
|
||||||
Журнал: `~/.config/ewsdr/txhealth.log`, по строке на передачу.
|
★Правило, ради которого прибор так и построен: предыдущий инструмент
|
||||||
|
(`TxTrace`) сбрасывал дамп синхронно в хвосте `SetMOX` и этими десятками
|
||||||
```
|
миллисекунд закрывал гонку в пейсинге TCI — с зондами всплесков не было, без них
|
||||||
2026-08-24 19:08:03 ОПАСНО (первое на 100 мс) передача 12.4 с, источник TCI: осушений 17, опасных 1, самое длинное 18.0 мс, FIFO-EMPTY 0
|
они возвращались (лечение гонки — `fa38fa7`). Инструмент не имеет права ничего
|
||||||
опасный #1: на 100 мс от PTT, 18.0 мс, очередь 0, аванс 3584, долг 2200, в полёте 1024, квант 512, шаг 5333 мкс, пачка клиента 44.1 мс
|
делать в тракте передачи.
|
||||||
```
|
|
||||||
|
|
||||||
Как читать:
|
|
||||||
|
|
||||||
* **`чисто`** — за эту передачу очередь ни разу не пересыхала дольше подушки.
|
|
||||||
Значит увиденный всплеск родился НЕ в нашем тракте: смотреть приёмный тракт
|
|
||||||
(дыра в DDC — на экране есть, в эфире нет), PA, реле, спуры.
|
|
||||||
* **`ОПАСНО`** — простой был, и подробная строка называет виновника по контексту:
|
|
||||||
пачка клиента длиннее аванса (`пачка клиента` > `аванс`/48 мс) — зернистость
|
|
||||||
MSHV; `долг` у потолка при малом `в полёте` — планировщик не успевает;
|
|
||||||
`шаг` заметно больше половины кванта — проспал поток планировщика.
|
|
||||||
* Строка статуса в UI показывает `·сухо N` — сколько опасных простоев набралось
|
|
||||||
за сеанс, чтобы не приходилось помнить, какая посылка была плохой.
|
|
||||||
|
|
||||||
Правило разбора то же, что и с pcap: **дальше сопоставлять**. Детектор говорит
|
|
||||||
«очередь сохла в такой-то момент», `pcap_tx_scan.py` по тому же куску времени
|
|
||||||
скажет, ушёл ли всплеск в эфир или это артефакт приёма.
|
|
||||||
|
|
||||||
## Приборы внутри процесса: кольцевая трассировка TX-тракта
|
## Приборы внутри процесса: кольцевая трассировка TX-тракта
|
||||||
|
|
||||||
|
|||||||
+1
-79
@@ -28,7 +28,7 @@ program tcitest;
|
|||||||
uses
|
uses
|
||||||
cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix,
|
cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix,
|
||||||
TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController,
|
TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController,
|
||||||
WsClient, WebUtils, WDSPEngine, Settings, CWMorse, TxHealth, PlatformUtils;
|
WsClient, WebUtils, WDSPEngine, Settings, CWMorse;
|
||||||
|
|
||||||
var
|
var
|
||||||
Passed, Failed: Integer;
|
Passed, Failed: Integer;
|
||||||
@@ -2207,83 +2207,6 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TestTxHealth;
|
|
||||||
// Детектор голодания очереди TX (TxHealth.pas). Стенд проверяет его отдельно от
|
|
||||||
// сети: порог «опасного» простоя, сводку по передаче, контекст пейсинга рядом с
|
|
||||||
// событием и то, что вне передачи прибор молчит.
|
|
||||||
// ★Диска здесь нет намеренно: TxHealthService писал бы в НАСТОЯЩИЙ журнал
|
|
||||||
// пользователя, поэтому стенд берёт сводки через TxHealthTake.
|
|
||||||
var
|
|
||||||
Base: Int64;
|
|
||||||
S: TTxSession;
|
|
||||||
D: TTxDrought;
|
|
||||||
Was: Integer;
|
|
||||||
begin
|
|
||||||
WriteLn('G. Детектор голодания очереди TX');
|
|
||||||
|
|
||||||
// Сводки, накопленные прошлыми частями стенда (они гоняют SetMOX), забираем,
|
|
||||||
// чтобы мерить своё.
|
|
||||||
while TxHealthTake(S) do ;
|
|
||||||
Was := TxHealthDangerTotal;
|
|
||||||
|
|
||||||
// Вне передачи прибор обязан молчать: простой на приёме — это норма, DUC-поток
|
|
||||||
// тогда не шлёт вообще ничего.
|
|
||||||
Base := MonotonicUs;
|
|
||||||
TxHealthDrought(Base, Base + 50000, 0);
|
|
||||||
TxHealthFifoEmpty;
|
|
||||||
Check('детектор: вне передачи простой не считается',
|
|
||||||
TxHealthDangerTotal = Was, IntToStr(TxHealthDangerTotal - Was));
|
|
||||||
|
|
||||||
TxHealthPtt(True, txhSrcTCI);
|
|
||||||
TxHealthPublishTci(3584, 2200, 1024, 512, 5333, 44100);
|
|
||||||
Base := MonotonicUs;
|
|
||||||
// Короткий простой: подушка отправителя его покрывает, в эфир он не выходит.
|
|
||||||
TxHealthDrought(Base, Base + 8000, 7);
|
|
||||||
Check('детектор: простой короче подушки не опасен',
|
|
||||||
TxHealthDangerTotal = Was, IntToStr(TxHealthDangerTotal - Was));
|
|
||||||
// Длинный: FIFO радио сохнет, в эфире голая несущая.
|
|
||||||
TxHealthDrought(Base + 100000, Base + 118000, 0);
|
|
||||||
TxHealthFifoEmpty;
|
|
||||||
Check('детектор: простой длиннее подушки опасен',
|
|
||||||
TxHealthDangerTotal = Was + 1, IntToStr(TxHealthDangerTotal - Was));
|
|
||||||
TxHealthPtt(False, 0);
|
|
||||||
|
|
||||||
Check('детектор: сводка передачи снялась', TxHealthTake(S));
|
|
||||||
Check('детектор: простоев посчитано верно', S.Droughts = 2, IntToStr(S.Droughts));
|
|
||||||
Check('детектор: опасных посчитано верно', S.Dangerous = 1, IntToStr(S.Dangerous));
|
|
||||||
Check('детектор: самое длинное — длиннее подушки',
|
|
||||||
S.LongestUs = 18000, IntToStr(S.LongestUs));
|
|
||||||
Check('детектор: момент первого опасного — от фронта PTT',
|
|
||||||
(S.FirstDangerMs >= 90) and (S.FirstDangerMs <= 130),
|
|
||||||
IntToStr(S.FirstDangerMs));
|
|
||||||
Check('детектор: источник модуляции в сводке', S.Source = txhSrcTCI);
|
|
||||||
Check('детектор: жалоба радио (FIFO EMPTY) считается только на передаче',
|
|
||||||
S.FifoEmpty = 1, IntToStr(S.FifoEmpty));
|
|
||||||
|
|
||||||
// ★Ради этого прибор и делался: рядом с опасным простоем лежит состояние
|
|
||||||
// пейсинга — по нему видно, чей простой (аванса, зернистости клиента или
|
|
||||||
// планировщика), и оператору не нужно называть интервал на глаз.
|
|
||||||
Check('детектор: контекст пейсинга лежит рядом с событием',
|
|
||||||
TxHealthDetail(S.Id, 0, D));
|
|
||||||
Check('детектор: в контексте аванс и пачка клиента',
|
|
||||||
(D.Lead = 3584) and (D.GapUs = 44100) and (D.Quantum = 512) and
|
|
||||||
(D.LenUs = 18000),
|
|
||||||
Format('аванс %d, пачка %d, квант %d, простой %d',
|
|
||||||
[D.Lead, D.GapUs, D.Quantum, D.LenUs]));
|
|
||||||
Check('детектор: второго опасного в этой передаче нет',
|
|
||||||
not TxHealthDetail(S.Id, 1, D));
|
|
||||||
|
|
||||||
// Чистая передача: сводка обязана прийти с честным «опасных 0» — именно она и
|
|
||||||
// снимает подозрение с нашего тракта.
|
|
||||||
TxHealthPtt(True, txhSrcCard);
|
|
||||||
Base := MonotonicUs;
|
|
||||||
TxHealthDrought(Base, Base + 3000, 9);
|
|
||||||
TxHealthPtt(False, 0);
|
|
||||||
Check('детектор: чистая передача помечена как чистая',
|
|
||||||
TxHealthTake(S) and (S.Dangerous = 0) and (S.Droughts = 1) and
|
|
||||||
(S.FirstDangerMs < 0), Format('опасных %d', [S.Dangerous]));
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TestEndToEnd;
|
procedure TestEndToEnd;
|
||||||
const
|
const
|
||||||
PORT = 40098;
|
PORT = 40098;
|
||||||
@@ -2734,7 +2657,6 @@ begin
|
|||||||
TestServer;
|
TestServer;
|
||||||
TestEndToEnd;
|
TestEndToEnd;
|
||||||
TestTxPacing;
|
TestTxPacing;
|
||||||
TestTxHealth;
|
|
||||||
WriteLn;
|
WriteLn;
|
||||||
WriteLn(Format('Итого: %d проверок, провалено %d', [Passed + Failed, Failed]));
|
WriteLn(Format('Итого: %d проверок, провалено %d', [Passed + Failed, Failed]));
|
||||||
if Failed > 0 then Halt(1);
|
if Failed > 0 then Halt(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user