diff --git a/venus/app-configs/hammerspoon/screen_sharing_metrics.lua b/venus/app-configs/hammerspoon/screen_sharing_metrics.lua index da0d208..c511743 100644 --- a/venus/app-configs/hammerspoon/screen_sharing_metrics.lua +++ b/venus/app-configs/hammerspoon/screen_sharing_metrics.lua @@ -1,5 +1,5 @@ --- Network probes for the connected Screen Sharing client and default router. --- These are ICMP round trips, not measurements of the video stream itself. +-- TCP RTT for the connected Screen Sharing client and ICMP probes for the router. +-- Neither measures the video stream's end-to-end latency. local interval = 2 local window = 60 local staleAfter = 8 @@ -80,7 +80,9 @@ local function clientStatus(time) return "active" end -local function metricText(label, target, status, time) +local function routerText(status, time) + local label = "Rtr." + local target = router if status == "inactive" then return "No " .. label end if status ~= "active" then return label .. " " .. status end local stats = summary(target, time) @@ -89,11 +91,18 @@ local function metricText(label, target, status, time) return string.format("%s %s/%.0f%%", label, latency, stats.loss) end +local function clientText(time) + local status = clientStatus(time) + if status == "inactive" then return "No SS" end + if status ~= "active" then return "SS " .. status end + local stats = summary(client, time) + return stats.current and string.format("SS %.0fms", stats.current) or "SS —ms" +end + local function render() local time = now() local routerStatus = router.address and "active" or "inactive" - menu:setTitle(metricText("SS", client, clientStatus(time), time) - .. " | " .. metricText("Rtr.", router, routerStatus, time)) + menu:setTitle(clientText(time) .. " | " .. routerText(routerStatus, time)) end local function parseClient(output) @@ -132,6 +141,49 @@ local function discoverClient() end end +-- Read TCP's RTT estimate from the existing Screen Sharing socket; no client probe is sent. +local function sampleClientRtt() + if not client.address or client.inFlight then return end + local address = client.address + client.inFlight = true + if not run("/usr/bin/nettop", { "-L", "1", "-m", "tcp", "-n", "-x", "-J", "rtt_avg" }, + function(code, output) + client.inFlight = false + if client.address ~= address then return end + if code ~= 0 then + hs.printf("Could not read Screen Sharing TCP RTT from nettop") + render() + return + end + for line in (output or ""):gmatch("[^\r\n]+") do + local socket, rttText = line:match("^([^,]+),([^,]*),") + if socket then + local separator = socket:find("<->", 1, true) + if separator then + local localEndpoint = socket:sub(1, separator - 1) + local remoteEndpoint = socket:sub(separator + 3) + local remoteAddress = remoteEndpoint:match("^(.*)[:%.]%d+$") + if localEndpoint:match("[:%.]5900$") and remoteAddress == address then + local rtt = tonumber(rttText:match("^%s*([%d.]+)")) + if rtt then + local time = now() + local sample = { at = time, rtt = rtt } + table.insert(client.samples, sample) + client.lastSample = time + client.lastReply = sample + prune(client, time) + break + end + end + end + end + end + render() + end) then + client.inFlight = false + end +end + local function refreshRouter() local ok, data = pcall(function() return store:contents("State:/Network/Global/IPv4") @@ -140,6 +192,21 @@ local function refreshRouter() setAddress(router, state and state.Router or nil) end +local function clientDetail(time) + local status = clientStatus(time) + if status ~= "active" then + return { title = "SS TCP RTT: " .. status, disabled = true } + end + local stats = summary(client, time) + return { + { title = "SS TCP RTT (avg): " .. client.address, disabled = true }, + { title = "Latest sample: " .. (stats.current and string.format("%.1f ms", stats.current) + or (stats.stale and "stale" or "unavailable")), disabled = true, indent = 1 }, + { title = "60s peak sample: " .. (stats.peak and string.format("%.1f ms", stats.peak) or "—"), + disabled = true, indent = 1 }, + } +end + local function probe(target) if not target.address or target.inFlight then return end local address = target.address @@ -194,7 +261,7 @@ menu:setMenu(function() if rows.title then rows = { rows } end for _, row in ipairs(rows) do table.insert(items, row) end end - append(detail("Screen Sharing client", client, clientStatus(time), time)) + append(clientDetail(time)) table.insert(items, { title = "-" }) append(detail("Router", router, router.address and "active" or "inactive", time)) table.insert(items, { title = "-" }) @@ -202,14 +269,14 @@ menu:setMenu(function() local signal = ok and wifi and wifi.rssi table.insert(items, { title = "Wi-Fi signal: " .. (signal and string.format("%d dBm", signal) or "unavailable"), disabled = true }) - table.insert(items, { title = "ICMP round trips; not video latency", disabled = true }) + table.insert(items, { title = "SS: TCP RTT; router: ICMP. Not video latency", disabled = true }) return items end) local function tick() refreshRouter() discoverClient() - if clientStatus(now()) == "active" then probe(client) end + if clientStatus(now()) == "active" then sampleClientRtt() end probe(router) render() end