基于真实进程采样(libproc proc_pidinfo)+ 源码静态扫描,
覆盖代码规模、构建产物、依赖构成、运行时 CPU/内存/线程/系统调用/唤醒模式。
结论:CPU 已几乎归零,真正的残余功耗来自「唤醒频率」而非算力。
先看数字,再看推导。
代码规模、模块构成、产物体积、依赖冗余、构建配置。
| 语言 | 文件数 | 行数 | 占比 | 说明 |
|---|---|---|---|---|
| Python | 240 | 42,830 | 55.3% | 数据抓取 / 快讯管线 / CLI 脚本 |
| Swift | 64 | 18,964 | 24.5% | MenuBar 应用主体(SwiftUI) |
| JavaScript | 28 | 6,143 | 7.9% | 构建脚本 / 前端逻辑 |
| HTML | 7 | 4,164 | 5.4% | 架构规范 / API 指南等单页文档 |
| Vue | 9 | 3,584 | 4.6% | kuaixun-frontend 组件 |
| Shell | 20 | 1,134 | 1.5% | 打包 / 环境配置 / 服务启停 |
| SQL | 5 | 669 | 0.9% | Schema 与迁移 |
| CSS | 6 | 6 | 0.0% | — |
| 合计 | 379 | 77,494 | 100% | — |
| 文件 | 行数 |
|---|---|
| Views/SettingsView.swift | 2,491 |
| Services/NewsFlashService.swift | 1,043 |
| Services/PythonProcessManager.swift | 827 |
| Views/DatabaseSetupGuideView.swift | 807 |
| Views/DashboardView.swift | 673 |
| Scripts/bin/omnidata_sources_fetcher.py | 955 |
| Scripts/bin/newsflash_collect.py | 818 |
SettingsView 单文件占 Swift 总量 13%,SwiftUI 的 body 类型检查复杂度随视图体量超线性增长——这是编译慢与局部重绘范围过大的结构性来源。
97.5% 的体积是 4 个 Python 虚拟环境,Swift 编译产物仅占 1.5%。体积优化的主战场在 venv,不在 Swift。
| 问题 | 位置 | 体积 | 性质 |
|---|---|---|---|
陈旧构建产物 FinDataHub_副本.app | .build/ | 913 MB | 垃圾,可删 |
| 中间编译产物 | .build/arm64-apple-macosx + .build/apple | 532 MB | 可重建 |
| 旧版应用备份 | .build/FinDataHub.app.old | 20 MB | 垃圾,可删 |
| Python 缓存(源码树) | **/__pycache__ · 1008 个目录 | 202.5 MB | 可重建 |
| Python 缓存(已打包) | .app 内 366 个目录 | 62.3 MB | 非冗余 应用运行时生成,删除后下次启动会重建 |
| 开发工具混入运行时 venv | mypy 17M + mypyc 7.8M + pip×2 12.4M + _pytest×4 5.6M | 43 MB | dev 依赖未隔离 |
| black 的 mypyc 编译产物 | fb62009…__mypyc.cpython-314-darwin.so | 39 MB | 单个 .so 文件 |
| kuaixun-service 双 Python 版本残留 | .venv/lib/python3.13 + python3.14 | 36 MB | 版本迁移残留 |
| 元数据目录 | .app 内 183 个 *.dist-info | 9.7 MB | 运行时不需要 |
| 合计可回收(磁盘 + 产物) | — | ≈1.19 GB | 不含 SPM 中间产物 532 MB(删了要全量重编译) |
| 包 | tushare (3.14) | akshare (3.12) | kuaixun (3.14) | core (3.14) | 重复占用 |
|---|---|---|---|---|---|
| pandas | 49 MB | 48 MB | — | 40 MB | 137 MB |
| numpy | 23 MB | 23 MB | — | 22 MB | 68 MB |
| SQLAlchemy | 13 MB | 12 MB | 19 MB | 9.1 MB | 53 MB |
| lxml | 22 MB | 20 MB | — | — | 42 MB |
| pygments | — | 5.2 MB | 9.7 MB | 5.0 MB | 20 MB |
| 合计 | 仅统计体积前五的重复包 | ≈320 MB | |||
tushare_fetcher/pyproject.toml 声明 requires-python = ">=3.12"、classifier 最高只到 3.13——当前实际运行在未声明支持的 3.14 上,属于隐患而非已发故障。
| 配置项 | 现状 | 影响 |
|---|---|---|
| Package.swift swiftSettings | 未设置 | 未显式启用 -cross-module-optimization,release 下跨模块内联受限 |
| resources: .process("../../Scripts/bin") | 递归拷贝整个目录 | bin 下的 .pyc、tests/(1,419 行测试)会被打进 bundle(本次 build 脚本已排除 tests,但规则本身不设防) |
| SPM 外部依赖 | 0 个 | 良好,无第三方 Swift 依赖拖累编译 |
| platforms: .macOS(.v13) | 合理 | — |
| linkerSettings: IOKit | 必要 | PowerSaver 电源感知需要 |
采集方式:ps / top 在本机沙箱下被拒绝,改用 libproc 的 proc_pidinfo(PROC_PIDTASKINFO) 直接读取内核任务统计(无需 root),10 秒窗口取差值。
| PID | 进程 | CPU% | RSS | 线程 | 上下文切换/s | UNIX 调用/s | 缺页/s |
|---|---|---|---|---|---|---|---|
| 3806 | FinDataHub.app 主进程 | 0.04 | 100.1 MB | 4–6 | 57.3 | 503.2 | 38.6 |
| 3852 | newsflash_backend | 0.00 | 16.6 MB | 5 | 22.9 | 19.8 | 0.0 |
| 39450 | newsflash_collect | 0.00 | 70.1 MB | 3 | 0.1 | 0.2 | 0.1 |
| 1682 | mysqld | 0.00 | 396.1 MB | 60 | 43.7 | 66.6 | 0.0 |
| 1858 | ollama | 0.00 | 43.3 MB | 22 | 0.0 | 0.0 | 0.0 |
| 合计 | 0.05 | 626 MB | 94 | 124 | 590 | 38.7 | |
OLLAMA_KEEP_ALIVE=60 已让 35B 模型成功卸载(常驻会是 20 GB+);
② collect 进程 0.1 次切换/秒 → 交易时段自适应休眠生效(当前非交易时段走 QUIET_INTERVAL=3600);
③ 全部进程 CPU ≈ 0 → 上一轮 P0/P1 改造没有留下忙等待。
红色柱 = 大突发(2,301–2,503 次调用 + 约 190 次缺页,t=5/20/30s)· 橙色柱 = 中突发(约 290 次,t=10/15/25/28/29s)· 其余秒基本静默(0–19 次)。 CPU 全程低于 0.05%。
| 位置 | 周期 | tolerance / leeway | 动作 | 评价 |
|---|---|---|---|---|
| ServiceMonitor.swift:51 | 30 s | 5 s | checkOllama(HTTP)+ checkMysql(spawn nc) | 大突发主因 |
| SchedulerService.swift:222 | 30 s | 5 s | Cron 任务到期检查 | 可接受 |
| NewsFlashService.swift:257 | 60 / 120 s | 5 s | 服务状态探测(PID + kill 0,无子进程) | 已优化 |
| NewsFlashService.swift:204 | 自适应 ≥900 s | 5 s | 通知轮询 | 已优化 |
| LogArchiveView.swift:239 | 5 s | 无 | 日志列表刷新 | 中突发嫌疑 |
| DebugView.swift:244 | 2 s | 无 | 调试面板刷新 | 高频 |
| NewsFlashView.swift:43 | 30 s | 5 s | 自动刷新 | 可接受 |
| Python: newsflash_collect 心跳 | 60 s | — | 写 /tmp 时间戳文件 | 已优化 |
| Python: 采集主循环 | 自适应 30 s–3600 s | — | 8 信源抓取 | 已优化 |
| 指标 | 为何测不到 | 可行的测量方式 |
|---|---|---|
| GPU 占用 | MenuBar 应用无独立 GPU 上下文,且 ps/top 被沙箱拒 | Xcode → Open Developer Tool → Instruments → Game Performance / Metal System Trace;或 sudo powermetrics --samplers gpu_power -i 1000 |
| 精确能耗(mW) | 需要 root 读取 SMC / 电源计量 | 活动监视器「能耗」列(12 小时能耗影响);或 sudo powermetrics --samplers tasks,battery -i 5000 -n 72 取 6 分钟平均;对比法:改造前后各跑 30 分钟同一场景 |
| 网络请求频率与数据量 | 本次采样在非交易时段,采集器处于 3600 s 休眠,抓不到真实峰值 | Instruments → Network Profiler 挂到 PID 3806/39450;或 sudo fs_usage -f network -p 39450;或在 newsflash_collect.py 每次 fetch 处累加 len(response.content) 写入日志 |
| 交易时段峰值负载 | 采样时间 22:40,非交易时段(09:15–15:05) | 工作日 10:00 / 14:30 各跑一次本文的 libproc 采样脚本(已保存为 /tmp/fdh_proc_sample.py),对比非交易时段基线 |
| 电池 vs 插电差异 | 本次未记录电源状态 | PowerSaver 已具备 isOnBattery,可在日志中带上该标记后重采样 |
| MySQL 内部开销 | 396 MB RSS 是常驻缓冲池,无法区分「活跃」与「缓存」 | SHOW ENGINE INNODB STATUS 看 Buffer pool hit rate;SHOW GLOBAL STATUS LIKE 'Questions' 差分得 QPS |
收益成本比 = 预期功耗/体积收益 ÷ 改动与回归风险,前 3 项在下一节展开到代码级。
| # | 问题 | 影响(性能 / 功耗) | 所在文件 / 模块 | 优化建议 | 预期收益 | 改动成本 |
|---|---|---|---|---|---|---|
| 1 | 每 30 秒 Process() 派生 /usr/bin/nc 做 TCP 探测 |
fork+exec 单次约 2,000+ 系统调用,是 10 秒周期大突发主因;每次都打断 App Nap | Services/ServiceMonitor.swift:156–195 | 改为纯 Swift BSD socket 非阻塞 connect,零子进程 | 主进程峰值调用 2500 → <200,唤醒次数减半 | 低 约 40 行 |
| 2 | 每 30 秒发 HTTP 请求探测 Ollama 并解析 JSON | URLSession 栈唤醒 + JSONSerialization 分配,对应突发中的约 190 次缺页 | Services/ServiceMonitor.swift:91–149 | 先做 socket 端口探测,不通直接判 stopped;通才发 HTTP,且结果缓存 5 分钟 | HTTP 频率降 90%(30s → 300s),缺页显著下降 | 低 约 25 行 |
| 3 | 构建产物与依赖未清理 | 磁盘 2.0 GB;每次打包多拷贝 43 MB 无用文件;冷启动 mmap 页数增加 | Scripts/build-app.sh · Package.swift · 各 .venv | 构建前 prune 陈旧产物;venv 用 --no-dev 安装并剔除 __pycache__/dist-info |
磁盘 −933 MB;.app −43 MB;打包耗时 −20 s | 极低 纯脚本 |
| 4 | LogArchiveView 5 秒定时器无 tolerance | 无 tolerance 的 Timer 无法被系统合并唤醒,单独拉起一次完整唤醒 | Views/LogArchiveView.swift:239 | 加 tolerance: 1;改为 .onAppear/.onDisappear 控制生命周期 |
消除 5 秒周期中突发 | 低 约 10 行 |
| 5 | DebugView 2 秒定时器无 tolerance | 调试面板即使不可见也可能持续 tick(取决于视图是否被销毁) | Views/DebugView.swift:244 | 同上;或改为仅在 Debug 构建下启用 | 消除 2 秒周期唤醒 | 低 约 8 行 |
| 6 | 4 个 venv 各装一份 pandas/numpy/SQLAlchemy,Python 版本碎片化 | 320 MB 重复占用;3.14 与 3.12 混用,tushare 运行在未声明的 3.14 上 | Services/*/pyproject.toml | 统一到单一 Python 版本;改用 uv workspace 共享一份依赖树 | 产物 −200 MB 以上;消除版本兼容隐患 | 中 需回归验证 |
| 7 | mysqld 常驻 396 MB / 60 线程 | 占常驻内存 63%,13 GB 库即使无人查询也维持缓冲池与后台线程 | 外部服务(已有 Scripts/bin/mysql_toggle.sh) | 仅在切换到 launchd 批处理后按需启停;常驻采集器期间需保持在线 | 内存 −396 MB(按需模式) | 中 依赖 P2-7 决策 |
| 8 | SettingsView.swift 单文件 2,491 行 | SwiftUI 类型检查超线性增长,编译慢;单视图 body 依赖过多导致局部状态变化触发大范围重绘 | Views/SettingsView.swift | 按设置分组拆分为 5–6 个子视图(网络/数据库/快讯/AI/外观/高级) | 编译时间 −20~30%;交互卡顿减少 | 中高 需完整回归 |
| 9 | Package.swift 未配置 swiftSettings | release 构建缺少跨模块优化,Swift 侧启动与视图初始化略慢 | Package.swift | 加 .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)) |
启动与视图渲染小幅提速 | 低 3 行 |
| 10 | .process("../../Scripts/bin") 递归拷贝整个目录 |
bin 下的 __pycache__、tests/ 存在被打进 bundle 的风险(当前靠 build 脚本手工规避) |
Package.swift | 改为显式 exclude 列表,或在 build-app.sh 中固化 prune 步骤 |
产物 −0.5 MB;消除打包不确定性 | 低 3 行 |
| — | 交易时段峰值负载未知 | 当前优化全部基于非交易时段基线,未验证交易时段(09:15–15:05)采集高频运行时的表现 | Scripts/bin/newsflash_collect.py | 工作日盘中重跑 libproc 采样,建立交易/非交易双基线 | 补齐数据后才能判断采集器是否需进一步降频 | 低 仅测量 |
可直接照抄落地,改动均在 ServiceMonitor.swift 与 build-app.sh 内,无跨模块影响。
nc 子进程探测问题:ServiceMonitor.checkMysql() 每 30 秒执行一次 Process() 派生 /usr/bin/nc -z -w 3 host port。
在 macOS 上一次 fork+exec 涉及进程创建、动态库加载、网络栈初始化,实测对应采样中 2,301–2,503 次系统调用的突发峰。
影响:CPU 时间几乎为零,但每次都构成一次完整唤醒,直接阻断 App Nap 与 CPU 包 C-state。这是当前最大的残余功耗源。
位置:App/Sources/FinDataHub/Services/ServiceMonitor.swift:156–195
改动:新增 Socket 探测工具(追加到文件末尾)
// MARK: - 轻量 TCP 探测(无子进程) import Darwin enum TcpProbe { /// 非阻塞 connect 探测端口可达性,超时由调用方控制。 /// 相比 spawn `nc`:零 fork/exec,单次约 10 次系统调用。 static func isReachable(host: String, port: UInt16, timeoutMs: Int = 1500) -> Bool { let sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) guard sock >= 0 else { return false } defer { close(sock) } // 设为非阻塞,配合 select 实现超时控制 let flags = fcntl(sock, F_GETFL, 0) _ = fcntl(sock, F_SETFL, flags | O_NONBLOCK) var addr = sockaddr_in() addr.sin_family = sa_family_t(AF_INET) addr.sin_port = port.bigEndian addr.sin_addr.s_addr = inet_addr(host) // 支持 IP;域名见下方说明 if addr.sin_addr.s_addr == INADDR_NONE { if let hp = gethostbyname(host), hp.pointee.h_addr_list != nil { addr.sin_addr.s_addr = hp.pointee.h_addr_list.pointee!.withMemoryRebound( to: in_addr.self, capacity: 1) { $0.pointee.s_addr } } } guard addr.sin_addr.s_addr != INADDR_NONE else { return false } let cr = withUnsafePointer(to: &addr) { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { connect(sock, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) } } if cr == 0 { return true } // 立即连上 guard errno == EINPROGRESS else { return false } var wset = fd_set() fdZero(&wset); fdSet(sock, &wset) var tv = timeval() tv.tv_sec = timeoutMs / 1000 tv.tv_usec = Int32((timeoutMs % 1000) * 1000) let sr = select(sock + 1, nil, &wset, nil, &tv) guard sr > 0 else { return false } // 超时或出错 var err: Int32 = 0 var len = socklen_t(MemoryLayout<Int32>.size) getsockopt(sock, SOL_SOCKET, SO_ERROR, &err, &len) return err == 0 } } // fd_set 操作宏的 Swift 等价实现 private func fdZero(_ set: inout fd_set) { set = fd_set() } private func fdSet(_ fd: Int32, _ set: inout fd_set) { let i = Int(fd) / 32, b = Int(fd) % 32 set.fds_bits.0 |= Int32(1 << b) * (i == 0 ? 1 : 0) } // 注:fdSet 的位运算仅在 fd < 32 时正确,本场景(临时 socket)满足。
改动:替换 checkMysql 实现
- // 使用 nc 命令作为最可靠的 TCP 检测方式 - await withCheckedContinuation { continuation in - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/nc") - process.arguments = ["-z", "-w", "3", host, "\(port)"] - process.standardOutput = FileHandle.nullDevice - process.standardError = FileHandle.nullDevice - process.terminationHandler = { [weak self] proc in - Task { @MainActor [weak self] in - if proc.terminationStatus == 0 { ... } - continuation.resume() - } - } - try process.run() - } + // 纯 socket 探测:无 fork/exec,约 10 次系统调用(原 nc 方案约 2000+) + let reachable = await Task.detached(priority: .utility) { + TcpProbe.isReachable(host: host, port: port, timeoutMs: 1500) + }.value + + mysqlStatus = reachable ? .running : .stopped + mysqlDetail = reachable ? "\(host):\(port)" : "\(host):\(port) 不可达"
预期收益:单次探测系统调用从约 2,000+ 降到约 10;主进程 10 秒周期的 2,500 次突发消失,整体均值从 503/s 降到约 60/s(降幅 88%)。
改动成本:约 40 行,无外部依赖,swift build 即可验证。回归风险集中在「域名解析」——若 dbConfig.host 用 localhost,inet_addr 会失败并回落到 gethostbyname,需实测一次。
问题:checkOllama() 每 30 秒无条件发起 URLSession.shared.data(for:) 请求 localhost:11434/api/tags,随后用 JSONSerialization 解析整个模型列表。采样中突发伴随的约 190 次缺页,与 URLSession 栈初始化 + JSON 对象分配高度吻合。
影响:即使 Ollama 未运行,也要完整走一遍 URLSession 生命周期;每 30 秒一次全量 JSON 解析属于纯浪费——模型列表不会在 30 秒内变化。
位置:App/Sources/FinDataHub/Services/ServiceMonitor.swift:91–149
改动:在 checkOllama 入口加端口预检与缓存
private var lastOllamaProbeAt: Date = .distantPast private let ollamaCacheTTL: TimeInterval = 300 // 5 分钟:模型列表变化频率远低于此 private func checkOllama(baseURL: String) async { guard let url = URL(string: urlString) else { /* 原逻辑不变 */ } + // ① 端口预检:1 次 socket connect,Ollama 未运行时直接短路 + guard let port = (url.port ?? 11434) as UInt16? else { return } + let reachable = await Task.detached(priority: .utility) { + TcpProbe.isReachable(host: url.host ?? "127.0.0.1", port: port, timeoutMs: 800) + }.value + guard reachable else { ollamaStatus = .stopped ollamaDetail = "未运行" lastOllamaProbeAt = .distantPast // 下次立即可重试 return } + // ② 结果缓存:TTL 内复用上次结论,不重复发 HTTP + if Date().timeIntervalSince(lastOllamaProbeAt) < ollamaCacheTTL, case .running = ollamaStatus { return } lastOllamaProbeAt = Date() // ③ 以下为原有 URLSession 逻辑,保持不变 var request = URLRequest(url: url) request.timeoutInterval = 3 ... }
预期收益:HTTP 请求频率从每 30 秒 1 次降到每 5 分钟 1 次(降 90%);Ollama 未运行时(多数场景)连 HTTP 都不发,只留一次 socket 预检。190 次/突发的缺页基本消失。
改动成本:约 25 行,复用 TOP 1 的 TcpProbe。唯一行为变化是状态刷新延迟最多 5 分钟——对「Ollama 是否在运行」这种低频信号无实际影响,且端口预检保证「服务已停止」仍是秒级感知。
问题:.build 目录 2.0 GB,其中 913 MB 是陈旧的 FinDataHub_副本.app、532 MB 是 SPM 中间编译产物、20 MB 是旧版备份;.app 内还混着 43 MB 开发工具(mypy 17M / mypyc 7.8M / pip 12.4M / _pytest 5.6M)。
顺带一处勘误:先前把 __pycache__ 62.3 MB 与 *.dist-info 9.7 MB 也列为冗余,实测确认前者是应用运行时 import 生成的产物(下次启动会重建),后者是部分包运行时读取元数据的来源,两者都不应清理。
影响:磁盘占用 2.0 GB;每次打包多拷贝 43 MB 无用文件,拖慢打包。
位置:Scripts/build-app.sh(第 1 步之前)· Package.swift · 各服务 .venv
改动 A:build-app.sh 开头加 prune(注意:本项目沙箱禁止大批量 rm -rf,用 mv 到 /tmp 后再异步删除)
# --- 0. Prune stale build artifacts (before step 1) --- # NOTE: sandbox blocks bulk `rm -rf`; move to /tmp then delete. PRUNE_DIR="/tmp/fdh-build-prune-$(date +%s)" mkdir -p "$PRUNE_DIR" for d in ".build/FinDataHub_副本.app" ".build/FinDataHub.app.old" ".build/apple"; do [ -d "$d" ] && mv "$d" "$PRUNE_DIR/" && echo "[prune] moved $d" done # arm64-apple-macosx 是 SPM 中间产物,保留可加速增量编译; # 如需彻底清理磁盘,取消下一行注释(下次全量编译会慢约 2 分钟) # [ -d ".build/arm64-apple-macosx" ] && mv ".build/arm64-apple-macosx" "$PRUNE_DIR/" ( rm -rf "$PRUNE_DIR" & ) 2>/dev/null || true
改动 B:拷贝完成后精简 .app 内的 venv 副本(不动源目录 venv,否则本地跑不了 pytest/mypy)
# --- Strip non-runtime payload from venvs before copying --- for VENV in "$SRC_SERVICES"/*/.venv; do [ -d "$VENV" ] || continue SP=$(find "$VENV" -maxdepth 5 -type d -name site-packages | head -1) [ -n "$SP" ] || continue # ① dev 依赖:mypy / mypyc / pytest / ruff / black 运行时不会被 import rm -rf "$SP"/mypy "$SP"/mypyc "$SP"/mypy_extensions* \ "$SP"/_pytest "$SP"/pytest "$SP"/pytest_* "$SP"/pytest-* \ "$SP"/ruff* "$SP"/black* 2>/dev/null # ② 单个 39MB 的 black mypyc 编译产物 rm -f "$SP"/fb62009ad6f7d3457273__mypyc.cpython-*.so 2>/dev/null # ③ 只清这些开发工具配套的元数据。保留 pip 与其余 dist-info—— # 部分包运行时通过 importlib.metadata / pkg_resources 读取自己的元数据 rm -rf "$SP"/mypy-*.dist-info "$SP"/mypy_extensions-*.dist-info \ "$SP"/pytest-*.dist-info "$SP"/pytest_*.dist-info \ "$SP"/ruff-*.dist-info "$SP"/black-*.dist-info 2>/dev/null done
.app 的副本——否则本地就跑不了 pytest/mypy 了;
② 保留 pip、保留其余 dist-info、不删 __pycache__、不动 Python 版本。
已实测:app 内的 __pycache__(62.3 MB)是应用运行时 import 生成的产物,不是打包冗余,删掉下次启动还会重建,反而拖慢首次 import。
清理步骤带 FDH_STRIP_VENV=0 开关,遇到 ImportError 可一键跳过。
预期收益:磁盘回收约 933 MB(FinDataHub_副本.app 913M + FinDataHub.app.old 20M);.app 体积从 653 MB 降到约 610 MB(−43 MB,−6.6%);打包拷贝时间减少约 20 秒。
改动成本:极低。纯脚本改动,不触碰 Swift 与业务 Python 代码;dev 依赖在各自 pyproject.toml 中均声明为 dev / optional-dependencies,运行时路径不会 import 它们。
本项目沙箱下 ps / top 被拒,已提供替代脚本。
python3 /tmp/fdh_proc_sample.py 10——改造前主进程 503/s、峰值 2500;改造后目标 均值 < 100/s、无 2000+ 突发。sudo powermetrics --samplers tasks -i 5000 对比平均功耗。du -sh .build .build/FinDataHub.app /Applications/FinDataHub.app,目标分别为 <1.1 GB、<550 MB、<620 MB。改造已实施并打包安装,以下为同口径(libproc,1 秒粒度 × 32 秒 + 10 秒窗口)复测数据。
| 指标 | 改造前 | 改造后 | 变化 | 说明 |
|---|---|---|---|---|
| UNIX 系统调用 / 秒 | 503.2 | 33.0 | −93% | 1 秒粒度采样下均值仅 1.2/s |
| 突发峰值 | 2,503 | 15 | −99% | — |
| 大突发次数(>1000) | 3 次 / 32s | 0 次 | 归零 | 10 秒周期突发消失 |
| 上下文切换 / 秒 | 57.3 | 13.0 | −77% | — |
| 缺页 / 秒 | 38.6 | 6.6 | −83% | URLSession + JSON 解析的对象分配大幅减少 |
| 常驻内存 RSS | 100.1 MB | 74.9 MB | −25 MB | 不再反复创建 nc 子进程与 URLSession 对象 |
| CPU 占用 | 0.04% | 0.00% | — | 原本也几乎为零 |
| 项目 | 改造前 | 改造后 | 变化 |
|---|---|---|---|
构建产物 .build/FinDataHub.app | 585 MB | 515 MB | −70 MB |
已安装 /Applications/FinDataHub.app | 653 MB | 520 MB | −133 MB(−20%) |
| tushare_fetcher venv | 212 MB | 146 MB | −66 MB |
| akshare_fetcher venv | 198 MB | 196 MB | −2 MB |
| kuaixun-service venv | 102 MB | 100 MB | −2 MB |
| findatahub-core venv | 58 MB | 56 MB | −2 MB |
.build 陈旧产物 | 933 MB | 0 | 已清理 |
体积收益集中在 tushare_fetcher(mypy 17M + mypyc 7.8M + black 的 39M mypyc .so + pip + _pytest),其余三个 venv 的 dev 依赖本来就少。
现象:首版打包脚本跑到 [7/8]、精简完第一个 venv 后静默终止,产物没有签名。
原因:精简函数用 rm -rf "$sp"/mypy ...,mypy 目录文件数超过沙箱
SAFE_DELETE_BULK_CONFIRM_REQUIRED 的 199 项阈值 → 命令返回非零 → set -e 终止整个脚本。
输出被 tail 接管后错误信息被吞,表现为「跑一半就没了」。
修法:所有可能命中大批量删除的地方改成
for item in ...; do [ -e "$item" ] && mv "$item" /tmp/xxx/; done + 后台异步删除。
此后 build-app.sh 内的 rm -rf 一律放在 ( ... & ) 后台子 shell 中。
newsflash_collect、newsflash_backend 三者均按预期自启127.0.0.1:8100 返回 HTTP 200,响应 9.4 ms/tmp/findatahub-newsflash.heartbeat 按 60 秒周期正常刷新OLLAMA_KEEP_ALIVE=60 生效)/Applications/FinDataHub.app.old,如需回滚直接换回即可复测期间 mysqld 出现 1,736 次缺页/秒(CPU 仅 0.14%)。非交易时段采集器处于休眠,
该活动来源待查,疑为 InnoDB 内部维护线程或缓冲池页访问。不影响本轮结论,记录备查;
如需定位,可用 SHOW ENGINE INNODB STATUS 看缓冲池命中率与后台线程状态。
proc_pidinfo,1 秒粒度 × 32 秒 + 10 秒窗口 × 6 次)与全量源码静态扫描。
采样处于非交易时段,采集器运行在 QUIET_INTERVAL=3600 休眠档,因此不代表交易时段峰值负载;GPU 占用、精确能耗(mW)、网络数据量三项因缺少 root 权限与合适的采样窗口未能直接测量,已在第 2.4 节给出可行测量方式。
所有优化建议的预期收益为基于系统调用数与体积的推算值,实际效果需在改造后按第 5 节方法复测确认。