- 账号与同步:GitHub / Google / Apple 登录后把菜单栏项目与风格等偏好同步到 getopenstats.com,换一台 Mac 登录即恢复(AccountSync 模块、SyncController、SettingsDocument、账号设置页,openstats:// 回调) - 磁盘页成为工具页:已用 / 可清除 / 可用分段容量条、空间分析、文件系统检查、本地快照清理(辅助工具协议 v4)、其他磁盘推出 - 公网 IP:归属地数据源可选 CleanIP.io / ipapi.is / DB-IP / ipinfo.io / 本地 GeoLite2,每台 Mac 自己查、本地缓存 7 天;IPv4 / IPv6 双栈各自查询与切换;弹窗显示 ASN、运营方、原生 / 住宅等徽章与纯净度评分色带;接口区块可重置上传 / 下载统计 - 弹窗:右上角按钮改为该指标自己的图标 + 总设置;主窗口页面标题栏的开关点不动已修复;滚动条改为悬浮式,内容不再偏左;网络弹窗卡片被标题行撑宽的问题已修复 - 设置:⌘ 拖动排序说明;每个菜单栏项目单独选风格并带实时预览;网络与磁盘的进程榜单固定 5 行并平滑排序;CPU 持续高负载通知 - 设计系统:分段条、评分色带、标签徽章、胶囊切换、刷新按钮等组件与徽章配色 token - 更新日志、架构说明、README 与译文同步
37 lines
1.5 KiB
Swift
37 lines
1.5 KiB
Swift
import Foundation
|
|
|
|
/// 排行榜的平滑打分:给每个条目按指数移动平均记一个分数,列表按分数排序。
|
|
/// 瞬时速率每秒都在变,直接按它排会让条目忽隐忽现、上下乱跳;按最近十几秒的平均排,
|
|
/// 一个应用读写完之后还会在榜上停留一会儿慢慢退下去,列表看起来是稳的
|
|
struct ActivityRanking: Equatable {
|
|
struct Entry: Equatable {
|
|
let id: String
|
|
let score: Double
|
|
}
|
|
|
|
private(set) var scores: [String: Double] = [:]
|
|
/// 每次采样保留的旧分数比例;0.75 时约 3 次采样后旧值影响减半
|
|
var decay = 0.75
|
|
/// 分数低于这个值且当前没有活动的条目移出榜单(字节 / 秒)
|
|
var floor: Double = 512
|
|
|
|
/// 用本次采样的瞬时值更新分数;没出现在本次采样里的条目按 0 衰减
|
|
mutating func update(_ current: [String: Double]) {
|
|
var next: [String: Double] = [:]
|
|
for id in Set(scores.keys).union(current.keys) {
|
|
let value = current[id] ?? 0
|
|
let score = (scores[id] ?? 0) * decay + value * (1 - decay)
|
|
if score >= floor || value > 0 { next[id] = score }
|
|
}
|
|
scores = next
|
|
}
|
|
|
|
func ranked(limit: Int) -> [Entry] {
|
|
Array(scores.map { Entry(id: $0.key, score: $0.value) }
|
|
.sorted { $0.score == $1.score ? $0.id < $1.id : $0.score > $1.score }
|
|
.prefix(limit))
|
|
}
|
|
|
|
var isEmpty: Bool { scores.isEmpty }
|
|
}
|