Files
EzVibe/docs/investigation/segfault-root-cause-final.md
e2hang 710bc84c3f fix: segfault root cause — QThread.currentThread() thread affinity check
Root cause: add_message() used QApplication.instance().thread() to
check thread affinity, but this always returns the main thread (same
as container.thread()), so the check was always False. Qt widget
operations (deleteLater, insertWidget, setHtml) were executed from
the scheduler background thread, corrupting internal state.

Fix: use QThread.currentThread() to compare against the actual
executing thread.

Also: replace QScrollArea+QLabel with QTextBrowser for message
display, eliminating widget management entirely.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 13:57:42 +08:00

5.6 KiB
Raw Permalink Blame History

EzVibe Segfault — 完整根因分析与最终修复

日期: 2026-05-23 状态: 已修复


一、崩溃现象

多种崩溃表现,随时间演变:

阶段 表现 时机
初始 GPU state invalid after WaitForGetOffsetInRange + segfault Live2D 纹理加载后立即崩溃
Phase 2 QObject::setParent 警告 (x23) + segfault first_frame_ok 后 ~20-30 秒
Phase 3 QObject::setParent 警告 (x12) + startTimer 警告 + segfault first_frame_ok 后 ~20-30 秒
Phase 4 Cannot create children for parent in different thread + segfault 消息到达时立即崩溃

每次崩溃表现不同,但都发生在 scheduler/brain timer 触发 show_reminder_refresh_messages 时。


二、诊断过程

阶段 1GPU 命令缓冲区损坏

  • 添加 --no-sandbox --in-process-gpu Chromium flags
  • 结果GPU 错误消失,但 segfault 仍在(延迟出现)

阶段 2连续 WebGL 渲染循环

  • setInterval(100ms) 连续渲染改为 burst 模式
  • 结果:setParent 警告从 23 减少到 12首帧渲染成功

阶段 3QWebChannel 调试探针

  • 引入 qwebchannel.js + [JS→PY] checkpoint 消息
  • 定位Live2D 初始化完全成功WebGL, Shader, Model, Textures, Buffers, First Frame 全部通过)
  • 崩溃发生在 first_frame_ok 后 20-30 秒

阶段 4隔离测试

  • 禁用 brain timer + test_reminder → 稳定运行 2 分钟+
  • 确认:崩溃触发点为 show_reminder_refresh_messages

阶段 5QTextBrowser 揭示真相

  • 将 QScrollArea+QLabel 替换为 QTextBrowser
  • 崩溃变为:Cannot create children for a parent that is in a different thread
  • 关键线索QTextDocument 在主线程,但 setHtml() 从其他线程调用

三、根因

一行代码的 Bug

ui/pet_window.py 第 284 行(修复前):

# ❌ bug: QApplication.instance().thread() 和 container.thread() 都是主线程
if self._QtWidgets.QApplication.instance().thread() != self._container.thread():

QApplication.instance().thread() 返回主线程。self._container.thread() 也返回主线程。两者是同一个 QThread 对象,比较结果永远为 False。

导致:从 scheduler 后台线程调用 _do_add_message_refresh_messages在非主线程执行 Qt Widget 操作

崩溃机制

scheduler 线程 (ezvibe-scheduler)
  → show_reminder(message)
    → add_message(text)
      → 线程检查: main_thread != main_thread → False ✗
      → _do_add_message(text) ← 在 scheduler 线程执行!
        → _refresh_messages()
          → deleteLater() / insertWidget() / setHtml()
            → 跨线程修改 Qt Widget 树
            → 内部状态损坏
            → SIGSEGV

为什么症状多样化

  • deleteLater() 跨线程 → QObject::setParent 警告 + 延迟 segfault
  • insertWidget() 跨线程 → 布局级联失效 + QBoxLayout::setGeometry crash
  • setHtml() 跨线程 → QTextDocument Cannot create children + 立即 crash
  • 同一时刻的 Qt 内部状态不同 → 每次崩溃栈不同

为什么 Live2D/WebEngine 似乎是个因素

  • 禁用 Live2D 时QWebEngineView 不存在 → 控件树更简单 → 跨线程损坏不致命
  • 启用 Live2D 时QWebEngineView 的 Chromium compositor 持有 GPU 资源 → 控件树损坏触发 compositor 访问无效指针 → segfault
  • Live2D 放大了症状,但不是根因

四、修复

一行修复

# ✅ 正确:比较当前执行线程与 container 所在线程
if self._QtCore.QThread.currentThread() != self._container.thread():

QThread.currentThread() 返回当前正在执行代码的线程,而非 QApplication 所在的线程。

辅助优化

修改 文件 作用
QScrollArea+QLabel → QTextBrowser pet_window.py setHtml() 替代控件管理,消除 deleteLater/insertWidget
纹理缩放 1024×1024, 无 mipmap viewer.html GPU 内存 128MB→8MB消除 GPU 内存压力
QWebChannel 集成 + 调试探针 viewer.html, live2d_view.py JS→PY 消息追踪
Chromium flags main.py --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist
托盘图标 pet_window.py 32×32 蓝色 pixmap修复 "No Icon set"

五、变更文件

文件 关键修改
ui/pet_window.py QThread.currentThread() 修复QTextBrowser 替代 QScrollArea
assets/live2d/march7/viewer.html QWebChannel纹理缩放burst 渲染gl.finish
ui/live2d_view.py onJsMessage 调试playMotion/setExpression 按需渲染
main.py Chromium flags
agent/scheduler.py test_reminder 恢复启用

六、经验教训

  1. QApplication.instance().thread()QThread.currentThread()。前者返回 QApplication 所在的线程(始终是主线程),后者返回当前执行代码的线程。对于线程安全判断,必须使用后者。

  2. QTextBrowser 比 QScrollArea + QLabel 更适合消息列表。QTextBrowser 内部管理内容渲染,不需要创建/删除 QWidget 子控件,避免了 layout cascade 问题。

  3. GPU/WebGL 崩溃可能是症状而非根因。当 Qt 控件树因跨线程操作而损坏时,任何涉及控件树遍历的操作(包括 WebEngine compositor都可能触发崩溃。

  4. 调试探针比猜测更有效。QWebChannel checkpoints 精准定位了崩溃发生在 first_frame_ok 之后,而 QTextBrowser 的显式报错最终揭示了线程问题。