dsh-best-ip 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -6
- package/bench.js +177 -57
- package/client.js +200 -88
- package/index.js +280 -62
- package/package.json +1 -1
package/client.js
CHANGED
|
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
autoApplyTitle: 'Apply the recommended address after successful testing. Keep the current address on failure.',
|
|
32
32
|
speedTest: 'Automatic speed test',
|
|
33
33
|
speedTestHint:
|
|
34
|
-
'Discover
|
|
34
|
+
'Discover addresses, reserve one slot for the previously selected IP, then fill the remaining slots by lowest ping (all by default) over 3 rounds by default. Rank by capped harmonic speed, lower-quartile speed, actual success rate and measurement confidence; skipped requests do not count as failures.',
|
|
35
35
|
maxBenchmarkIps: 'Maximum test IPs',
|
|
36
36
|
benchmarkIpLimitAll: 'All',
|
|
37
37
|
benchmarkIpLimitCustom: 'Custom',
|
|
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
benchmarkCostLimitSummary: '{count} IPs: up to about {compactTotalEn} tokens',
|
|
43
43
|
benchmarkCostDetails: 'Token budget details',
|
|
44
44
|
benchmarkCostHint: 'Budget for {count} usable IPs × {runs} runs/IP × up to {tokens} output tokens/run: about {total} output tokens, plus input tokens. Actual usage depends on usable IPs and output length.',
|
|
45
|
-
benchmarkCostAllHint: '“All” is estimated using the extra scan budget of {count} IPs. DNS answers are additional, so the actual IP count may be higher.',
|
|
45
|
+
benchmarkCostAllHint: '“All” is estimated using the extra scan budget of {count} IPs. DNS answers and a previously selected IP are additional to the scan budget, so the actual IP count may be higher.',
|
|
46
46
|
stepProbe: 'Discovering addresses',
|
|
47
47
|
stepBenchmark: 'Timing throughput',
|
|
48
48
|
stepConfirm: 'Repeating measurements',
|
|
@@ -83,22 +83,45 @@ window.__ModuleLoader__.load({
|
|
|
83
83
|
defaultChangedHint: 'The DSH default provider/model changed. Samples were saved, but no address was applied.',
|
|
84
84
|
recommended: 'Recommended',
|
|
85
85
|
systemDns: 'System DNS',
|
|
86
|
-
resultHint: '
|
|
87
|
-
score: '
|
|
88
|
-
scoreHint: '
|
|
86
|
+
resultHint: 'V2 scores rank first; provisional scores need at least 2 reliable samples for recommendation. ⚠ retains raw TPS: high values may be capped, not failed. Skipped nodes are eliminated; legacy records need a retest.',
|
|
87
|
+
score: 'Composite score',
|
|
88
|
+
scoreHint: '100 × (80% capped harmonic TPS + 20% capped Q25) / median node harmonic TPS × actual success rate² × measurement confidence. Confidence = reliable/successful requests × min(1, reliable runs/2). Unstarted, skipped and cancelled requests are not failures.',
|
|
89
|
+
provisional: 'Provisional',
|
|
90
|
+
confirmed: 'Confirmed',
|
|
91
|
+
eliminated: 'Eliminated / skipped',
|
|
92
|
+
legacyRecord: 'Legacy record; retest required',
|
|
93
|
+
incompleteRetest: 'Incomplete retest; this result is not confirmed.',
|
|
94
|
+
appliedIncompleteHint: 'Address applied manually before the full retest completed; this result is not confirmed.',
|
|
95
|
+
appliedLegacyHint: 'Address applied manually from a legacy record; retest with scoring v2 before relying on it.',
|
|
96
|
+
noRecommendationHint: 'Test complete. Not enough reliable evidence for a recommendation; current address unchanged.',
|
|
97
|
+
measured: 'tested',
|
|
98
|
+
skipped: 'skipped',
|
|
99
|
+
budget: 'planned',
|
|
100
|
+
round: 'Round',
|
|
101
|
+
leading: 'Provisional leader',
|
|
102
|
+
successfulSamples: 'successful',
|
|
103
|
+
failedSamples: 'failed',
|
|
104
|
+
measurementConfidence: 'Measurement confidence',
|
|
105
|
+
scoreUncalibrated: 'Provisional calibration',
|
|
106
|
+
scoreReference: 'Shared speed reference',
|
|
107
|
+
scoreCap: 'Shared speed cap',
|
|
108
|
+
scoreUncapped: 'not capped',
|
|
109
|
+
scoredTps: 'TPS used for scoring',
|
|
110
|
+
timeoutBudget: 'timeout budget',
|
|
89
111
|
stop: 'Stop test',
|
|
90
112
|
stopping: 'Stopping…',
|
|
91
113
|
stopped: 'Stopped',
|
|
92
114
|
stoppedHint: 'Stopped. Completed samples were saved; the current address was not changed.',
|
|
93
115
|
stoppingHint: 'Cancelling active requests and saving completed samples…',
|
|
94
116
|
sampleMissing: 'not tested',
|
|
117
|
+
sampleSkipped: 'skipped',
|
|
95
118
|
sampleStopped: 'stopped',
|
|
96
119
|
sampleFailed: 'failed',
|
|
97
120
|
sampleUnmeasurable: 'unmeasurable',
|
|
98
121
|
sampleLegacy: 'individual samples unavailable; retest',
|
|
99
122
|
samplesInsufficient: 'insufficient reliable samples',
|
|
100
123
|
reliableSamples: 'reliable',
|
|
101
|
-
sampleReasons: { 'transport-error': 'request failed', cancelled: 'cancelled', unmeasurable: 'invalid usage/timing', 'insufficient-tokens': 'fewer than 32 output tokens', 'missing-decode-window': 'missing decode duration', 'invalid-decode-window': 'invalid decode duration', 'insufficient-decode-window': 'decode window below 200 ms', 'high-outlier': '
|
|
124
|
+
sampleReasons: { 'transport-error': 'request failed', cancelled: 'cancelled', unmeasurable: 'invalid usage/timing', 'insufficient-tokens': 'fewer than 32 output tokens', 'missing-decode-window': 'missing decode duration', 'invalid-decode-window': 'invalid decode duration', 'insufficient-decode-window': 'decode window below 200 ms', 'high-outlier': 'legacy high-outlier flag; retest with scoring v2', 'high-capped': 'raw high TPS retained; only its scoring value is capped, not a request failure', 'low-tail': 'slow sample retained in scoring' },
|
|
102
125
|
forget: 'Clear and restore system DNS',
|
|
103
126
|
probedAgo: 'measured {age} ago',
|
|
104
127
|
never: 'never',
|
|
@@ -163,7 +186,7 @@ window.__ModuleLoader__.load({
|
|
|
163
186
|
autoApplyTitle: '测速成功后应用推荐地址;失败保留当前地址。',
|
|
164
187
|
speedTest: '自动测速',
|
|
165
188
|
speedTestHint:
|
|
166
|
-
'
|
|
189
|
+
'先发现地址,优先为上次已选 IP 保留一个测速名额,其余按 ping 从低到高选取(默认全部),按轮次测速(默认 3 轮);综合封顶调和速度、低四分位速度、实际成功率和测量可信度评分,跳过的请求不算失败。',
|
|
167
190
|
maxBenchmarkIps: '最大测速 IP 数',
|
|
168
191
|
benchmarkIpLimitAll: '全部',
|
|
169
192
|
benchmarkIpLimitCustom: '任意(自定义)',
|
|
@@ -174,7 +197,7 @@ window.__ModuleLoader__.load({
|
|
|
174
197
|
benchmarkCostLimitSummary: '{count} 个最多约{compactTotal} token',
|
|
175
198
|
benchmarkCostDetails: 'Token 预算详情',
|
|
176
199
|
benchmarkCostHint: '按 {count} 个有效 IP × 每 IP {runs} 次 × 每次最多 {tokens} 输出 token 估算:约 {total} 输出 token 预算,另计输入 token。实际消耗取决于有效 IP 数和输出长度。',
|
|
177
|
-
benchmarkCostAllHint: '“全部”以额外扫描上限 {count} 个 IP 为例;DNS
|
|
200
|
+
benchmarkCostAllHint: '“全部”以额外扫描上限 {count} 个 IP 为例;DNS 返回地址和上次已选 IP 不计入扫描上限,实际 IP 数可能更多。',
|
|
178
201
|
stepProbe: '正在发现可用地址',
|
|
179
202
|
stepBenchmark: '正在测速',
|
|
180
203
|
stepConfirm: '正在多轮复测',
|
|
@@ -215,22 +238,45 @@ window.__ModuleLoader__.load({
|
|
|
215
238
|
defaultChangedHint: 'DSH 默认供应商或模型已改变,已保留测试样本,但未应用地址。',
|
|
216
239
|
recommended: '推荐地址',
|
|
217
240
|
systemDns: '系统 DNS',
|
|
218
|
-
resultHint: '
|
|
219
|
-
score: '
|
|
220
|
-
scoreHint: '得分 =
|
|
241
|
+
resultHint: '优先按 v2 综合分排名;暂列分需至少 2 次可信样本才可推荐。⚠ 保留原始 TPS,高值仅封顶而非失败;跳过节点视为淘汰,旧记录需重测。',
|
|
242
|
+
score: '综合得分',
|
|
243
|
+
scoreHint: '得分 = 100 ×(80% 封顶调和 TPS + 20% 封顶 Q25)÷ 各节点调和 TPS 中位数 × 实际成功率² × 测量可信度。可信度 = 可信样本/成功请求 × min(1, 可信样本数/2)。未测、跳过、取消均不是失败。',
|
|
244
|
+
provisional: '暂列',
|
|
245
|
+
confirmed: '已确认',
|
|
246
|
+
eliminated: '淘汰 / 已跳过',
|
|
247
|
+
legacyRecord: '旧记录,需重测',
|
|
248
|
+
incompleteRetest: '未完整复测,此结果尚未确认。',
|
|
249
|
+
appliedIncompleteHint: '已手动应用尚未完整复测的地址;此结果尚未确认。',
|
|
250
|
+
appliedLegacyHint: '已手动应用旧记录地址,请使用 v2 评分重新测速后再作判断。',
|
|
251
|
+
noRecommendationHint: '测试完成,可信证据不足以推荐;当前地址未改变。',
|
|
252
|
+
measured: '已测',
|
|
253
|
+
skipped: '跳过',
|
|
254
|
+
budget: '计划',
|
|
255
|
+
round: '轮次',
|
|
256
|
+
leading: '可信暂列',
|
|
257
|
+
successfulSamples: '成功',
|
|
258
|
+
failedSamples: '失败',
|
|
259
|
+
measurementConfidence: '测量可信度',
|
|
260
|
+
scoreUncalibrated: '暂未完成校准',
|
|
261
|
+
scoreReference: '全场共同速度基准',
|
|
262
|
+
scoreCap: '全场共同速度上限',
|
|
263
|
+
scoreUncapped: '未封顶',
|
|
264
|
+
scoredTps: '评分使用 TPS',
|
|
265
|
+
timeoutBudget: '超时预算',
|
|
221
266
|
stop: '停止测试',
|
|
222
267
|
stopping: '正在停止…',
|
|
223
268
|
stopped: '已停止',
|
|
224
269
|
stoppedHint: '已停止,已完成的样本已保留;当前地址未改变。',
|
|
225
270
|
stoppingHint: '正在取消在途请求,并保存已完成的样本…',
|
|
226
271
|
sampleMissing: '未测',
|
|
272
|
+
sampleSkipped: '跳过',
|
|
227
273
|
sampleStopped: '停止',
|
|
228
274
|
sampleFailed: '失败',
|
|
229
275
|
sampleUnmeasurable: '不可测',
|
|
230
276
|
sampleLegacy: '逐次值未记录,需重新测速',
|
|
231
277
|
samplesInsufficient: '可靠样本不足',
|
|
232
278
|
reliableSamples: '可靠',
|
|
233
|
-
sampleReasons: { 'transport-error': '请求失败', cancelled: '已取消', unmeasurable: '用量或计时无效', 'insufficient-tokens': '输出不足 32 token', 'missing-decode-window': '缺少出词时长', 'invalid-decode-window': '出词时长无效', 'insufficient-decode-window': '出词计时不足 200 毫秒', 'high-outlier': '
|
|
279
|
+
sampleReasons: { 'transport-error': '请求失败', cancelled: '已取消', unmeasurable: '用量或计时无效', 'insufficient-tokens': '输出不足 32 token', 'missing-decode-window': '缺少出词时长', 'invalid-decode-window': '出词时长无效', 'insufficient-decode-window': '出词计时不足 200 毫秒', 'high-outlier': '旧版高值异常标记,请使用 v2 重测', 'high-capped': '原始高 TPS 保留,仅评分值封顶,不是请求失败', 'low-tail': '低速样本,仍计入评分' },
|
|
234
280
|
forget: '清除并恢复系统 DNS',
|
|
235
281
|
probedAgo: '{age}前测量',
|
|
236
282
|
never: '从未',
|
|
@@ -363,6 +409,9 @@ window.__ModuleLoader__.load({
|
|
|
363
409
|
chips: { display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' },
|
|
364
410
|
mono: { fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace', fontSize: 12 },
|
|
365
411
|
table: { width: '100%', minWidth: 740, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, fontSize: 12 },
|
|
412
|
+
runsTable: { width: '100%', minWidth: 0, tableLayout: 'fixed', borderCollapse: 'separate', borderSpacing: 0, fontSize: 12 },
|
|
413
|
+
runsHead: { display: 'block', fontSize: 10, fontWeight: 400, color: 'var(--dsw-alias-label-secondary)', lineHeight: 1.35 },
|
|
414
|
+
runsCell: { padding: '7px 4px', borderBottom: '1px solid var(--dsw-alias-border-l1)', verticalAlign: 'top', minWidth: 0, fontVariantNumeric: 'tabular-nums' },
|
|
366
415
|
resultsSurface: { width: '100%', minWidth: 0, containerType: 'inline-size', containerName: 'dsh-best-ip-results' },
|
|
367
416
|
resultViewport: { width: '100%', minWidth: 0, maxHeight: 'min(60vh, 560px)', overflow: 'auto', position: 'relative', isolation: 'isolate', border: '1px solid var(--dsw-alias-border-l1)', borderRadius: 9, boxSizing: 'border-box' },
|
|
368
417
|
resultIdentity: { position: 'sticky', left: 0, zIndex: 2, background: 'var(--dsw-alias-bg-layer-1)', borderRight: '1px solid var(--dsw-alias-border-l1)' },
|
|
@@ -510,6 +559,10 @@ window.__ModuleLoader__.load({
|
|
|
510
559
|
/** Human-readable, specific progress line from the server's live snapshot. */
|
|
511
560
|
function describeProgress(progress, t) {
|
|
512
561
|
if (progress === null || progress === undefined) return t('stepProbe')
|
|
562
|
+
const budget = `${progress.timing ?? 0} ${t('measured')} + ${progress.skipped ?? 0} ${t('skipped')} / ${progress.timingTotal ?? 0} ${t('budget')}`
|
|
563
|
+
const round = progress.round > 0 && progress.rounds > 0 ? ` · ${t('round')} ${progress.round}/${progress.rounds}` : ''
|
|
564
|
+
const leading = progress.leading ? ` · ${t('leading')}: ${progress.leading}` : ''
|
|
565
|
+
const timing = ` · ${budget}${round}`
|
|
513
566
|
switch (progress.stage) {
|
|
514
567
|
case 'discover':
|
|
515
568
|
return `${t('stepProbe')} · ${t('resolversDone')} ${progress.resolversDone ?? 0}/${progress.resolvers ?? 0}`
|
|
@@ -517,15 +570,15 @@ window.__ModuleLoader__.load({
|
|
|
517
570
|
case 'scan':
|
|
518
571
|
return `${t('stepProbe')} · ${progress.attempted ?? 0}/${progress.total ?? 0} ${t('probed')} · ${progress.usable ?? 0} ${t('usable')}`
|
|
519
572
|
case 'timing':
|
|
520
|
-
return `${t('stepBenchmark')}
|
|
573
|
+
return `${t('stepBenchmark')}${timing}${leading}`
|
|
521
574
|
case 'confirm':
|
|
522
|
-
return `${t('stepConfirm')} · ${(progress.rows ?? []).length} ${t('addresses')}
|
|
575
|
+
return `${t('stepConfirm')} · ${(progress.rows ?? []).length} ${t('addresses')}${timing}${leading}`
|
|
523
576
|
case 'stopping':
|
|
524
|
-
return t('stopping')
|
|
577
|
+
return `${t('stopping')}${progress.timingTotal !== undefined ? timing : ''}`
|
|
525
578
|
case 'stopped':
|
|
526
|
-
return t('stopped')
|
|
579
|
+
return `${t('stopped')}${progress.timingTotal !== undefined ? timing : ''}`
|
|
527
580
|
case 'done':
|
|
528
|
-
return t('stepDone')
|
|
581
|
+
return `${t('stepDone')}${progress.timingTotal !== undefined ? timing : ''}`
|
|
529
582
|
case 'failed':
|
|
530
583
|
return progress.message ?? t('stepFailed')
|
|
531
584
|
default:
|
|
@@ -541,46 +594,47 @@ window.__ModuleLoader__.load({
|
|
|
541
594
|
return [failure, outcome?.saveError].filter(Boolean).join('\n') || null
|
|
542
595
|
}
|
|
543
596
|
|
|
544
|
-
/**
|
|
545
|
-
* Addresses as rows, their runs as columns.
|
|
546
|
-
*
|
|
547
|
-
* A sweep produces one run per address, the confirm phase produces several
|
|
548
|
-
* per address; laying runs out horizontally keeps both readable and makes
|
|
549
|
-
* "same address, second attempt" obvious at a glance.
|
|
550
|
-
*
|
|
551
|
-
* Only TPS is shown here. The choice this table supports is which address
|
|
552
|
-
* streams fastest, and TTFT next to it crowded the row without changing any
|
|
553
|
-
* decision — the measured column is the one worth the width.
|
|
554
|
-
*/
|
|
597
|
+
/** Rows arrive in the server's round-end rank order; never reshuffle them mid-round. */
|
|
555
598
|
function RunsTable({ progress, t }) {
|
|
556
599
|
const rows = Array.isArray(progress?.rows) ? progress.rows : []
|
|
557
600
|
if (rows.length === 0) return null
|
|
558
|
-
const width = Math.max(1, ...rows.map((row) => (Array.isArray(row.runs) ? row.runs.length : 0)))
|
|
559
|
-
const columns =
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
rows.map((row) =>
|
|
569
|
-
h('div', { key: row.ip, style: { ...S.mono, display: 'flex', gap: 12, alignItems: 'baseline' } },
|
|
570
|
-
h('span', { style: { minWidth: 132 } }, `${row.ip}${row.colo ? ` ${row.colo}` : ''}`),
|
|
571
|
-
columns.map((index) => {
|
|
572
|
-
const run = Array.isArray(row.runs) ? row.runs[index] : undefined
|
|
573
|
-
return h(
|
|
574
|
-
'span',
|
|
575
|
-
{ key: index, style: { minWidth: 118, ...S.muted } },
|
|
576
|
-
run === undefined
|
|
577
|
-
? '…'
|
|
578
|
-
: run.error !== undefined && run.error !== null
|
|
579
|
-
? h('span', { style: { color: 'var(--dsw-alias-state-error-primary)' } }, `✗ ${String(run.error).slice(0, 40)}`)
|
|
580
|
-
: `${run.tps === null || run.tps === undefined ? '-' : run.tps} t/s`,
|
|
581
|
-
)
|
|
582
|
-
}),
|
|
601
|
+
const width = Math.max(1, progress.rounds ?? 0, ...rows.map((row) => Math.max(row.summary?.plannedRuns ?? 0, Array.isArray(row.runs) ? row.runs.length : 0)))
|
|
602
|
+
const columns = Array.from({ length: width }, (_, index) => index)
|
|
603
|
+
// Fixed columns squeezed the third round off the panel. Share the remaining
|
|
604
|
+
// width between rounds so the default three fit without sideways scrolling.
|
|
605
|
+
return h('div', { style: { width: '100%', minWidth: 0, overflowX: 'auto' } },
|
|
606
|
+
h('table', { style: S.runsTable },
|
|
607
|
+
h('colgroup', null,
|
|
608
|
+
h('col', { style: { width: '38%' } }),
|
|
609
|
+
h('col', { style: { width: '22%' } }),
|
|
610
|
+
columns.map((index) => h('col', { key: index, style: { width: `${40 / width}%` } })),
|
|
583
611
|
),
|
|
612
|
+
h('thead', null, h('tr', null,
|
|
613
|
+
h('th', { scope: 'col', style: { ...S.th, padding: '6px 4px' } }, t('ipAndEdge')),
|
|
614
|
+
h('th', { scope: 'col', style: { ...S.th, padding: '6px 4px' }, title: t('scoreHint') }, t('score')),
|
|
615
|
+
columns.map((index) => h('th', { key: index, scope: 'col', style: { ...S.th, padding: '6px 4px', textAlign: 'right' } },
|
|
616
|
+
h('span', { style: { fontWeight: 500 } }, `${t('run')}${index + 1}`),
|
|
617
|
+
)),
|
|
618
|
+
)),
|
|
619
|
+
h('tbody', null, rows.map((row, index) => {
|
|
620
|
+
// Old live snapshots can still display real attempts, but never gain a v2 score.
|
|
621
|
+
const summary = { ...row.summary, samples: Array.isArray(row.summary?.samples) ? row.summary.samples : row.runs ?? [], plannedRuns: Math.max(width, row.summary?.plannedRuns ?? 0) }
|
|
622
|
+
const values = sampleValues(summary, t).slice(1, -1).split(', ')
|
|
623
|
+
const details = sampleDetails(summary, t).split('\n')
|
|
624
|
+
return h('tr', { key: row.ip },
|
|
625
|
+
h('td', { style: { ...S.runsCell, ...S.mono, overflowWrap: 'anywhere' } },
|
|
626
|
+
h('div', null, `${row.rank ?? index + 1}. ${row.ip}`),
|
|
627
|
+
row.colo ? h('div', { style: { ...S.muted, fontSize: 10 } }, row.colo) : null,
|
|
628
|
+
),
|
|
629
|
+
h('td', { style: S.runsCell }, h(ResultScore, { bench: row.summary, t })),
|
|
630
|
+
columns.map((column) => h('td', {
|
|
631
|
+
key: column,
|
|
632
|
+
title: details[column],
|
|
633
|
+
style: { ...S.runsCell, ...S.mono, ...S.muted, padding: column === width - 1 ? '7px 0 7px 4px' : '7px 4px',
|
|
634
|
+
textAlign: 'right', whiteSpace: 'normal', overflowWrap: 'anywhere' },
|
|
635
|
+
}, values[column] ?? t('sampleMissing'))),
|
|
636
|
+
)
|
|
637
|
+
})),
|
|
584
638
|
),
|
|
585
639
|
)
|
|
586
640
|
}
|
|
@@ -622,23 +676,48 @@ window.__ModuleLoader__.load({
|
|
|
622
676
|
function orderedCandidates(candidates, benchmarks) {
|
|
623
677
|
const group = (entry) => {
|
|
624
678
|
const bench = benchmarks?.[entry.ip]
|
|
625
|
-
|
|
626
|
-
if (
|
|
627
|
-
|
|
628
|
-
|
|
679
|
+
const sampled = Array.isArray(bench?.samples) && bench.samples.length > 0
|
|
680
|
+
if (bench?.scoringVersion === 2 && !bench.historical) {
|
|
681
|
+
if (bench.eliminated) return 2
|
|
682
|
+
if (Number.isFinite(bench.stabilityScore) && bench.stabilityScore > 0) return 0
|
|
683
|
+
return sampled ? 1 : 4
|
|
684
|
+
}
|
|
685
|
+
return sampled || Number.isFinite(bench?.tps) ? 3 : 4
|
|
629
686
|
}
|
|
687
|
+
const value = (candidate, key, fallback = 0) => {
|
|
688
|
+
const bench = benchmarks?.[candidate.ip]
|
|
689
|
+
return bench?.scoringVersion === 2 && !bench.historical && Number.isFinite(bench[key]) ? bench[key] : fallback
|
|
690
|
+
}
|
|
691
|
+
const rtt = (candidate) => Number.isFinite(candidate.rtt) ? candidate.rtt : Infinity
|
|
692
|
+
// Keep this tuple identical to the server. Old scores, including another v2 cohort, are incomparable.
|
|
630
693
|
return [...(candidates ?? [])].sort((a, b) => group(a) - group(b) ||
|
|
631
|
-
(
|
|
632
|
-
(
|
|
694
|
+
value(b, 'stabilityScore') - value(a, 'stabilityScore') ||
|
|
695
|
+
value(b, 'reliableRuns') - value(a, 'reliableRuns') ||
|
|
696
|
+
value(b, 'effectiveTps') - value(a, 'effectiveTps') ||
|
|
697
|
+
value(b, 'conservativeTps') - value(a, 'conservativeTps') ||
|
|
698
|
+
value(a, 'ttftMs', Infinity) - value(b, 'ttftMs', Infinity) ||
|
|
699
|
+
rtt(a) - rtt(b) || String(a.ip).localeCompare(String(b.ip)))
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Skipped slots are display-only: samples continue to mean real attempts in their original order.
|
|
703
|
+
function sampleSlots(bench) {
|
|
704
|
+
if (!Array.isArray(bench?.samples)) return null
|
|
705
|
+
const skipped = Number.isSafeInteger(bench.skippedRuns) && bench.skippedRuns > 0 ? bench.skippedRuns : 0
|
|
706
|
+
const pending = Number.isSafeInteger(bench.pendingRuns) && bench.pendingRuns > 0 ? bench.pendingRuns : 0
|
|
707
|
+
const planned = Number.isSafeInteger(bench.plannedRuns) && bench.plannedRuns > 0 ? bench.plannedRuns : 0
|
|
708
|
+
const count = Math.max(bench.samples.length + skipped + pending, planned)
|
|
709
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
710
|
+
sample: bench.samples[index],
|
|
711
|
+
missing: index >= bench.samples.length && index < bench.samples.length + skipped ? 'sampleSkipped' : 'sampleMissing',
|
|
712
|
+
}))
|
|
633
713
|
}
|
|
634
714
|
|
|
635
715
|
function sampleValues(bench, t) {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
const values =
|
|
639
|
-
|
|
640
|
-
if (
|
|
641
|
-
if (sample.cancelled) return t('sampleStopped')
|
|
716
|
+
const slots = sampleSlots(bench)
|
|
717
|
+
if (slots === null) return t('sampleLegacy')
|
|
718
|
+
const values = slots.map(({ sample, missing }) => {
|
|
719
|
+
if (!sample) return t(missing)
|
|
720
|
+
if (sample.cancelled || sample.error === 'test stopped') return t('sampleStopped')
|
|
642
721
|
if (Number.isFinite(sample.tps)) return `${sample.tps.toFixed(1)}${sample.flags?.length ? '⚠' : ''}`
|
|
643
722
|
return sample.error ? t('sampleFailed') : t('sampleUnmeasurable')
|
|
644
723
|
})
|
|
@@ -646,15 +725,22 @@ window.__ModuleLoader__.load({
|
|
|
646
725
|
}
|
|
647
726
|
|
|
648
727
|
function sampleDetails(bench, t) {
|
|
649
|
-
|
|
650
|
-
|
|
728
|
+
const slots = sampleSlots(bench)
|
|
729
|
+
if (slots === null) return t('sampleLegacy')
|
|
730
|
+
return slots.map(({ sample, missing }, index) => {
|
|
731
|
+
const label = `${t('run')}${index + 1}: `
|
|
732
|
+
if (!sample) return `${label}${t(missing)}${missing === 'sampleSkipped' && bench.skippedReason ? `; ${bench.skippedReason}` : ''}`
|
|
651
733
|
const flags = (sample.flags ?? []).map((flag) => t(`sampleReasons.${flag}`)).join('; ')
|
|
652
|
-
|
|
734
|
+
const state = sample.cancelled || sample.error === 'test stopped' ? `${t('sampleStopped')}; ` : sample.error ? `${t('sampleFailed')}; ` : ''
|
|
735
|
+
const scored = Number.isFinite(sample.scoredTps) ? `; ${t('scoredTps')}: ${sample.scoredTps}` : ''
|
|
736
|
+
const timeout = Number.isFinite(sample.timeoutMs) && sample.timeoutMs > 0 ? `; ${t('timeoutBudget')}: ${sample.timeoutMs} ms` : ''
|
|
737
|
+
return `${label}${state}${sample.tps ?? '-'} TPS; ${sample.outputTokens ?? '-'} tokens; ${sample.decodeMs ?? '-'} ms${scored}${timeout}${flags ? `; ${flags}` : ''}`
|
|
653
738
|
}).join('\n')
|
|
654
739
|
}
|
|
655
740
|
|
|
656
741
|
function SampleBadges({ bench, t }) {
|
|
657
742
|
if (!Array.isArray(bench?.samples)) return h('div', { style: { ...S.muted, whiteSpace: 'normal' } }, t('sampleLegacy'))
|
|
743
|
+
if (sampleSlots(bench).length === 0) return null
|
|
658
744
|
const labels = sampleValues(bench, t).slice(1, -1).split(', ')
|
|
659
745
|
const details = sampleDetails(bench, t).split('\n')
|
|
660
746
|
return h('div', { style: S.sampleBadges, 'aria-label': t('samplesLabel') },
|
|
@@ -676,12 +762,14 @@ window.__ModuleLoader__.load({
|
|
|
676
762
|
)
|
|
677
763
|
}
|
|
678
764
|
|
|
679
|
-
function ResultApply({ ip, selected, disabled, onChoose, label, t }) {
|
|
765
|
+
function ResultApply({ ip, selected, disabled, onChoose, label, bench, t }) {
|
|
680
766
|
if (selected) return h('span', { style: { ...S.muted, display: 'block', textAlign: 'center', padding: '7px 0' } }, `✓ ${t('inUse')}`)
|
|
767
|
+
const warning = bench?.scoringVersion === 2 && !bench.historical ? bench.confirmed !== true ? t('incompleteRetest') : ''
|
|
768
|
+
: bench && ((bench.samples?.length ?? 0) > 0 || Number.isFinite(bench.tps) || Number.isFinite(bench.stabilityScore)) ? t('legacyRecord') : ''
|
|
681
769
|
return h('button', {
|
|
682
770
|
type: 'button', className: 'dsh-best-ip-result-apply',
|
|
683
771
|
style: { ...S.btn, ...S.resultApply }, disabled,
|
|
684
|
-
title: `${label ?? t('use')}: ${ip}`, 'aria-label': `${label ?? t('use')}: ${ip}`,
|
|
772
|
+
title: `${label ?? t('use')}: ${ip}${warning ? ` · ${warning}` : ''}`, 'aria-label': `${label ?? t('use')}: ${ip}`,
|
|
685
773
|
onClick: () => onChoose(ip),
|
|
686
774
|
}, label ?? t('use'))
|
|
687
775
|
}
|
|
@@ -695,19 +783,39 @@ window.__ModuleLoader__.load({
|
|
|
695
783
|
}
|
|
696
784
|
|
|
697
785
|
function ResultScore({ bench, t }) {
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
786
|
+
const samples = Array.isArray(bench?.samples) ? bench.samples : []
|
|
787
|
+
const measured = samples.length > 0 || (bench?.runs ?? 0) > 0 || Number.isFinite(bench?.tps) || Number.isFinite(bench?.stabilityScore)
|
|
788
|
+
const v2 = bench?.scoringVersion === 2 && !bench.historical
|
|
789
|
+
const eliminated = v2 && (bench.eliminated === true || bench.skippedRuns > 0)
|
|
790
|
+
const status = !v2 ? measured ? 'legacyRecord' : 'untested'
|
|
791
|
+
: eliminated ? 'eliminated' : bench.confirmed === true && bench.rankEligible === true ? 'confirmed' : measured ? 'provisional' : 'untested'
|
|
792
|
+
const successful = bench?.successfulRuns ?? (samples.length > 0
|
|
793
|
+
? samples.filter((sample) => sample && !sample.cancelled && !sample.error).length
|
|
794
|
+
: Math.max(0, (bench?.runs ?? 0) - (bench?.failed ?? 0) - (bench?.cancelled ?? 0)))
|
|
795
|
+
const failed = bench?.failed ?? samples.filter((sample) => sample && !sample.cancelled && sample.error && sample.error !== 'test stopped').length
|
|
796
|
+
const confidence = Number.isFinite(bench?.measurementConfidence) ? `\n${t('measurementConfidence')}: ${(bench.measurementConfidence * 100).toFixed(0)}%` : ''
|
|
797
|
+
const context = v2 ? bench.scoreContext : undefined
|
|
798
|
+
const shared = context ? `\n${t('scoreReference')}: ${context.speedReference} TPS\n${t('scoreCap')}: ${Number.isFinite(context.speedCap) ? `${context.speedCap} TPS` : t('scoreUncapped')}` : ''
|
|
799
|
+
const calibration = context?.calibrated === false ? `\n${t('scoreUncalibrated')}` : ''
|
|
800
|
+
const counts = `${successful} ${t('successfulSamples')} · ${failed} ${t('failedSamples')} · ${bench?.reliableRuns ?? 0} ${t('reliableSamples')}`
|
|
801
|
+
return h('div', { style: S.resultMetric, title: `${v2 ? t('scoreHint') : t('legacyRecord')}${shared}${confidence}${calibration}` },
|
|
802
|
+
// A single trustworthy run has a useful provisional score even though it cannot be recommended yet.
|
|
803
|
+
h('div', { style: { fontWeight: 600, fontVariantNumeric: 'tabular-nums', whiteSpace: 'nowrap' } }, Number.isFinite(bench?.stabilityScore) ? bench.stabilityScore.toFixed(1) : '—'),
|
|
804
|
+
h('div', { style: { ...S.muted, fontSize: 11 } }, t(status)),
|
|
805
|
+
measured || eliminated ? h('div', { style: { ...S.muted, fontSize: 11 } }, counts) : null,
|
|
806
|
+
v2 && measured && (bench.reliableRuns ?? 0) < 2 ? h('div', { style: { ...S.muted, fontSize: 11 } }, t('samplesInsufficient')) : null,
|
|
702
807
|
)
|
|
703
808
|
}
|
|
704
809
|
|
|
705
810
|
function ResultTiming({ row, bench, t }) {
|
|
706
|
-
const
|
|
707
|
-
const
|
|
811
|
+
const samples = Array.isArray(bench?.samples) ? bench.samples : []
|
|
812
|
+
const attempted = samples.length || bench?.runs || 0
|
|
813
|
+
const cancelled = (bench?.cancelled ?? 0) > 0 || samples.some((sample) => sample?.cancelled || sample?.error === 'test stopped')
|
|
814
|
+
const failed = attempted > 0 && ((bench?.failed ?? 0) > 0 || samples.some((sample) => sample?.error && !sample.cancelled && sample.error !== 'test stopped') ||
|
|
815
|
+
(!cancelled && (bench?.error !== undefined || bench?.eligible === false)))
|
|
708
816
|
return h('div', { style: S.resultMetric },
|
|
709
817
|
h('div', { style: S.muted }, `${t('ping')}: ${Number.isFinite(row.rtt) ? `${row.rtt} ms` : '—'}`),
|
|
710
|
-
h('div', { style: S.muted }, `${t('ttft')}: ${Number.isFinite(bench?.ttftMs) ? `${bench.ttftMs} ms` : failed ? t('unusable') : t('untested')}`),
|
|
818
|
+
h('div', { style: S.muted }, `${t('ttft')}: ${Number.isFinite(bench?.ttftMs) ? `${bench.ttftMs} ms` : failed ? t('unusable') : cancelled ? t('sampleStopped') : t('untested')}`),
|
|
711
819
|
)
|
|
712
820
|
}
|
|
713
821
|
|
|
@@ -866,7 +974,7 @@ window.__ModuleLoader__.load({
|
|
|
866
974
|
if (runError) setError(runError)
|
|
867
975
|
if (final?.stage === 'stopped') setNote(t('stoppedHint'))
|
|
868
976
|
if (final?.stage === 'done') {
|
|
869
|
-
setNote(final.defaultChanged ? t('defaultChangedHint') : final.applied ? t('applies') : t('previewDone').replace('{ip}', final.recommended
|
|
977
|
+
setNote(final.defaultChanged ? t('defaultChangedHint') : final.applied ? t('applies') : final.recommended ? t('previewDone').replace('{ip}', final.recommended) : t('noRecommendationHint'))
|
|
870
978
|
}
|
|
871
979
|
} catch (problem) {
|
|
872
980
|
setError(problem?.message ?? String(problem))
|
|
@@ -956,9 +1064,11 @@ window.__ModuleLoader__.load({
|
|
|
956
1064
|
setError(null)
|
|
957
1065
|
try {
|
|
958
1066
|
const result = await call('/select', { host: measurement.hostname, provider: selection.provider, ip, testOnly: false })
|
|
959
|
-
if (result.result?.saveError) setError(result.result.saveError)
|
|
960
1067
|
await load()
|
|
961
|
-
|
|
1068
|
+
if (result.result?.saveError) setError(result.result.saveError)
|
|
1069
|
+
const bench = measurement.benchmarks?.[ip]
|
|
1070
|
+
setNote(bench?.scoringVersion === 2 && !bench.historical ? bench.confirmed !== true ? t('appliedIncompleteHint') : null
|
|
1071
|
+
: bench && ((bench.samples?.length ?? 0) > 0 || Number.isFinite(bench.tps) || Number.isFinite(bench.stabilityScore)) ? t('appliedLegacyHint') : null)
|
|
962
1072
|
} catch (problem) {
|
|
963
1073
|
setError(problem?.message ?? String(problem))
|
|
964
1074
|
}
|
|
@@ -985,7 +1095,9 @@ window.__ModuleLoader__.load({
|
|
|
985
1095
|
const benchmarkCost = benchmarkIpLimitInvalid ? null : describeBenchmarkCost(state, draft.maxBenchmarkIps, t)
|
|
986
1096
|
const benchmarked = measurement?.benchmarks ?? {}
|
|
987
1097
|
const sortedRows = orderedCandidates(measurement?.candidates, benchmarked)
|
|
988
|
-
const
|
|
1098
|
+
const recommendedIp = sortedRows.some((row) => row.ip === measurement?.recommended && benchmarked[row.ip]?.scoringVersion === 2 && !benchmarked[row.ip]?.historical &&
|
|
1099
|
+
benchmarked[row.ip]?.rankEligible === true && !benchmarked[row.ip]?.eliminated && Number.isFinite(benchmarked[row.ip]?.stabilityScore) && benchmarked[row.ip].stabilityScore > 0)
|
|
1100
|
+
? measurement.recommended : undefined
|
|
989
1101
|
|
|
990
1102
|
const statItems = [
|
|
991
1103
|
['statPinned', state.pinned],
|
|
@@ -1187,9 +1299,9 @@ window.__ModuleLoader__.load({
|
|
|
1187
1299
|
h('div', { style: { ...S.chips, minWidth: 0 } },
|
|
1188
1300
|
h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, measurement.hostname),
|
|
1189
1301
|
h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, `${t('inUse')}: ${measurement.activeAddress ?? t('systemDns')}`),
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
? h('div', { style: { width: 148, maxWidth: '100%' } }, h(ResultApply, { ip:
|
|
1302
|
+
recommendedIp ? h('span', { style: { ...S.chip, ...S.mono, whiteSpace: 'normal', overflowWrap: 'anywhere' } }, `${t('recommended')}: ${recommendedIp}`) : null,
|
|
1303
|
+
recommendedIp && recommendedIp !== measurement.activeAddress
|
|
1304
|
+
? h('div', { style: { width: 148, maxWidth: '100%' } }, h(ResultApply, { ip: recommendedIp, bench: benchmarked[recommendedIp], disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, label: t('applyRecommended'), t }))
|
|
1193
1305
|
: null,
|
|
1194
1306
|
),
|
|
1195
1307
|
// Persisted empty results need a message even after reopening the page.
|
|
@@ -1211,13 +1323,13 @@ window.__ModuleLoader__.load({
|
|
|
1211
1323
|
h('tbody', null, sortedRows.map((row) => {
|
|
1212
1324
|
const bench = benchmarked[row.ip]
|
|
1213
1325
|
const selected = measurement.activeAddress === row.ip
|
|
1214
|
-
const recommended =
|
|
1326
|
+
const recommended = row.ip === recommendedIp
|
|
1215
1327
|
return h('tr', { key: row.ip },
|
|
1216
1328
|
h('td', { style: { ...S.td, ...S.resultIdentity } }, h(ResultIdentity, { row, selected, recommended, t })),
|
|
1217
1329
|
h('td', { style: S.td }, h(ResultSpeed, { bench, t })),
|
|
1218
1330
|
h('td', { style: { ...S.td, ...(recommended ? S.best : {}) } }, h(ResultScore, { bench, t })),
|
|
1219
1331
|
h('td', { style: S.td }, h(ResultTiming, { row, bench, t })),
|
|
1220
|
-
h('td', { style: { ...S.td, ...S.resultAction } }, h(ResultApply, { ip: row.ip, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
|
|
1332
|
+
h('td', { style: { ...S.td, ...S.resultAction } }, h(ResultApply, { ip: row.ip, bench, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
|
|
1221
1333
|
)
|
|
1222
1334
|
})),
|
|
1223
1335
|
),
|
|
@@ -1225,11 +1337,11 @@ window.__ModuleLoader__.load({
|
|
|
1225
1337
|
h('div', { className: 'dsh-best-ip-results-cards', style: S.resultCards }, sortedRows.map((row) => {
|
|
1226
1338
|
const bench = benchmarked[row.ip]
|
|
1227
1339
|
const selected = measurement.activeAddress === row.ip
|
|
1228
|
-
const recommended =
|
|
1340
|
+
const recommended = row.ip === recommendedIp
|
|
1229
1341
|
return h('article', { key: row.ip, style: S.resultCard, 'aria-label': row.ip },
|
|
1230
1342
|
h('div', { style: S.resultCardHeader },
|
|
1231
1343
|
h('div', { style: { flex: '1 1 auto', minWidth: 0 } }, h(ResultIdentity, { row, selected, recommended, t })),
|
|
1232
|
-
h('div', { style: { flex: '0 0 112px', maxWidth: '48%' } }, h(ResultApply, { ip: row.ip, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
|
|
1344
|
+
h('div', { style: { flex: '0 0 112px', maxWidth: '48%' } }, h(ResultApply, { ip: row.ip, bench, selected, disabled: busy || timing || running !== null || benchmarkBlocked, onChoose: choose, t })),
|
|
1233
1345
|
),
|
|
1234
1346
|
h('div', { style: S.resultCardMetrics },
|
|
1235
1347
|
h('div', { style: S.resultMetric }, h('div', { style: S.hint }, t('medianTps')), h(ResultSpeed, { bench, t })),
|