fix(uptime): точно разбирать составные длительности RTT
This commit is contained in:
1 parent
737e04a6a9
commit
ca2b634743
9 files changed
+184
-24
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import assert from "node:assert/strict"
|
||||
import { parsePing } from "./uptime-collector.js"
|
||||
import { parsePing, parsePingTimeMs } from "./uptime-collector.js"
|
||||
import fs from "node:fs"
|
||||
for (const rows of [[], [{}], [{status:"not enough permissions"}], [{sent:"0",received:"0","packet-loss":"0","avg-rtt":"0ms"}], [{sent:"4",received:"5","packet-loss":"0"}], [{sent:"4bad",received:"4","packet-loss":"0"}]]) {
|
||||
const result=parsePing(rows)
|
||||
assert.equal(result.measured,false);assert.equal(result.avgRtt,null)
|
||||
@@ -11,3 +12,26 @@ assert.equal(up.measured,true);assert.equal(up.status,"up");assert.equal(up.loss
|
||||
const partial=parsePing([{time:"500us"},{status:"timeout"}])
|
||||
assert.equal(partial.measured,true);assert.equal(partial.loss,50)
|
||||
console.log("ping measurement: empty/unsupported is not a measurement; zero and real 100 percent loss preserved")
|
||||
for (const [raw, expected] of [["53ms561us",53.561],["1s53ms561us",1053.561],["500us",0.5],["0ms",0],["53,561ms",53.561],["00:00:01.5",1500],[".5",0.5],["53",53]] as const) {
|
||||
assert(Math.abs(parsePingTimeMs(raw)! - expected) < 1e-9)
|
||||
}
|
||||
for (const raw of ["53bad", "53msbad", "-1", "Infinity", "1e3", "1ms2ms", "1us2ms", "00:60:00", "00:00:60", "1s2s", "", undefined]) assert.equal(parsePingTimeMs(raw),null)
|
||||
assert.equal(parsePing([{time:"53bad"}]).measured,false)
|
||||
assert.equal(parsePing([{time:"53ms900us"}]).avgRtt,54)
|
||||
assert.equal(parsePing([{time:"1s53ms561us"}]).avgRtt,1054)
|
||||
assert.equal(parsePing([{sent:"4",received:"4","packet-loss":"0","avg-rtt":"bad"}]).avgRtt,null)
|
||||
// Replay the recorded real CHR payload when the repository evidence is available.
|
||||
const nativePath = new URL("../../../docs/ux-ui/acceptance/ping-measurement-native-2026-10-12.json", import.meta.url)
|
||||
if(fs.existsSync(nativePath)) {
|
||||
const native = JSON.parse(fs.readFileSync(nativePath,"utf8"))
|
||||
for(const c of native.cases) {
|
||||
const parsed = parsePing(c.raw)
|
||||
assert.equal(parsed.measured,true); assert.equal(parsed.loss,0)
|
||||
assert(c.raw.every((r:{time:string}) => parsePingTimeMs(r.time)! > 53 && parsePingTimeMs(r.time)! < 54))
|
||||
assert.equal(parsed.avgRtt,53)
|
||||
}
|
||||
console.log(`ping RTT recorded native payload replay: ${native.cases.length} cases passed`)
|
||||
} else {
|
||||
console.log("ping RTT recorded native payload replay: skipped; evidence file not present")
|
||||
}
|
||||
console.log("ping RTT compound duration/strict token tests ok")
|
||||
@@ -23,26 +23,34 @@ export function getUptimeCollectorsState(): { resources: boolean; ping: boolean
|
||||
return { resources: collectingResources, ping: collectingPing }
|
||||
}
|
||||
|
||||
function parsePingTimeMs(raw: string | undefined): number | null {
|
||||
export function parsePingTimeMs(raw: string | undefined): number | null {
|
||||
if (!raw) return null
|
||||
const s = String(raw).trim().toLowerCase().replace(",", ".")
|
||||
const us = s.match(/^(\d+(?:\.\d+)?)\s*us$/)
|
||||
if (us) return Number.parseFloat(us[1]) / 1000
|
||||
const ms = s.match(/^(\d+(?:\.\d+)?)\s*ms$/)
|
||||
if (ms) return Number.parseFloat(ms[1])
|
||||
const sec = s.match(/^(\d+(?:\.\d+)?)\s*s$/)
|
||||
if (sec) return Number.parseFloat(sec[1]) * 1000
|
||||
|
||||
const clock = s.match(/^(\d+):(\d+):(\d+(?:\.\d+)?)$/)
|
||||
if (clock) {
|
||||
const h = Number.parseFloat(clock[1])
|
||||
const m = Number.parseFloat(clock[2])
|
||||
const sc = Number.parseFloat(clock[3])
|
||||
return (h * 3600 + m * 60 + sc) * 1000
|
||||
const total = (h * 3600 + m * 60 + sc) * 1000
|
||||
return m < 60 && sc < 60 && Number.isFinite(total) ? total : null
|
||||
}
|
||||
|
||||
const n = Number.parseFloat(s)
|
||||
return Number.isFinite(n) ? n : null
|
||||
// Bare numbers retain the existing millisecond unit, but never parse a prefix.
|
||||
if (/^(?:\d+(?:\.\d*)?|\.\d+)$/.test(s)) {
|
||||
const n = Number(s)
|
||||
return Number.isFinite(n) ? n : null
|
||||
}
|
||||
// Native CHR replies include combined values such as 53ms561us.
|
||||
const parts = [...s.matchAll(/(\d+(?:\.\d+)?)\s*(ms|us|s)/g)]
|
||||
if (!parts.length || parts.map(p => p[0]).join("").replace(/\s/g, "") !== s.replace(/\s/g, "")) return null
|
||||
const factors: Record<string, number> = { s: 1000, ms: 1, us: 0.001 }
|
||||
let total = 0, previousFactor = Infinity
|
||||
for (const part of parts) {
|
||||
const factor = factors[part[2]]
|
||||
if (factor >= previousFactor) return null
|
||||
total += Number(part[1]) * factor
|
||||
previousFactor = factor
|
||||
}
|
||||
return Number.isFinite(total) ? total : null
|
||||
}
|
||||
|
||||
export async function getSettings() {
|
||||
@@ -69,7 +77,7 @@ export function parsePing(results: Array<{ time?: string; status?: string; sent?
|
||||
const sum = [...results].reverse().find((r) =>
|
||||
r.sent != null || r.received != null || r["packet-loss"] != null || r["avg-rtt"] != null,
|
||||
)
|
||||
const okReplies = results.filter((r) => r.time && String(r.status ?? "").toLowerCase() !== "timeout")
|
||||
const okReplies = results.filter((r) => parsePingTimeMs(r.time) !== null && String(r.status ?? "").toLowerCase() !== "timeout")
|
||||
const timedOutReplies = results.filter((r) => String(r.status ?? "").toLowerCase() === "timeout")
|
||||
const rtts = okReplies
|
||||
.map((r) => parsePingTimeMs(r.time))
|
||||
|
||||
Reference in new issue
Block a user