mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
fix(connectivity): remove automatic result expiry
This commit is contained in:
+2
-2
@@ -90,10 +90,10 @@ _避免用词_:可移除节点(Removable node)。
|
||||
服务器连通性测试请求访问的目标地址,是用户可修改的非敏感偏好信息,在展示、日志与诊断中无需脱敏。
|
||||
|
||||
**代理响应延迟(Proxy response latency)**:
|
||||
完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。
|
||||
完成目标地址选择与预热后,经指定服务器配置访问测试 URL 的 HTTP 响应头耗时,以成功样本的中位数表示。结果保留测试时间,不按固定时长或网络变化自动判为过期,用户结合测试时间自行判断参考价值。它与测试目标及测试条件有关,不表示服务器 IP 的纯网络往返时间或下载速度。
|
||||
|
||||
**配置组最快响应延迟(Fastest group response latency)**:
|
||||
配置组全部后代服务器的成功测试结果中,符合展示条件的最小代理响应延迟。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。
|
||||
配置组全部后代服务器最近一次测试结果中的成功项,其最小代理响应延迟。最近一次结果失败的服务器不贡献更早的成功结果;汇总不按测试时间排除结果。它不表示活动配置组实际选用的服务器或负载均衡后的响应耗时。
|
||||
|
||||
**托管插件(Managed plugin)**:
|
||||
由产品随应用发布的 SIP003 插件,其发布版本与来源事实属于随应用提供的程序,不代表用户自备的同名程序。
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
|
||||
@@ -52,23 +52,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"已过期": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Expired"
|
||||
}
|
||||
},
|
||||
"zh-Hans": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"无成功结果": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
||||
@@ -159,12 +159,10 @@ private struct ObservedConnectivityProperties<Content: View>: 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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user