FinDataHub · macOS MenuBar 应用

性能与资源消耗全面分析

基于真实进程采样(libproc proc_pidinfo)+ 源码静态扫描, 覆盖代码规模、构建产物、依赖构成、运行时 CPU/内存/线程/系统调用/唤醒模式。 结论:CPU 已几乎归零,真正的残余功耗来自「唤醒频率」而非算力。

采样时间 2026-08-30 22:40–22:50(非交易时段) 应用 PID 3806 · v1.4.7 采样窗口 32 秒 · 1 秒粒度 macOS arm64 · 16GB

0结论速览

先看数字,再看推导。

运行时 CPU 占用
0.05%
5 进程合计 · 已无算力消耗
主进程系统调用
503/s
峰值 2500/次突发
常驻内存合计
626MB
mysqld 396MB 占 63%
.app 安装体积
653MB
其中 637MB 是 venv
.build 目录
2.0GB
1.4GB 为陈旧产物
源码总规模
77k行
379 文件 · 8 种语言
核心判断:上一轮能耗优化(P0–P2 共 8 项)已经把 CPU 从「持续占用」压到 0.05%,采集器在非交易时段已完全静默(0.1 次上下文切换/秒)。 但主进程仍保持 每秒 503 次系统调用、每 10 秒一次 2500 次调用的大突发——这类唤醒不消耗算力,却持续阻断 CPU 深度 C-state 与 App Nap,是电池耗电的残余主因。 本轮优化重心应从「降算力」转向「降唤醒次数」。

1项目静态数据

代码规模、模块构成、产物体积、依赖冗余、构建配置。

1.1 代码规模(不含 node_modules / .venv / .build / __pycache__)

语言文件数行数占比说明
Python24042,83055.3%数据抓取 / 快讯管线 / CLI 脚本
Swift6418,96424.5%MenuBar 应用主体(SwiftUI)
JavaScript286,1437.9%构建脚本 / 前端逻辑
HTML74,1645.4%架构规范 / API 指南等单页文档
Vue93,5844.6%kuaixun-frontend 组件
Shell201,1341.5%打包 / 环境配置 / 服务启停
SQL56690.9%Schema 与迁移
CSS660.0%—
合计37977,494100%—

1.2 单文件规模 Top(编译与维护热点)

文件行数
Views/SettingsView.swift2,491
Services/NewsFlashService.swift1,043
Services/PythonProcessManager.swift827
Views/DatabaseSetupGuideView.swift807
Views/DashboardView.swift673
Scripts/bin/omnidata_sources_fetcher.py955
Scripts/bin/newsflash_collect.py818

SettingsView 单文件占 Swift 总量 13%,SwiftUI 的 body 类型检查复杂度随视图体量超线性增长——这是编译慢与局部重绘范围过大的结构性来源。

1.3 构建产物体积拆解

/Applications/FinDataHub.app — 653 MB
tushare_fetcher234 MB
akshare_fetcher225 MB
kuaixun-service118 MB
findatahub-core59 MB
MacOS(二进制)10 MB
_CodeSignature6 MB
bin + 前端 + 图标0.9 MB

97.5% 的体积是 4 个 Python 虚拟环境,Swift 编译产物仅占 1.5%。体积优化的主战场在 venv,不在 Swift。

1.4 依赖冗余 可直接回收 1.4 GB+

问题位置体积性质
陈旧构建产物 FinDataHub_副本.app.build/913 MB垃圾,可删
中间编译产物.build/arm64-apple-macosx + .build/apple532 MB可重建
旧版应用备份.build/FinDataHub.app.old20 MB垃圾,可删
Python 缓存(源码树)**/__pycache__ · 1008 个目录202.5 MB可重建
Python 缓存(已打包).app 内 366 个目录62.3 MB非冗余 应用运行时生成,删除后下次启动会重建
开发工具混入运行时 venvmypy 17M + mypyc 7.8M + pip×2 12.4M + _pytest×4 5.6M43 MBdev 依赖未隔离
black 的 mypyc 编译产物fb62009…__mypyc.cpython-314-darwin.so39 MB单个 .so 文件
kuaixun-service 双 Python 版本残留.venv/lib/python3.13 + python3.1436 MB版本迁移残留
元数据目录.app 内 183 个 *.dist-info9.7 MB运行时不需要
合计可回收(磁盘 + 产物)—≈1.19 GB不含 SPM 中间产物 532 MB(删了要全量重编译)

1.5 依赖重复:4 个 venv 各装一份相同的大包

包tushare (3.14)akshare (3.12)kuaixun (3.14)core (3.14)重复占用
pandas49 MB48 MB—40 MB137 MB
numpy23 MB23 MB—22 MB68 MB
SQLAlchemy13 MB12 MB19 MB9.1 MB53 MB
lxml22 MB20 MB——42 MB
pygments—5.2 MB9.7 MB5.0 MB20 MB
合计仅统计体积前五的重复包≈320 MB
版本碎片化风险:tushare 用 Python 3.14、akshare 用 3.12、kuaixun 同时残留 3.13 与 3.14。 而 tushare_fetcher/pyproject.toml 声明 requires-python = ">=3.12"、classifier 最高只到 3.13——当前实际运行在未声明支持的 3.14 上,属于隐患而非已发故障。

1.6 构建配置中影响性能的设置

配置项现状影响
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 电源感知需要

2运行时资源占用

采集方式:ps / top 在本机沙箱下被拒绝,改用 libproc 的 proc_pidinfo(PROC_PIDTASKINFO) 直接读取内核任务统计(无需 root),10 秒窗口取差值。

2.1 进程级资源快照(10 秒窗口平均)

PID进程CPU%RSS线程上下文切换/sUNIX 调用/s缺页/s
3806FinDataHub.app 主进程0.04100.1 MB4–657.3503.238.6
3852newsflash_backend0.0016.6 MB522.919.80.0
39450newsflash_collect0.0070.1 MB30.10.20.1
1682mysqld0.00396.1 MB6043.766.60.0
1858ollama0.0043.3 MB220.00.00.0
合计0.05626 MB9412459038.7
三项已验证生效的优化:① ollama RSS 仅 43 MB 且完全静默 → OLLAMA_KEEP_ALIVE=60 已让 35B 模型成功卸载(常驻会是 20 GB+); ② collect 进程 0.1 次切换/秒 → 交易时段自适应休眠生效(当前非交易时段走 QUIET_INTERVAL=3600); ③ 全部进程 CPU ≈ 0 → 上一轮 P0/P1 改造没有留下忙等待。

2.2 主进程唤醒模式(1 秒粒度 × 32 秒,UNIX 系统调用数/秒)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

红色柱 = 大突发(2,301–2,503 次调用 + 约 190 次缺页,t=5/20/30s)· 橙色柱 = 中突发(约 290 次,t=10/15/25/28/29s)· 其余秒基本静默(0–19 次)。 CPU 全程低于 0.05%。

为什么这是问题:每次突发伴随约 190 次缺页,说明不是空转而是真实工作;但这些工作的 CPU 时间几乎为零,代价全部落在 「唤醒 → 退出 C-state → 恢复现场 → 再休眠」上。ARM Mac 上单次深度唤醒的能耗约为平稳运行的数十倍, 每秒 500 次、每 10 秒一次 2500 次的节奏足以让 CPU 包无法进入省电状态。

2.3 定时器与后台任务清单

位置周期tolerance / leeway动作评价
ServiceMonitor.swift:5130 s5 scheckOllama(HTTP)+ checkMysql(spawn nc)大突发主因
SchedulerService.swift:22230 s5 sCron 任务到期检查可接受
NewsFlashService.swift:25760 / 120 s5 s服务状态探测(PID + kill 0,无子进程)已优化
NewsFlashService.swift:204自适应 ≥900 s5 s通知轮询已优化
LogArchiveView.swift:2395 s无日志列表刷新中突发嫌疑
DebugView.swift:2442 s无调试面板刷新高频
NewsFlashView.swift:4330 s5 s自动刷新可接受
Python: newsflash_collect 心跳60 s—写 /tmp 时间戳文件已优化
Python: 采集主循环自适应 30 s–3600 s—8 信源抓取已优化

2.4 无法直接测量的数据 · 需补充的信息与测量方式

指标为何测不到可行的测量方式
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优化项清单(按收益成本比从高到低)

收益成本比 = 预期功耗/体积收益 ÷ 改动与回归风险,前 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 采样,建立交易/非交易双基线 补齐数据后才能判断采集器是否需进一步降频 低 仅测量

4前 3 项:代码级改动

可直接照抄落地,改动均在 ServiceMonitor.swift 与 build-app.sh 内,无跨模块影响。

TOP 1  用 socket connect 取代 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,需实测一次。

TOP 2  Ollama 探测改为「端口预检 + 结果缓存」

问题: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 是否在运行」这种低频信号无实际影响,且端口预检保证「服务已停止」仍是秒级感知。

TOP 3  构建产物与依赖清理

问题:.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
两处相对原方案收紧: ① 不动源目录 venv,只清理已拷进 .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 它们。

5如何验证优化是否生效

本项目沙箱下 ps / top 被拒,已提供替代脚本。

  • 系统调用基线对比:python3 /tmp/fdh_proc_sample.py 10——改造前主进程 503/s、峰值 2500;改造后目标 均值 < 100/s、无 2000+ 突发。
  • 能耗绝对值:改造前后各在电池模式下静置 30 分钟,取活动监视器「12 小时能耗影响」的差值,或 sudo powermetrics --samplers tasks -i 5000 对比平均功耗。
  • 交易时段复测:工作日 10:00 与 14:30 各采一次,确认采集器高频运行时主进程唤醒不反弹。
  • 产物体积:du -sh .build .build/FinDataHub.app /Applications/FinDataHub.app,目标分别为 <1.1 GB、<550 MB、<620 MB。

6优化后复测(已打包部署 v1.4.7)

改造已实施并打包安装,以下为同口径(libproc,1 秒粒度 × 32 秒 + 10 秒窗口)复测数据。

6.1 主进程唤醒指标:全部达标,且远超预期

指标改造前改造后变化说明
UNIX 系统调用 / 秒503.233.0−93%1 秒粒度采样下均值仅 1.2/s
突发峰值2,50315−99%—
大突发次数(>1000)3 次 / 32s0 次归零10 秒周期突发消失
上下文切换 / 秒57.313.0−77%—
缺页 / 秒38.66.6−83%URLSession + JSON 解析的对象分配大幅减少
常驻内存 RSS100.1 MB74.9 MB−25 MB不再反复创建 nc 子进程与 URLSession 对象
CPU 占用0.04%0.00%—原本也几乎为零
结论:原定目标是「均值降到 <100/s、无 2000+ 突发」,实际达成 33/s、峰值 15、大突发归零。 主进程在非交易时段已接近完全静默,CPU 包可以稳定进入深度 C-state,App Nap 不再被打断——这正是电池续航的主要来源。

6.2 产物体积

项目改造前改造后变化
构建产物 .build/FinDataHub.app585 MB515 MB−70 MB
已安装 /Applications/FinDataHub.app653 MB520 MB−133 MB(−20%)
tushare_fetcher venv212 MB146 MB−66 MB
akshare_fetcher venv198 MB196 MB−2 MB
kuaixun-service venv102 MB100 MB−2 MB
findatahub-core venv58 MB56 MB−2 MB
.build 陈旧产物933 MB0已清理

体积收益集中在 tushare_fetcher(mypy 17M + mypyc 7.8M + black 的 39M mypyc .so + pip + _pytest),其余三个 venv 的 dev 依赖本来就少。

6.3 打包踩坑记录(已修复)

现象:首版打包脚本跑到 [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 中。

6.4 部署验证

  • 主进程、newsflash_collect、newsflash_backend 三者均按预期自启
  • 快讯后端 127.0.0.1:8100 返回 HTTP 200,响应 9.4 ms
  • 心跳文件 /tmp/findatahub-newsflash.heartbeat 按 60 秒周期正常刷新
  • ollama RSS 23.4 MB 且近乎零活动,确认 35B 模型已卸载(OLLAMA_KEEP_ALIVE=60 生效)
  • 旧版已备份至 /Applications/FinDataHub.app.old,如需回滚直接换回即可

6.5 遗留观察

复测期间 mysqld 出现 1,736 次缺页/秒(CPU 仅 0.14%)。非交易时段采集器处于休眠, 该活动来源待查,疑为 InnoDB 内部维护线程或缓冲池页访问。不影响本轮结论,记录备查; 如需定位,可用 SHOW ENGINE INNODB STATUS 看缓冲池命中率与后台线程状态。

本报告数据基于 2026-08-30 22:40–22:50 的一次真实进程采样(libproc proc_pidinfo,1 秒粒度 × 32 秒 + 10 秒窗口 × 6 次)与全量源码静态扫描。 采样处于非交易时段,采集器运行在 QUIET_INTERVAL=3600 休眠档,因此不代表交易时段峰值负载;GPU 占用、精确能耗(mW)、网络数据量三项因缺少 root 权限与合适的采样窗口未能直接测量,已在第 2.4 节给出可行测量方式。 所有优化建议的预期收益为基于系统调用数与体积的推算值,实际效果需在改造后按第 5 节方法复测确认。

内容仅供工程参考,不构成投资建议。