diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs index 79dbd57..a0d3066 100644 --- a/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs +++ b/backend/src/MoShiro.Backend.Analysis/Metrics/MetricRegistry.cs @@ -12,7 +12,7 @@ public static class MetricRegistry [ LinkMetrics.Rtt, LinkMetrics.LossUp, LinkMetrics.LossDown, LinkMetrics.LossTotal, LinkMetrics.JitterUp, LinkMetrics.JitterDown, LinkMetrics.LossRun, LinkMetrics.LossRunUp, LinkMetrics.LossRunDown, LinkMetrics.Reorder, LinkMetrics.Dup, LinkMetrics.SamplesUdp, - TcpMetrics.Rtt, TcpMetrics.Slow, TcpMetrics.Stall, TcpMetrics.StallMax, TcpMetrics.Retrans, TcpMetrics.Srtt, TcpMetrics.BulkRate, TcpMetrics.BulkPeak, TcpMetrics.SamplesTcp, + TcpMetrics.Rtt, TcpMetrics.Slow, TcpMetrics.Stall, TcpMetrics.StallMax, TcpMetrics.Retrans, TcpMetrics.RetransUp, TcpMetrics.RetransDown, TcpMetrics.Srtt, TcpMetrics.BulkRate, TcpMetrics.BulkPeak, TcpMetrics.SamplesTcp, SessionMetrics.ConnectSuccess, SessionMetrics.ConnectTcp, SessionMetrics.ConnectTls, SessionMetrics.FailTimeout, SessionMetrics.FailReset, SessionMetrics.FailTls, SessionMetrics.HoldResets, SessionMetrics.HoldLifetime, @@ -24,7 +24,7 @@ public static class MetricRegistry [ new("quic", "QUIC Datagram", ["loss.up", "loss.down", "loss.total", "rtt.p50", "rtt.p95", "rtt.p99", "jitter.up.p95", "jitter.down.p95", "loss.run.up_max", "loss.run.down_max", "reorder", "dup"]), new("udp", "QUIC Datagram (legacy alias)", ["loss.up", "loss.down", "loss.total", "rtt.p50", "rtt.p95", "rtt.p99", "jitter.up.p95", "jitter.down.p95", "loss.run_max", "reorder", "dup"]), - new("tcp", "TCP", ["tcp.rtt.p50", "tcp.rtt.p95", "tcp.rtt.p99", "tcp.rtt.p999", "tcp.rtt.max", "tcp.rtt.tail", "tcp.slow", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans", "tcp.srtt.p95"]), + new("tcp", "TCP", ["tcp.rtt.p50", "tcp.rtt.p95", "tcp.rtt.p99", "tcp.rtt.p999", "tcp.rtt.max", "tcp.rtt.tail", "tcp.slow", "tcp.stall.per_min", "tcp.stall_max", "tcp.retrans", "tcp.retrans.up", "tcp.retrans.down", "tcp.srtt.p95"]), new("connect", "Connect", ["connect.success", "connect.tcp_ms.p95", "connect.tls_ms.p95", "connect.fail.timeout", "connect.fail.rst", "connect.fail.tls"]), new("session", "Session", ["hold.resets.per_h", "hold.lifetime_max"]), new("scores", "Scores", ["score.base", "score.mc", "score.hy2", "score.reality"]), diff --git a/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs b/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs index a4759f1..1c27c3c 100644 --- a/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs +++ b/backend/src/MoShiro.Backend.Analysis/Metrics/TcpMetrics.cs @@ -27,6 +27,14 @@ public static class TcpMetrics public static readonly MetricDef Retrans = Metric.Ratio( "tcp.retrans", s_tcpInfo, Ops.KernelDelta("retrans_total", "segs_out"), "TCP retransmissions", "Retransmitted segments as a share of segments sent (kernel counters).", good: 0.5, poor: 3); + public static readonly MetricDef RetransUp = Metric.Ratio( + "tcp.retrans.up", s_tcpInfo, Ops.KernelDelta("retrans_total", "segs_out", TcpSide.Forward), "TCP retransmissions (forward)", + "Retransmitted segments as a share of segments sent by the 拨号方 (kernel counters): loss on the 拨号方 to Agent path.", good: 0.5, poor: 3); + + public static readonly MetricDef RetransDown = Metric.Ratio( + "tcp.retrans.down", s_tcpInfo, Ops.KernelDelta("retrans_total", "segs_out", TcpSide.Return), "TCP retransmissions (return)", + "Retransmitted segments as a share of segments sent by the Agent (kernel counters): loss on the Agent to 拨号方 path.", good: 0.5, poor: 3); + public static readonly MetricDef Srtt = Metric.Histogram( "tcp.srtt", s_tcpInfo, Ops.Column("srtt_us", 0.001), "Kernel srtt", "Smoothed RTT sampled from the kernel.", Unit.Ms, [Stat.P95], good: 60, poor: 200); diff --git a/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs b/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs index 0755942..0713448 100644 --- a/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs +++ b/backend/src/MoShiro.Backend.Analysis/Ops/Ops.cs @@ -23,7 +23,7 @@ public static class Ops public static MetricOp RowCount { get; } = new RowCountOp(); - public static MetricOp KernelDelta(string field, string over) => new KernelDeltaOp(field, over); + public static MetricOp KernelDelta(string field, string over, TcpSide only = TcpSide.Both) => new KernelDeltaOp(field, over, only); public static MetricOp Column(string column, double scale) => new ColumnValueOp(column, scale); diff --git a/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs b/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs index 2479342..77c379b 100644 --- a/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs +++ b/backend/src/MoShiro.Backend.Analysis/Ops/SessionOps.cs @@ -9,19 +9,32 @@ internal static class TcpInfoTime side is not null && side.Values[row] == 0 ? view.DialerToUtcMs(ts.Values[row]) : view.AgentToUtcMs(ts.Values[row]); } -/// 两次采样之间累计计数的差值(分子)与另一个累计计数的差值(分母),按 (side, conn) 分别计算;旧数据没有 conn 时按 side 计算。 +/// 哪一侧的内核采样:拨号方 一侧发出的段走去程(拨号方到 Agent),Agent 一侧发出的段走回程。 +public enum TcpSide +{ + Both, + Forward, + Return, +} + +/// +/// 两次采样之间累计计数的差值(分子)与另一个累计计数的差值(分母),按 (side, conn) 分别计算;旧数据没有 conn 时按 side 计算。 +/// 只取一侧时,另一侧的行整个跳过(基线也不记),所以去程和回程的重传各自只由自己这一侧发出的段算出。 +/// internal sealed class KernelDeltaOp : MetricOp { private readonly string _field; private readonly string _over; + private readonly TcpSide _only; - public KernelDeltaOp(string field, string over) + public KernelDeltaOp(string field, string over, TcpSide only = TcpSide.Both) { _field = field; _over = over; + _only = only; } - public override string Signature => $"kernel_delta/{_field}/{_over}/2"; + public override string Signature => _only == TcpSide.Both ? $"kernel_delta/{_field}/{_over}/2" : $"kernel_delta/{_field}/{_over}/{_only}/2"; public override void Evaluate(ChunkView view, RawStream stream, IObservationSink sink) { @@ -43,7 +56,13 @@ internal sealed class KernelDeltaOp : MetricOp continue; } - var key = (side?.Values[i] ?? 0, connection?.IsValid(i) == true ? connection.Values[i] : 0); + long rowSide = side?.Values[i] ?? 0; + if ((_only == TcpSide.Forward && rowSide != 0) || (_only == TcpSide.Return && rowSide == 0)) + { + continue; + } + + var key = (rowSide, connection?.IsValid(i) == true ? connection.Values[i] : 0); if (last.TryGetValue(key, out (long Ts, long Field, long Over) previous)) { // Ignore delayed/duplicate samples without replacing the newer baseline. diff --git a/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs b/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs index 949c0fb..9da06ce 100644 --- a/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs +++ b/backend/tests/MoShiro.Backend.Analysis.Tests/OpTests.cs @@ -260,6 +260,23 @@ public sealed class OpTests Assert.Equal(300UL, retrans.Denominator); } + [Fact] + public void Retrans_ForwardAndReturnAreSplitBySide() + { + long ts = ChunkFactory.DialerUs(0); + var builder = new StreamBuilder(Kinds.TcpEcho.FindStream("tcp_info")!); + builder.Add(ts, 0, 40_000, 0, 10, 10, 1000, 0, 1); + builder.Add(ts, 1, 40_000, 0, 10, 100, 2000, 0, 1); + builder.Add(ts + 1_000_000, 0, 40_000, 0, 10, 11, 1100, 0, 1); + builder.Add(ts + 1_000_000, 1, 40_000, 0, 10, 102, 2200, 0, 1); + RawChunk chunk = ChunkFactory.Echo(s_agent, "tcp_echo", T0, 10, 100, + _ => new EchoPacket(false, false, 20_000, 20_000), extraStreams: [builder.Build()]); + var forward = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans.up").Items["tcp.retrans.up"]; + var back = (RatioAcc)EvaluateOneBucket(chunk, "tcp.retrans.down").Items["tcp.retrans.down"]; + Assert.Equal((1UL, 100UL), (forward.Numerator, forward.Denominator)); + Assert.Equal((2UL, 200UL), (back.Numerator, back.Denominator)); + } + [Fact] public void Connect_Outcomes_AreCountedByClass() { diff --git a/web/display/src/LineStrips.tsx b/web/display/src/LineStrips.tsx index c14aac2..5fecb08 100644 --- a/web/display/src/LineStrips.tsx +++ b/web/display/src/LineStrips.tsx @@ -29,11 +29,11 @@ interface Props { m: Record } -/** 一条线路的 QUIC/TCP 双行摘要,各自显示时延、质量点阵和丢包/重传率。 */ +/** 一条线路的 QUIC/TCP 摘要:RTT 是往返(⇄);去程是拨号方到监听方(→),回程反过来(←);QUIC 看各方向丢包,TCP 看各方向重传,都配质量点阵。 */ export function LineStrips({ lineId, range, m }: Props) { const info = OVERVIEW_RANGES.find((r) => r.value === range) ?? OVERVIEW_RANGES[0] const series = useQuery( - () => api.get(`/api/public/lines/${encodeURIComponent(lineId)}/series`, { range: info.value, anchor: 'now', step: info.step, preset: 'quic,tcp', metrics: 'loss.up,loss.down,loss.total,rtt.p50,rtt.p95,rtt.p99,tcp.retrans,tcp.rtt.p50' }), + () => api.get(`/api/public/lines/${encodeURIComponent(lineId)}/series`, { range: info.value, anchor: 'now', step: info.step, preset: 'quic,tcp', metrics: 'loss.up,loss.down,loss.total,rtt.p50,rtt.p95,rtt.p99,tcp.retrans.up,tcp.retrans.down,tcp.rtt.p50' }), [lineId, info.value], 30_000, ) @@ -57,24 +57,25 @@ export function LineStrips({ lineId, range, m }: Props) { const at = shown !== null && t[shown] !== undefined ? shown : null const lossUp = col('loss.up') const lossDown = col('loss.down') - const retrans = col('tcp.retrans') + const retransUp = col('tcp.retrans.up') + const retransDown = col('tcp.retrans.down') const up = m['loss.up'] const down = m['loss.down'] const upText = up === null || up === undefined ? '丢包 —' : `${fmtPct(up)} 丢包` const downText = down === null || down === undefined ? '丢包 —' : `${fmtPct(down)} 丢包` - const retransValue = m['tcp.retrans'] - const retransTone = levelOf(retransValue) - const retransText = retransValue === null || retransValue === undefined ? '重传 —' : `${fmtPct(retransValue)} 重传` + const retransUpValue = m['tcp.retrans.up'] + const retransDownValue = m['tcp.retrans.down'] + const retransText = (v: number | null | undefined) => (v === null || v === undefined ? '重传 —' : `${fmtPct(v)} 重传`) return (
setHover(null)}>
- QUIC{fmtMs(m['rtt.p50'])}悬停看 p95 / p99 + QUIC RTT ⇄{fmtMs(m['rtt.p50'])}悬停看 p95 / p99 {series.error ? 数据未更新 : null}
- 去程 + 去程 → {upText}
@@ -85,7 +86,7 @@ export function LineStrips({ lineId, range, m }: Props) {
- 回程 + 回程 ← {downText}
@@ -96,12 +97,28 @@ export function LineStrips({ lineId, range, m }: Props) {
- TCP{fmtMs(m['tcp.rtt.p50'])} - {retransText} + TCP RTT ⇄{fmtMs(m['tcp.rtt.p50'])}
-
- {(s ? retrans : Array.from({ length: common.cells }, () => null)).map((value, i) => ( - +
+
+
+ 去程 → + {retransText(retransUpValue)} +
+
+ {(s ? retransUp : Array.from({ length: common.cells }, () => null)).map((value, i) => ( + + ))} +
+
+
+
+ 回程 ← + {retransText(retransDownValue)} +
+
+ {(s ? retransDown : Array.from({ length: common.cells }, () => null)).map((value, i) => ( + ))}
@@ -127,7 +144,10 @@ export function LineStrips({ lineId, range, m }: Props) {
TCP - 重传 {fmtPct(col('tcp.retrans')[at])} + 去程重传 {fmtPct(col('tcp.retrans.up')[at])} + + + 回程重传 {fmtPct(col('tcp.retrans.down')[at])} RTT p50 {fmtMs(col('tcp.rtt.p50')[at])} diff --git a/web/display/src/pages/Compare.tsx b/web/display/src/pages/Compare.tsx index f0e66c5..1abaa6f 100644 --- a/web/display/src/pages/Compare.tsx +++ b/web/display/src/pages/Compare.tsx @@ -22,14 +22,16 @@ const METRICS: MetricInfo[] = [ { id: 'rtt.p50', label: '延迟 p50', unit: 'ms', better: 'lower' }, { id: 'rtt.p95', label: '延迟 p95', unit: 'ms', better: 'lower' }, { id: 'loss.total', label: 'QUIC 丢包', unit: '%', better: 'lower' }, - { id: 'loss.up', label: 'QUIC 上行丢包', unit: '%', better: 'lower' }, - { id: 'loss.down', label: 'QUIC 下行丢包', unit: '%', better: 'lower' }, - { id: 'loss.run.up_max', label: 'QUIC 上行最长连续丢包', unit: '', better: 'lower' }, - { id: 'loss.run.down_max', label: 'QUIC 下行最长连续丢包', unit: '', better: 'lower' }, - { id: 'jitter.up.p95', label: '上行抖动', unit: 'ms', better: 'lower' }, - { id: 'jitter.down.p95', label: '下行抖动', unit: 'ms', better: 'lower' }, + { id: 'loss.up', label: 'QUIC 去程丢包', unit: '%', better: 'lower' }, + { id: 'loss.down', label: 'QUIC 回程丢包', unit: '%', better: 'lower' }, + { id: 'loss.run.up_max', label: 'QUIC 去程最长连续丢包', unit: '', better: 'lower' }, + { id: 'loss.run.down_max', label: 'QUIC 回程最长连续丢包', unit: '', better: 'lower' }, + { id: 'jitter.up.p95', label: 'QUIC 去程抖动', unit: 'ms', better: 'lower' }, + { id: 'jitter.down.p95', label: 'QUIC 回程抖动', unit: 'ms', better: 'lower' }, { id: 'tcp.rtt.p50', label: 'TCP RTT p50', unit: 'ms', better: 'lower' }, { id: 'tcp.retrans', label: 'TCP 重传', unit: '%', better: 'lower' }, + { id: 'tcp.retrans.up', label: 'TCP 去程重传', unit: '%', better: 'lower' }, + { id: 'tcp.retrans.down', label: 'TCP 回程重传', unit: '%', better: 'lower' }, { id: 'score.reality', label: 'REALITY 评分', unit: '', better: 'higher' }, { id: 'score.hy2', label: 'Hy2 评分', unit: '', better: 'higher' }, { id: 'score.mc', label: 'MC 评分', unit: '', better: 'higher' }, diff --git a/web/display/src/pages/Line.tsx b/web/display/src/pages/Line.tsx index e5732e5..9b0ce0c 100644 --- a/web/display/src/pages/Line.tsx +++ b/web/display/src/pages/Line.tsx @@ -38,7 +38,8 @@ const TILES: { group: Exclude; label: string; key: string; { group: 'tcp-connect', label: 'TCP RTT p50', key: 'tcp.rtt.p50', fmt: fmtMs }, { group: 'tcp-connect', label: 'TCP RTT p99', key: 'tcp.rtt.p99', fmt: fmtMs }, { group: 'tcp-connect', label: 'TCP RTT 最大', key: 'tcp.rtt.max', fmt: fmtMs }, - { group: 'tcp-connect', label: 'TCP 重传率', key: 'tcp.retrans', fmt: fmtPct, good: 0.5, poor: 3 }, + { group: 'tcp-connect', label: 'TCP 去程重传率', key: 'tcp.retrans.up', fmt: fmtPct, good: 0.5, poor: 3 }, + { group: 'tcp-connect', label: 'TCP 回程重传率', key: 'tcp.retrans.down', fmt: fmtPct, good: 0.5, poor: 3 }, { group: 'tcp-connect', label: 'TCP 建连 p95', key: 'connect.tcp_ms.p95', fmt: fmtMs, optional: true }, { group: 'tcp-connect', label: 'TLS 握手 p95', key: 'connect.tls_ms.p95', fmt: fmtMs, optional: true }, { group: 'tcp-connect', label: '建连成功率', key: 'connect.success', fmt: fmtPct, optional: true }, @@ -64,11 +65,11 @@ const GROUPS = [ const CHARTS: ChartDef[] = [ { id: 'rtt', title: '延迟', group: 'quic', unit: 'ms', series: [{ key: 'rtt.p50', label: 'p50', color: '--chart-1' }, { key: 'rtt.p95', label: 'p95', color: '--chart-2' }, { key: 'rtt.p99', label: 'p99', color: '--chart-3' }] }, - { id: 'jitter', title: '抖动(p95)', group: 'quic', unit: 'ms', series: [{ key: 'jitter.up.p95', label: '上行', color: '--chart-2' }, { key: 'jitter.down.p95', label: '下行', color: '--chart-1' }] }, - { id: 'loss', title: '丢包', group: 'quic', unit: '%', series: [{ key: 'loss.up', label: '上行', color: '--chart-2' }, { key: 'loss.down', label: '下行', color: '--chart-3' }, { key: 'loss.total', label: '合计', color: '--chart-1', dash: [4, 3] }] }, - { id: 'loss-runs', title: '最长连续丢包', group: 'quic', unit: '包', series: [{ key: 'loss.run.up_max', label: '上行', color: '--chart-2' }, { key: 'loss.run.down_max', label: '下行', color: '--chart-3' }] }, + { id: 'jitter', title: '抖动(p95)', group: 'quic', unit: 'ms', series: [{ key: 'jitter.up.p95', label: '去程', color: '--chart-2' }, { key: 'jitter.down.p95', label: '回程', color: '--chart-1' }] }, + { id: 'loss', title: '丢包', group: 'quic', unit: '%', series: [{ key: 'loss.up', label: '去程', color: '--chart-2' }, { key: 'loss.down', label: '回程', color: '--chart-3' }, { key: 'loss.total', label: '合计', color: '--chart-1', dash: [4, 3] }] }, + { id: 'loss-runs', title: '最长连续丢包', group: 'quic', unit: '包', series: [{ key: 'loss.run.up_max', label: '去程', color: '--chart-2' }, { key: 'loss.run.down_max', label: '回程', color: '--chart-3' }] }, { id: 'tcp', title: '往返时延(RTT)', group: 'tcp-connect', unit: 'ms', series: [{ key: 'tcp.rtt.p50', label: 'p50', color: '--chart-1' }, { key: 'tcp.rtt.p95', label: 'p95', color: '--chart-2' }, { key: 'tcp.rtt.max', label: '最大', color: '--chart-3' }] }, - { id: 'retrans', title: '重传率', group: 'tcp-connect', unit: '%', fill: true, series: [{ key: 'tcp.retrans', label: '重传率', color: '--chart-3' }] }, + { id: 'retrans', title: '重传率', group: 'tcp-connect', unit: '%', series: [{ key: 'tcp.retrans.up', label: '去程', color: '--chart-2' }, { key: 'tcp.retrans.down', label: '回程', color: '--chart-3' }, { key: 'tcp.retrans', label: '合计', color: '--chart-1', dash: [4, 3] }] }, { id: 'connect-time', title: '建连耗时(p95)', group: 'tcp-connect', unit: 'ms', series: [{ key: 'connect.tcp_ms.p95', label: 'TCP 建连', color: '--chart-1' }, { key: 'connect.tls_ms.p95', label: 'TLS 握手', color: '--chart-2' }] }, { id: 'connect-ok', title: '建连成功率', group: 'tcp-connect', unit: '%', zero: false, series: [{ key: 'connect.success', label: '成功率', color: '--chart-3' }] }, { id: 'score', title: '综合评分', group: 'score', wide: true, unit: '', series: [{ key: 'score.reality', label: 'REALITY', color: '--chart-1' }, { key: 'score.hy2', label: 'Hy2', color: '--chart-2' }, { key: 'score.mc', label: 'MC', color: '--chart-4' }, { key: 'score.base', label: '综合', color: '--chart-5', dash: [4, 3] }] }, @@ -316,7 +317,7 @@ export function Line() {

QUIC Datagram 丢包

总丢包{fmtPct(m['loss.total'])}
-
↑ {fmtPct(m['loss.up'])}/↓ {fmtPct(m['loss.down'])}
+
去 {fmtPct(m['loss.up'])}/回 {fmtPct(m['loss.down'])}
最长连续 ↑ {fmtNum(m['loss.run.up_max'])} / ↓ {fmtNum(m['loss.run.down_max'])} 包
diff --git a/web/display/src/pages/Overview.tsx b/web/display/src/pages/Overview.tsx index 3299065..1f4463c 100644 --- a/web/display/src/pages/Overview.tsx +++ b/web/display/src/pages/Overview.tsx @@ -12,7 +12,7 @@ import { PageError } from '../lib' import { IPV6_STATES, IpVersionPicker, ipVersionOf } from '@shared/ipVersions' import { LineStrips, OVERVIEW_RANGES, type OverviewRange } from '../LineStrips' -const METRICS = ['rtt.p50', 'rtt.p95', 'rtt.p99', 'loss.total', 'loss.up', 'loss.down', 'tcp.rtt.p50', 'tcp.retrans'].join(',') +const METRICS = ['rtt.p50', 'rtt.p95', 'rtt.p99', 'loss.total', 'loss.up', 'loss.down', 'tcp.rtt.p50', 'tcp.retrans', 'tcp.retrans.up', 'tcp.retrans.down'].join(',') type SortKey = 'name' | 'rtt' | 'loss' @@ -136,7 +136,7 @@ function NodeCard({ node, labels, infos }: { node: HostItem; labels: string[]; i ) } -/** 一条线路(拨号方 → 监听方):近一段时间的 QUIC Datagram 上下行丢包、RTT 和 TCP 重传。 */ +/** 一条线路(拨号方 ⇄ 监听方):RTT 是往返;去程是拨号方到监听方(左到右),回程是监听方到拨号方(右到左)。QUIC 看两个方向的丢包,TCP 看两个方向的重传。 */ function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRange; infos: Map }) { const dialerInfo = infos.get(l.dialer_id) const agentInfo = infos.get(l.agent_id) @@ -159,7 +159,7 @@ function LineCard({ l, range, infos }: { l: OverviewLineDto; range: OverviewRang {dialerInfo?.country ? : null} {l.dialer_name} {dialerInfo?.isp_key ? : null} - → + ⇄ {agentInfo?.country ? : null} {l.name}