定位常驻进程、轮询机制与子进程 spawn 三类能耗热点,给出按优先级排序、改动最小见效最快的优化路径。所有结论基于源码取证,附具体文件与行号。
FinDataHub 是一个「菜单栏常驻」应用,本体用 SwiftUI 编写,但它真正的能耗并不在 Swift 壳,而在它拉起的一批 Python/数据库/模型常驻进程。
| 功能模块 | 进程 / 组件 | 运行形态 | 能耗贡献 |
|---|---|---|---|
| 数据同步(AkShare / TuShare) | Python CLI(akshare_fetcher / tushare_fetcher) | 按需启动,同步完即退 | 低(按需) |
| 快讯采集(8 信源) | newsflash_collect.py(常驻) | 常驻循环抓取 | 高 |
| 快讯后端 API | newsflash_backend.py(:8100) | 常驻 HTTP 服务 | 中 |
| 定时调度(Cron) | Swift SchedulerService | 30s Timer 轮询 | 低 |
| AI 评分 / 摘要 | Ollama(qwen3.6:35b,:11434) | 常驻(若自启) | 极高(单项最大) |
| 数据存储 | MySQL(本地 13GB 库) | 常驻 mysqld | 中 |
| 服务监控 / 自动恢复 | Swift ServiceMonitor / AutoRecoveryManager | 30s / 20s 轮询 | 中 |
按「常驻无感知 → 高频轮询 → 视图级」三级拆解,每项附代码证据。
| # | 热点 | 代码证据 | 机理 |
|---|---|---|---|
| A1 | 快讯采集器常驻 newsflash_collect.py |
NewsFlashService.swift:492-495 启动时传 --interval 120;信源内部频率更激进:jin10=10s、cls/wallstreetcn=30s、sina/eastmoney=60s;pipeline.py:207 run_forever(30s) |
全天候发起 HTTP 抓取 + HTML 解析,网络射频反复唤醒 + CPU 解析 |
| A2 | Ollama 本地大模型 qwen3.6:35b |
AppConstants.swift:363-365 默认模型 35B、API localhost:11434;ServiceMonitor.swift 每 30s 探测它 |
35B 模型常驻内存数十 GB,即使空闲也阻止系统深度睡眠,是单项最大能耗 |
| A3 | MySQL 常驻 13GB 库 |
ServiceMonitor.swift:154-192 持续检测其端口 | mysqld 常驻内存,影响内存换页与后台能耗 |
| A4 | 快讯后端常驻 :8100 |
NewsFlashService.swift:672-688 startBackend() | 常驻 HTTP 服务,维持端口监听与事件循环 |
| # | 热点 | 频率 | 代码证据 | 代价 |
|---|---|---|---|---|
| B1 | 快讯服务健康检查 | 30s | NewsFlashService.swift:226 | 每轮 fetchCollectorStatus + fetchBackendStatus(:665/:741)各 spawn 一个 Python 解释器 → 每小时 240 次 fork/exec Python |
| B2 | Ollama + MySQL 探测 | 30s | ServiceMonitor.swift:51 | 每轮 spawn nc 子进程 + 发 HTTP 请求(Dashboard 打开时) |
| B3 | 通知轮询 | 300s | NewsFlashService.swift:202 | spawn Python 脚本拉取最新快讯 |
| B4 | 定时调度 Timer | 30s | SchedulerService.swift:220、AppConstants.timerInterval=30 | 仅启用 Cron 任务时运行 |
| # | 热点 | 频率 | 代码证据 |
|---|---|---|---|
| C1 | 同步进度 / AkShare / TuShare 视图计时器 | 1s | SyncProgressView.swift:7、AkShareView.swift:9、TuShareView.swift:12 |
| C2 | 快讯视图自动刷新 | 30s | NewsFlashView.swift:43(默认关闭,可配置) |
| C3 | 日志归档视图轮询 | 5s | LogArchiveView.swift:239 |
主 App 在 FinDataHubApp.swift:27-28 显式调用 disableAutomaticTermination + disableSuddenTermination,并配合开机自启(LaunchAtLoginService)与快讯自启(FinDataHubApp.swift:33-43 启动 5s 后拉起后端+采集器)。结果是:即便你一个窗口都没开,这套进程也在后台持续运转。
三类能耗机理,分别对应不同的治理手段。
P0 = 改动最小、见效最快、以配置/开关为主;P1 = 少量代码改动;P2 = 架构级重构。建议先落 P0。
改动点:保留 ollama 开机自启(主进程仅约 50MB,可忽略),但设置 OLLAMA_KEEP_ALIVE=60(秒):35B 模型在 60 秒无请求后自动从内存卸载,AI 评分/摘要调用时再加载。当前 AppConstants.swift:363-365 默认模型 qwen3.6:35b,模型常驻内存是单项最大能耗源,卸载后该项功耗趋近于 0,自启能力与 AI 功能完全保留。
实现代价:极低 —— Ollama 环境变量配置,不动业务代码。
改动点:① 金十 fetch_interval=10s(jin10.py:20)提到 30s;② 默认 --interval 120 提升到 300s;③ 增加「非交易时段(15:05~次日 9:15)与深夜自动降频/暂停抓取」策略 —— 采集器进程保持自启常驻,但空闲时段内部进入低功耗等待,不发起任何网络请求。
实现代价:低 —— 采集器侧加一个时间段判断即可。
改动点:当前 fetchCollectorStatus / fetchBackendStatus 每次启动 Python 解释器。改为:读 PID 文件后 kill -0 <pid> 判断存活(Process 直接调系统调用,不起 Python),并把 collector status 与 backend status 合并为一次查询;轮询间隔 30s → 60s。
实现代价:低~中 —— 复用已有的 /tmp/findatahub-*.pid 文件即可。
改动点:用 IOPSCopyPowerSourcesInfo 检测供电状态,实时切换两档阈值:电池供电时(移动办公场景)自动放大采集间隔(120s→480s)、健康检查 30s→120s、暂停通知轮询与视图自动刷新;插电时恢复全速。自启与全部功能保留,仅移动时自动降功耗 —— 正好命中「出门电池掉得快」的核心场景。
实现代价:低 —— Swift 侧一个电源状态监听 + 一组阈值切换(约 40 行)。
改动点:给所有 Timer.scheduledTimer / DispatchSourceTimer 设置 leeway(如 5s),并统一收敛到同一调度队列,允许 macOS Timer Coalescing 合并唤醒 —— 这是「常驻保留但能休眠」的关键:App 进程仍在,但密集定时器不再阻止系统挂起它。
改动点:三个 1s 的 Timer.publish 仅用于显示「已耗时」,可用 Text(timerInterval:) 或 TimelineView 替代,只在视图可见时刷新,后台自动停止。
改动点:用 launchd 的 StartCalendarInterval 在交易时段(如 9:00~15:30 每 5 分钟)跑一次性采集,替代 7×24 常驻循环。launchd 同样支持开机自启(RunAtLoad),自启体验不变,但彻底消除空闲时段的常驻进程。
改动点:MySQL 仅在数据同步/入库时启动、完成后停止(不影响自启与日常体验)。不移除 disableAutomaticTermination(FinDataHubApp.swift:27-28),而是通过「Timer 全量加 leeway + 空闲暂停轮询」让 macOS 的 App Nap 能挂起常驻 App —— 进程在,但功耗趋近于 0。
以下改动已写入源码并通过编译验证(swift build --disable-sandbox 通过;Python 逻辑 10/10 单测通过)。自启动功能全部保留。
| 任务 | 状态 | 修改文件 | 关键改动 |
|---|---|---|---|
| P0-1 Ollama 模型空闲卸载 | ✅ 已完成 | Scripts/bin/configure_ollama_keepalive.sh(新建)、DependencyManifest.swift | 注入 OLLAMA_KEEP_ALIVE=60 到 brew / Ollama.app plist;安装流程自动执行 |
| P0-2 采集器交易时段自适应 | ✅ 已完成 | Scripts/bin/newsflash_collect.py | adaptive_sleep_seconds():交易时段全速 / 盘后 4 倍降频 / 深夜周末 1 小时 1 次;进程不退出 |
| P0-3 健康检查免 spawn + 降频 | ✅ 已完成 | NewsFlashService.swift、AppConstants.swift、newsflash_collect.py | probeProcessStatus() 读 PID + kill(pid,0) + 心跳文件,替代每 30s spawn 2 个 Python;间隔 30s→60s |
| P0-4 电源感知省电模式 | ✅ 已完成 | PowerSaver.swift(新建)、NewsFlashService.swift | 监听电源状态:电池供电时健康检查 60s→120s、通知轮询≥15 分钟;插电自动恢复 |
| P1-5 Timer leeway(App Nap) | ✅ 已完成 | ServiceMonitor.swift、NewsFlashService.swift、SchedulerService.swift、NewsFlashView.swift | 5 处 Timer 加 tolerance/leeway,允许系统合并唤醒,常驻 App 可被 App Nap 挂起 |
| P1-6 视图 1s 计时器降频 | ✅ 已完成 | SyncProgressView.swift、AkShareView.swift、TuShareView.swift | Timer.publish(1s) → TimelineView(.periodic),窗口关闭系统自动停止 tick |
| P2-7 采集器 launchd 批处理 | ✅ 已完成 | install_collector_launchd.py(新建)、uninstall_collector_launchd.py(新建) | RunAtLoad 保留自启;交易时段每 5 分钟 / 盘后 30 分钟 / 深夜周末每小时跑一次 once,进程跑完即退 |
| P2-8 MySQL 按需启停 + App Nap | ✅ 已完成 | Scripts/bin/mysql_toggle.sh(新建) | MySQL 启停工具(brew / mysql.server);App Nap 已由 P1-5 + P0-4 覆盖 |
python3 Scripts/bin/install_collector_launchd.py / uninstall_collector_launchd.py;③ Ollama 配置脚本可随时重跑(幂等)。分阶段落地后的累计收益估算(移动办公 / 电池供电场景)。
预估依据:Ollama 35B 模型常驻是单项最大能耗源,通过 OLLAMA_KEEP_ALIVE 空闲卸载即可带来最显著改善(自启保留,仅模型不常驻);其次每 30s 的 Python 子进程 spawn 是持续 CPU 抖动来源;采集器 7×24 高频抓取是网络射频唤醒来源。三者治理后,FinDataHub 自启与功能完全不变,但运行模式从「全天候满血」变为「交易时段全速 + 空闲/移动自动降功耗」。