Skip to content

MacOS SFM 永久无响应挂起 #4396

Description

@XhstormR

操作系统

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

调用栈解读

  1. -[NSTextLayoutManager _estimatedTextLocationForVerticalOffset:...] 的作用是把垂直滚动偏移量反查为文本位置,其实现方式是线性枚举 layout fragment storage
  2. 11 次采样中有 8 次停在该函数同一条指令偏移+1556),且始终位于枚举回调内部。说明该枚举在一个采样间隔内无法收敛。
  3. 每次迭代都要分配一个 NSCountableTextLocation,并执行一次弱引用哈希查找(objc_loadWeakhashProbe)。单次迭代成本很高,而它被执行了极大量次数。
  4. 整条路径挂在 CA::Transaction::commit() 下,意味着它会被每一次显示周期重新触发 —— 应用永远无法完成一帧绘制,因此表现为永久转圈。
  5. 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: ScrollingLog work queue → JavaScriptCore),但全部停在 semaphore_wait_trap,最后运行时间为约 47276 秒前。与本问题无关。
  • 子进程 RootHelper [47390] 正常阻塞于 read()lck_mtx_sleep。无关。
  • MTLCompilerService(responsible process 为 SFM)空闲于 __sigsuspend_nocancel。无关。

修复建议

  1. 给日志缓冲区加上限。 日志视图必须限制保留的内容量(例如仅保留最近 N 行 / M 字节),而不是无界增长。这是唯一能彻底消除该失效模式的改动。
  2. 重新考虑日志视图的实现方式。 单个巨型 NSTextView 在 TextKit 2 下于该量级会严重退化。改为按行渲染(NSTableView / SwiftUI List,每条日志一行,或 LazyVStack)可完全绕开 NSTextLayoutManager 的视口位置估算。
  3. 降低视图嵌套深度。 文本视图外围有 12 层 _layoutSubtreeWithOldSize: 嵌套,会成倍放大每一次布局的开销。

受影响用户的临时规避方案

  • 关闭 / 禁用应用内日志面板。
  • 降低日志级别,减少文本累积量。
  • 长时间使用时定期重启 SFM。

重现方式

运行SFM GUI

日志

123.log.zip

支持我们

完整性要求

  • 我保证阅读了文档,了解所有我编写的配置文件项的含义,而不是大量堆砌看似有用的选项或默认值。
  • 我保证提供了可以在本地重现该问题的服务器、客户端配置文件与流程,而不是一个脱敏的复杂客户端配置文件。
  • 我保证提供了可用于重现我报告的错误的最简配置,而不是依赖远程服务器、TUN、图形界面客户端或者其他闭源软件。
  • 我保证提供了完整的配置文件与日志,而不是出于对自身智力的自信而仅提供了部分认为有用的部分。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions