操作系统
macOS
系统版本
Darwin Kernel Version 25.5.0
安装类型
sing-box for macOS 图形客户端程序
如果您使用图形客户端程序,请提供该程序版本。
版本1.14.0-beta.7 (1)
版本
描述
摘要
SFM 出现永久性无响应(转圈)。macOS spindump hang 报告显示:主线程以约 91% CPU 占用卡在 TextKit 2 布局中(-[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:...]),并且被每一次 CoreAnimation 显示周期重复触发。采样时进程已无响应 11.4 小时,内存 footprint 达 3661 MB。
这不是崩溃 —— 进程仍然存活,只是彻底卡死。
环境
| 项 |
值 |
| 应用 |
SFM 1.14.0-beta.7 (1),io.nekohasekai.sfavt.standalone |
| 路径 |
/Applications/SFM.app/Contents/MacOS/SFM |
| macOS |
26.5.2 (Build 25F84) |
| 硬件 |
Mac16,5 (arm64e),14 核,36 GB 内存 |
| 报告类型 |
spindump,Event: hang,Report Version 72 |
| Incident ID |
2699724E-FF28-4B2F-8479-6EDA44F1940A |
| 语言环境 |
zh-Hans-CN |
报告中的关键指标
| 指标 |
值 |
Event |
hang |
Duration |
41116.29 s |
| 采样前已无响应时长 |
41115 s(约 11.4 小时) |
| 采样窗口 |
1.10 s / 11 次采样,间隔 100 ms |
| 采样窗口内主线程 CPU |
0.999 s / 1.10 s(约 91%) —— 在真跑 CPU,不是阻塞 |
| 进程 footprint |
3661.00 MB |
Time Since Fork |
119522 s(约 33 小时) |
| Jetsam 优先级 / 内存上限 |
100 / 0 MB(无上限) |
| 系统内存压力 |
无(ThermalPressure -> 0,724 GB 空闲磁盘,整份报告无任何 jetsam kill) |
| 线程数 |
26(另有 3 个空闲 work queue 线程被省略) |
即:这是主线程用户态 CPU 密集型死循环,不是死锁、不是锁竞争、不是 I/O 等待,也与内存压力无关。
主线程调用栈(11/11 次采样完全一致)
11 start + 6992 (dyld)
11 ??? (SFM + 9956) ← 整条栈中唯一的 SFM 帧(main,未符号化)
11 static App.main() (SwiftUI)
11 runApp<A>(_:) (SwiftUI)
11 specialized runApp(_:) (SwiftUI)
11 NSApplicationMain (AppKit)
11 -[NSApplication run]
11 -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]
11 _DPSNextEvent
11 _DPSBlockUntilNextEventMatchingListInMode
11 ReceiveNextEventCommon (HIToolbox)
11 RunCurrentEventLoopInMode
11 __CFRunLoopRun
11 __CFRunLoopDoSource1
11 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
11 __CFMachPortPerform
11 UC::DriverCore::continueProcessing() (UpdateCycle)
11 stepTransactionFlush (AppKit)
11 CA::Transaction::flush_as_runloop_observer(bool) (QuartzCore)
11 CA::Transaction::commit()
11 CA::Transaction::run_commit_handlers(CATransactionPhase)
11 NSDisplayCycleFlush
11 NSDisplayCycleObserverInvoke
11 __NSWindowGetDisplayCycleObserverForLayout_block_invoke
11 -[NSWindow(NSConstraintBasedLayoutInternal) layoutIfNeeded]
11 -[NSWindow(NSConstraintBasedLayoutInternal) _layoutViewTree]
11 -[NSView layoutSubtreeIfNeeded]
11 -[NSView _layoutSubtreeIfNeededAndAllowTemporaryEngine:]
11 -[NSView _layoutSubtreeWithOldSize:] ┐
...(_layoutSubtreeWithOldSize: 共 12 层嵌套) │ 视图层级很深
11 -[NSView _layoutSubtreeWithOldSize:] ┘
11 _NSViewLayout
11 ___NSViewLayout_block_invoke
11 ??? (AppKit + 5414264)
11 ??? (AppKit + 5412652)
11 ??? (AppKit + 5653696)
11 ??? (AppKit + 5653656)
11 -[NSTextView layout] + 496
11 -[NSTextViewportLayoutController layoutViewport] + 828
11 -[NSTextView locationForPositionInViewport:offset:] + 192
11 -[NSTextLayoutManager textLayoutFragmentForPosition:inTextContainerAtIndex:avoidsAdditionalLayout:] + 208
11 -[NSTextLayoutManager ensureLayoutForBounds:] + 196
8 -[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:originLocation:originOffset:deltaFromOffset:] + 1556 ← 8/11 次采样,偏移量相同
8 -[_NSTextLayoutFragmentStorage enumerateTextLayoutFragmentFromLocation:options:usingBlock:] + 216
6 -[_NSTextRunStorage enumerateObjectsFromLocation:options:usingBlock:] + 220
5 -[_NSTextRunStorage _performCountableRunStorageOperation:] + 84
5 __69-[_NSTextRunStorage enumerateObjectsFromLocation:options:usingBlock:]_block_invoke.26 + 328
2 -[NSCountableTextLocation initWithIndex:] + 120
2 -[NSConcreteHashTable getItem:] (Foundation)
2 hashProbe + 324 (Foundation)
2 objc_loadWeak (libobjc.A.dylib)
1 objc_loadWeakRetained + 96
调用栈解读
-[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:...] 的作用是把垂直滚动偏移量反查为文本位置,其实现方式是线性枚举 layout fragment storage。
- 11 次采样中有 8 次停在该函数同一条指令偏移(
+1556),且始终位于枚举回调内部。说明该枚举在一个采样间隔内无法收敛。
- 每次迭代都要分配一个
NSCountableTextLocation,并执行一次弱引用哈希查找(objc_loadWeak → hashProbe)。单次迭代成本很高,而它被执行了极大量次数。
- 整条路径挂在
CA::Transaction::commit() 下,意味着它会被每一次显示周期重新触发 —— 应用永远无法完成一帧绘制,因此表现为永久转圈。
- 3661 MB 的 footprint 与「单个
NSTextView / NSTextContentStorage 中持有了海量文本」这一形态高度一致。
这份报告无法说明的事
SFM 二进制在整条栈中只贡献了一帧:??? (SFM + 9956),即 main。整条挂起路径 100% 位于 AppKit / UIFoundation 内部,且应用二进制在本报告中未符号化。
因此仅凭这份日志无法定位到 SFM 源码的任何具体位置。 要归因到某个具体视图,需要 dSYM 符号化的捕获,或带复现步骤的 Instruments Time Profiler 追踪。
未经证实的推断
涉事文本视图最可能是应用内的日志面板,依据是:(a) 3.6 GB 的 footprint;(b) 33 小时运行时长下的无界增长;(c) 它是 SFM 中唯一有可能持有该量级文本的 UI。这是基于 footprint 与应用形态的推断,并非来自调用栈的证据。如果 SFM 还存在其他大文本视图(例如配置编辑器),其嫌疑同等成立。
已排除的无关项
- 进程内存在 WebKit 线程(
WebCore: Scrolling、Log work queue → JavaScriptCore),但全部停在 semaphore_wait_trap,最后运行时间为约 47276 秒前。与本问题无关。
- 子进程
RootHelper [47390] 正常阻塞于 read() → lck_mtx_sleep。无关。
MTLCompilerService(responsible process 为 SFM)空闲于 __sigsuspend_nocancel。无关。
修复建议
- 给日志缓冲区加上限。 日志视图必须限制保留的内容量(例如仅保留最近 N 行 / M 字节),而不是无界增长。这是唯一能彻底消除该失效模式的改动。
- 重新考虑日志视图的实现方式。 单个巨型
NSTextView 在 TextKit 2 下于该量级会严重退化。改为按行渲染(NSTableView / SwiftUI List,每条日志一行,或 LazyVStack)可完全绕开 NSTextLayoutManager 的视口位置估算。
- 降低视图嵌套深度。 文本视图外围有 12 层
_layoutSubtreeWithOldSize: 嵌套,会成倍放大每一次布局的开销。
受影响用户的临时规避方案
- 关闭 / 禁用应用内日志面板。
- 降低日志级别,减少文本累积量。
- 长时间使用时定期重启 SFM。
重现方式
运行SFM GUI
日志
123.log.zip
支持我们
完整性要求
操作系统
macOS
系统版本
Darwin Kernel Version 25.5.0
安装类型
sing-box for macOS 图形客户端程序
如果您使用图形客户端程序,请提供该程序版本。
版本1.14.0-beta.7 (1)
版本
描述
摘要
SFM 出现永久性无响应(转圈)。macOS
spindumphang 报告显示:主线程以约 91% CPU 占用卡在 TextKit 2 布局中(-[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:...]),并且被每一次 CoreAnimation 显示周期重复触发。采样时进程已无响应 11.4 小时,内存 footprint 达 3661 MB。这不是崩溃 —— 进程仍然存活,只是彻底卡死。
环境
io.nekohasekai.sfavt.standalone/Applications/SFM.app/Contents/MacOS/SFMEvent: hang,Report Version 722699724E-FF28-4B2F-8479-6EDA44F1940A报告中的关键指标
EventhangDurationTime Since ForkThermalPressure -> 0,724 GB 空闲磁盘,整份报告无任何 jetsam kill)即:这是主线程用户态 CPU 密集型死循环,不是死锁、不是锁竞争、不是 I/O 等待,也与内存压力无关。
主线程调用栈(11/11 次采样完全一致)
调用栈解读
-[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:...]的作用是把垂直滚动偏移量反查为文本位置,其实现方式是线性枚举 layout fragment storage。+1556),且始终位于枚举回调内部。说明该枚举在一个采样间隔内无法收敛。NSCountableTextLocation,并执行一次弱引用哈希查找(objc_loadWeak→hashProbe)。单次迭代成本很高,而它被执行了极大量次数。CA::Transaction::commit()下,意味着它会被每一次显示周期重新触发 —— 应用永远无法完成一帧绘制,因此表现为永久转圈。NSTextView/NSTextContentStorage中持有了海量文本」这一形态高度一致。这份报告无法说明的事
SFM 二进制在整条栈中只贡献了一帧:
??? (SFM + 9956),即main。整条挂起路径 100% 位于 AppKit / UIFoundation 内部,且应用二进制在本报告中未符号化。因此仅凭这份日志无法定位到 SFM 源码的任何具体位置。 要归因到某个具体视图,需要 dSYM 符号化的捕获,或带复现步骤的 Instruments Time Profiler 追踪。
未经证实的推断
涉事文本视图最可能是应用内的日志面板,依据是:(a) 3.6 GB 的 footprint;(b) 33 小时运行时长下的无界增长;(c) 它是 SFM 中唯一有可能持有该量级文本的 UI。这是基于 footprint 与应用形态的推断,并非来自调用栈的证据。如果 SFM 还存在其他大文本视图(例如配置编辑器),其嫌疑同等成立。
已排除的无关项
WebCore: Scrolling、Log work queue→ JavaScriptCore),但全部停在semaphore_wait_trap,最后运行时间为约 47276 秒前。与本问题无关。RootHelper [47390]正常阻塞于read()→lck_mtx_sleep。无关。MTLCompilerService(responsible process 为 SFM)空闲于__sigsuspend_nocancel。无关。修复建议
NSTextView在 TextKit 2 下于该量级会严重退化。改为按行渲染(NSTableView/ SwiftUIList,每条日志一行,或LazyVStack)可完全绕开NSTextLayoutManager的视口位置估算。_layoutSubtreeWithOldSize:嵌套,会成倍放大每一次布局的开销。受影响用户的临时规避方案
重现方式
运行SFM GUI
日志
123.log.zip
支持我们
完整性要求