diff --git a/GLOSSARY.md b/GLOSSARY.md index 08c01f03..340f0f98 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -90,10 +90,10 @@ _避免用词_:可移除节点(Removable node)。 服务器连通性测试请求访问的目标地址,是用户可修改的非敏感偏好信息,在展示、日志与诊断中无需脱敏。 **代理响应延迟(Proxy response latency)**: -完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。 +完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。结果保留测试时间,不按固定时长或网络变化自动判为过期,用户结合测试时间自行判断参考价值。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。 **配置组最快响应延迟(Fastest group response latency)**: -配置组全部后代服务器的成功测试结果中,符合展示条件的最小代理响应延迟。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。 +配置组全部后代服务器最近一次测试结果中的成功项,其最小代理响应延迟。最近一次结果失败的服务器不贡献更早的成功结果;汇总不按测试时间排除结果。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。 **托管插件(Managed plugin)**: 由产品随应用发布的 SIP003 插件,其发布版本与来源事实属于随应用提供的程序,不代表用户自备的同名程序。 diff --git a/ShadowsocksX-NG2/App/Application/Connectivity/ServerConnectivity.swift b/ShadowsocksX-NG2/App/Application/Connectivity/ServerConnectivity.swift index c571eacf..cdfc9c87 100644 --- a/ShadowsocksX-NG2/App/Application/Connectivity/ServerConnectivity.swift +++ b/ShadowsocksX-NG2/App/Application/Connectivity/ServerConnectivity.swift @@ -14,10 +14,6 @@ final class ServerConnectivity: ObservableObject { let id = UUID() let finishedAt: Date let outcome: Outcome - var networkExpired = false - func isExpired(at date: Date) -> Bool { - networkExpired || date.timeIntervalSince(finishedAt) >= 300 - } } struct Batch { let targets: [NodeID] @@ -116,15 +112,13 @@ final class ServerConnectivity: ObservableObject { } } - func summary(for ids: [NodeID], at date: Date) -> Summary { + func summary(for ids: [NodeID]) -> Summary { let values = ids.compactMap { id in records[id]?.first.map { (id, $0) } } let successes = values.filter { if case .success = $0.1.outcome { return true } return false } - let fresh = successes.filter { !$0.1.isExpired(at: date) } - let candidates = fresh.isEmpty ? successes : fresh - let fastest = candidates.min { lhs, rhs in + let fastest = successes.min { lhs, rhs in guard case .success(let leftMilliseconds) = lhs.1.outcome, case .success(let rightMilliseconds) = rhs.1.outcome else { @@ -152,13 +146,6 @@ final class ServerConnectivity: ObservableObject { func networkChanged() { generation += 1 - for id in records.keys { - records[id] = records[id]?.map { record in - var copy = record - copy.networkExpired = true - return copy - } - } for id in testing { jobs[id]?.cancel() } } diff --git a/ShadowsocksX-NG2/App/Resources/Connectivity.xcstrings b/ShadowsocksX-NG2/App/Resources/Connectivity.xcstrings index a1c962ec..1d236899 100644 --- a/ShadowsocksX-NG2/App/Resources/Connectivity.xcstrings +++ b/ShadowsocksX-NG2/App/Resources/Connectivity.xcstrings @@ -52,23 +52,6 @@ } } }, - "已过期": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Expired" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "已过期" - } - } - } - }, "无成功结果": { "extractionState": "manual", "localizations": { diff --git a/ShadowsocksX-NG2/App/UI/Servers/CatalogNodeDetailView.swift b/ShadowsocksX-NG2/App/UI/Servers/CatalogNodeDetailView.swift index 21cb1632..2eb3fe06 100644 --- a/ShadowsocksX-NG2/App/UI/Servers/CatalogNodeDetailView.swift +++ b/ShadowsocksX-NG2/App/UI/Servers/CatalogNodeDetailView.swift @@ -159,12 +159,10 @@ private struct ObservedConnectivityProperties: View { var body: some View { if node.isGroup { - TimelineView(.periodic(from: .now, by: 1)) { context in - content( - NodeDetailPresentation.connectivitySummary( - module.summary(for: node.connectivityServerIDs, at: context.date), - serverName: serverName, locale: locale, timeZone: timeZone)) - } + content( + NodeDetailPresentation.connectivitySummary( + module.summary(for: node.connectivityServerIDs), + serverName: serverName, locale: locale, timeZone: timeZone)) } else { content( NodeDetailPresentation.connectivityHistory( diff --git a/ShadowsocksX-NG2/App/UI/Servers/ConnectivityViews.swift b/ShadowsocksX-NG2/App/UI/Servers/ConnectivityViews.swift index 86037baf..560e643d 100644 --- a/ShadowsocksX-NG2/App/UI/Servers/ConnectivityViews.swift +++ b/ShadowsocksX-NG2/App/UI/Servers/ConnectivityViews.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Observes only the network observations, with a periodic expiry projection. +/// Observes the latest connectivity measurements and testing progress. struct ConnectivityCell: View { @ObservedObject var module: ServerConnectivity let node: CatalogTreeNode @@ -8,43 +8,38 @@ struct ConnectivityCell: View { var selected = false var body: some View { - TimelineView(.periodic(from: .now, by: 1)) { context in - let value = presentation(at: context.date) - HStack(spacing: 5) { - Text(value.text) - .foregroundStyle( - selected - ? AnyShapeStyle( - Color(nsColor: .alternateSelectedControlTextColor) - .opacity(value.expired ? 0.85 : 1)) - : AnyShapeStyle(value.expired ? .secondary : .primary)) - if value.testing { - ProgressView().controlSize(.mini) - .tint(selected ? Color(nsColor: .alternateSelectedControlTextColor) : nil) - } + let value = presentation() + HStack(spacing: 5) { + Text(value.text) + .foregroundStyle( + selected + ? AnyShapeStyle(Color(nsColor: .alternateSelectedControlTextColor)) + : AnyShapeStyle(.primary)) + if value.testing { + ProgressView().controlSize(.mini) + .tint(selected ? Color(nsColor: .alternateSelectedControlTextColor) : nil) } - .help(value.help) - .accessibilityElement(children: .ignore) - .accessibilityLabel(Text(ConnectivityCopy.text("响应延迟"))) - .accessibilityValue( - Text( - value.text + " " + value.help + (value.testing ? " " + ConnectivityCopy.text("测试中") : "")) - ) } + .help(value.help) + .accessibilityElement(children: .ignore) + .accessibilityLabel(Text(ConnectivityCopy.text("响应延迟"))) + .accessibilityValue( + Text( + value.text + " " + value.help + (value.testing ? " " + ConnectivityCopy.text("测试中") : "")) + ) } private struct Presentation { let text: String let help: String - let expired: Bool let testing: Bool } - private func presentation(at date: Date) -> Presentation { + private func presentation() -> Presentation { let record: ServerConnectivity.Record? let text: String var prefix = "" - let summary = module.summary(for: node.connectivityServerIDs, at: date) + let summary = module.summary(for: node.connectivityServerIDs) if node.isGroup { record = summary.record text = record.map { ConnectivityCopy.outcome($0.outcome) } ?? (summary.hasResults ? "—" : "") @@ -53,15 +48,13 @@ struct ConnectivityCell: View { record = module.history(for: node.id).first text = record.map { ConnectivityCopy.outcome($0.outcome) } ?? "" } - let expired = record?.isExpired(at: date) ?? false let help = record.map { prefix + $0.finishedAt.formatted(date: .numeric, time: .standard) - + (expired ? " · " + ConnectivityCopy.text("已过期") : "") } ?? (summary.hasResults && node.isGroup ? ConnectivityCopy.text("无成功结果") : ConnectivityCopy.text("尚未测试")) - return Presentation(text: text, help: help, expired: expired, testing: summary.isTesting) + return Presentation(text: text, help: help, testing: summary.isTesting) } } diff --git a/ShadowsocksX-NG2/Tests/Connectivity/ServerConnectivityTests.swift b/ShadowsocksX-NG2/Tests/Connectivity/ServerConnectivityTests.swift index e355dbf2..2f7509dd 100644 --- a/ShadowsocksX-NG2/Tests/Connectivity/ServerConnectivityTests.swift +++ b/ShadowsocksX-NG2/Tests/Connectivity/ServerConnectivityTests.swift @@ -97,13 +97,13 @@ struct ServerConnectivityTests { } #expect(module.history(for: id).count == 10) #expect(module.history(for: id).first?.outcome == .http(302)) - let summary = module.summary(for: [id], at: now) + let summary = module.summary(for: [id]) #expect(summary.hasResults) #expect(summary.record == nil) #expect(!summary.isTesting) } - @Test func freshResultsBeatStaleAndTiesUseCatalogOrder() async { + @Test func fastestLatestSuccessIgnoresAgeAndTiesUseCatalogOrder() async { let ids = (0..<3).map { _ in NodeID.fresh() } var now = Date(timeIntervalSince1970: 100) var latency = 1 @@ -114,12 +114,14 @@ struct ServerConnectivityTests { now.addTimeInterval(300) latency = 20 await module.wait(for: module.test(Array(ids.suffix(2)))) - #expect(module.summary(for: ids, at: now).serverID == ids[1]) + let firstRecord = module.history(for: ids[0]).first + #expect(module.summary(for: ids).serverID == ids[0]) + #expect(module.summary(for: Array(ids.suffix(2))).serverID == ids[1]) module.networkChanged() - #expect(module.summary(for: ids, at: now).serverID == ids[0]) - #expect(module.summary(for: ids, at: now).record?.isExpired(at: now) == true) + #expect(module.summary(for: ids).serverID == ids[0]) + #expect(module.history(for: ids[0]).first == firstRecord) module.setURL(URL(string: "https://example.com/new")!) - #expect(!module.summary(for: ids, at: now).hasResults) + #expect(!module.summary(for: ids).hasResults) } @Test func urlAndNetworkChangesSuppressInFlightAndShutdownDrainsQueuedWork() async { diff --git a/ShadowsocksX-NG2/Tests/Servers/NodeDetailPresentationTests.swift b/ShadowsocksX-NG2/Tests/Servers/NodeDetailPresentationTests.swift index ec354640..1649c478 100644 --- a/ShadowsocksX-NG2/Tests/Servers/NodeDetailPresentationTests.swift +++ b/ShadowsocksX-NG2/Tests/Servers/NodeDetailPresentationTests.swift @@ -118,7 +118,7 @@ struct NodeDetailPresentationTests { let date = Date(timeIntervalSince1970: 37_421) let records = [ ServerConnectivity.Record( - finishedAt: date, outcome: .success(milliseconds: 42), networkExpired: true), + finishedAt: date, outcome: .success(milliseconds: 42)), ServerConnectivity.Record(finishedAt: date, outcome: .requestTimeout), ServerConnectivity.Record(finishedAt: date.addingTimeInterval(-60), outcome: .http(302)), ] @@ -158,7 +158,7 @@ struct NodeDetailPresentationTests { } @Test - func connectivityGroupPropertiesFollowFreshnessNetworkChangesAndNames() async { + func connectivityGroupPropertiesFollowLatestResultsNetworkChangesAndNames() async { let ids = [NodeID.fresh(), NodeID.fresh()] var now = Date(timeIntervalSince1970: 37_421) var latency = 10 @@ -170,28 +170,31 @@ struct NodeDetailPresentationTests { method: "aes-256-gcm", plugin: nil, pluginOpts: nil) }, now: { now }, measure: { _, _, _ in .success(milliseconds: latency) }) - func properties(at date: Date) -> NodeDetailPresentation.Properties { + func properties() -> NodeDetailPresentation.Properties { NodeDetailPresentation.connectivitySummary( - module.summary(for: ids, at: date), serverName: { $0 == ids[0] ? name : "Second" }, + module.summary(for: ids), serverName: { $0 == ids[0] ? name : "Second" }, locale: Locale(identifier: "en_GB"), timeZone: TimeZone(secondsFromGMT: 0)!) } await module.wait(for: module.test([ids[0]])) - #expect(properties(at: now).items.map(\.value) == ["10 ms", "First", "10:23:41"]) + #expect(properties().items.map(\.value) == ["10 ms", "First", "10:23:41"]) name = "Renamed" - #expect(properties(at: now).items[1].value == "Renamed") + #expect(properties().items[1].value == "Renamed") now.addTimeInterval(60) latency = 20 await module.wait(for: module.test([ids[1]])) - #expect(properties(at: now).items[1].value == "Renamed") - // First expires while Second is still fresh, without another measurement. + #expect(properties().items[1].value == "Renamed") + // The older result remains the fastest without another measurement. #expect( - properties(at: now.addingTimeInterval(240)).items.map(\.value) == [ - "20 ms", "Second", "10:24:41", + properties().items.map(\.value) == [ + "10 ms", "Renamed", "10:23:41", ]) module.networkChanged() - #expect(properties(at: now.addingTimeInterval(240)).items[1].value == "Renamed") + #expect( + properties().items.map(\.value) == [ + "10 ms", "Renamed", "10:23:41", + ]) module.setURL(URL(string: "http://example.com/check")!) - #expect(properties(at: now).items.map(\.value) == [ConnectivityCopy.text("尚未测试")]) + #expect(properties().items.map(\.value) == [ConnectivityCopy.text("尚未测试")]) } private func containsFooter(in view: NSView) -> Bool {