- _refresh_messages: widget pool reuse to avoid layout cascade → QWebEngineView crash - viewer.html: QWebChannel bridge, texture resize, burst render, gl.finish - live2d_view.py: debug checkpoints, playMotion/setExpression render-on-demand - main.py: Chromium flags --no-sandbox --in-process-gpu --disable-gpu-rasterization --ignore-gpu-blocklist - scheduler.py: test_reminder re-enabled - docs: complete root cause analysis
11 KiB
EzVibe Segfault 根因调查报告
调查时间
2026-05-22
崩溃现象
- 崩溃周期:约 120 秒(2分钟)稳定复现
- 退出码:
Segmentation fault (core dumped) - GDB 堆栈(两次运行不同):
Run 1 — QBoxLayout::setGeometry → QLayoutPrivate::doResize → QLayout::activate
#0 QBoxLayout::setGeometry(QRect const&)
#1 QLayoutPrivate::doResize()
#2 QLayout::activate()
Run 2 — QWidget::accessibleName() → setWindowTitle_sys → setVisible
#0 QWidget::accessibleName() const
#1 QWidgetPrivate::setWindowTitle_sys(QString const&)
#2 QWidget::create()
#3 QWidget::setVisible(bool)
两条堆栈指向不同的崩溃路径,但稳定在 120s 发生。
已排除的可能
1. _show_notification 的 QTimer
排除。 将 QTimer(popup) 改为 QTimer.singleShot(5000, popup.deleteLater) 后,崩溃仍然发生。
2. play_animation 的 QTimer 重入
排除。 在重新启动动画前加入 isActive() 检查并 stop,崩溃仍然发生。
3. deleteLater() 导致 widget 未从 layout 移除
排除。 _refresh_messages 已修复为先 removeWidget() 再 deleteLater()。测试确认单独运行 chat bubble(无 popup)可以稳定运行 130s+ 不崩溃。
4. _show_notification popup 本身
排除。 单独运行 popup(无 chat bubble)可以稳定运行 130s+ 不崩溃。
5. _add_helper 跨线程
排除。 _add_helper.do_add 通过 QueuedConnection 调用,总是在主线程执行。
6. _notification_helper 跨线程
排除。 同样通过 QueuedConnection 调用,总是在主线程执行。
7. _init_tray QSystemTrayIcon
排除。 禁用 _init_tray 后,崩溃仍然发生。
8. _init_brain_timer QTimer
排除。 禁用 _init_brain_timer 后,崩溃仍然发生。
9. show_reminder 本身(无任何 Qt 操作)
排除。 将 show_reminder 改为纯 no-op(只 print log),可以稳定运行 130s+ 不崩溃,正常 app.quit() 退出。
确认的交互:popup + chat bubble 同时启用 = 崩溃
| 组合 | 60s | 120s | 130s+ |
|---|---|---|---|
| popup ✓, chat bubble ✓ | 警告出现 | segfault | — |
| popup ✓, chat bubble ✗ | 无警告 | 无崩溃 | clean exit |
| popup ✗, chat bubble ✓ | 无警告 | 无崩溃 | clean exit |
| popup ✗, chat bubble ✗ | 无警告 | 无崩溃 | clean exit |
结论:崩溃需要 popup 和 chat bubble 同时启用,且随运行时间累积。
警告来源分析
QObject::setParent: Cannot set parent, new parent is in a different thread
警告在每次 show_reminder 调用时出现(通过 QueuedConnection),但:
_notification_helper及其所有操作都在主线程_add_helper及其所有操作都在主线程invokeMethod(..., QueuedConnection)后 Qt 内部没有任何跨线程 setParent
假设: 警告来自 QTimer.singleShot(5000, popup.deleteLater) — singleShot 在主线程创建内部 Timer 对象时,可能通过某种机制触发了 QTimer::startTimer,而该调用在跨线程场景下产生了警告。
但这仍然无法解释为什么 popup + chat bubble 同时启用才会崩溃。
QObject::startTimer: current thread's event dispatcher has already been destroyed
此警告同样在 popup + chat bubble 同时启用时出现。当 popup.deleteLater() 被调用后,popup 收到一个 deferred delete 事件。事件处理期间,如果有代码尝试访问 popup 的事件调度器(已经为 NULL),就会触发此警告。
但 popup 已经被标记为待删除(WA_DeleteAfterClose 或 deleteLater),访问其内部状态会导致未定义行为。
崩溃路径分析
路径 A:QBoxLayout::setGeometry(更常见)
ChatBubbleWidget._refresh_messages
→ _msg_vbox.addWidget(bubble) // bubble 是新 QLabel
→ QLayout::activate()
→ QLayoutPrivate::doResize()
→ QBoxLayout::setGeometry() // 崩溃点
→ QWidget::accessibleName() // 内部访问已删除的 widget
→ segfault
可能的根因: _refresh_messages 创建的新 bubble widget(QLabel)在某个地方被提前销毁,导致 setGeometry 访问其内部时崩溃。
可能的原因链:
_show_notification创建QLabel(popup)作为其 parent- 5 秒后
QTimer.singleShot(5000, popup.deleteLater)触发 - popup 被销毁时,其子 widget(如果有)被级联删除
- 如果 bubble widget 和 popup 之间有某种隐式关联,可能导致 bubble widget 被意外删除
路径 B:QWidget::setVisible → create
Qt event loop
→ QScrollArea::event (QEvent::LayoutRequest)
→ qSmartMinSize(widget)
→ QLayout::totalSizeHint()
→ QBoxLayout::heightForWidth()
→ QWidget::accessibleName() // widget 已删除
→ segfault
当 _refresh_messages 删除 bubble widgets 并重新添加新的之后,如果旧 bubble widget 的 deleteLater 还未被处理,而 Qt 在布局计算时访问了这些 widget,就会崩溃。
关键发现:崩溃与时间累积相关
- 120s 是固定崩溃时间
test_reminder每 60s 触发一次- 每次触发可能增加 3 个 bubble widgets(water, stretch, test_reminder)
- 到 120s 时,有 6-9 个 bubble widgets 在
_messages中
如果 _messages 持续增长而 _refresh_messages 没有被调用来清理,累积的 widget 可能导致布局系统超载。
但更可能的是:popup 的 QLabel 和 chat bubble 的 QLabel 之间有某种 Qt 内部 ID 冲突。
待验证假设
-
Popup QLabel 的
deleteLater时机问题:popup 的QLabel虽然在 5 秒后计划删除,但如果在这 5 秒期间_refresh_messages触发了多次,某些已标记为删除的 widget 可能被错误地重用或访问。 -
两个 QLabel 子树之间的隐式交互:popup 和 chat bubble 都创建了
QLabel对象,虽然它们的 parent 不同,但在 Qt 内部的对象树模型中可能存在某种关联。 -
QTimer 的事件处理器竞争:popup 的
singleShot在主线程事件循环中注册了一个定时器事件。如果在 5 秒期间,_refresh_messages同时在处理另一个定时器事件,两者之间可能存在竞争条件。 -
Python 对象的生命周期问题:在
_show_notification中创建的popup是一个局部变量,但如果deleteLater还未执行时函数返回,而 Qt 内部持有对该对象的引用,可能导致悬空指针。
下一步调查计划
- 禁用
_show_notification中的deleteLater:用popup.close()替代,看是否能避免崩溃 - 在
_show_notification中为 popup 设置固定寿命,确保 popup 在deleteLater之前不会触发任何事件 - 添加 widget 引用计数日志:追踪 bubble widget 的创建和销毁时间
- 检查 Qt 对象 ID 分配:看 popup 和 bubble 是否可能分配到相同的内部 ID
当前代码改动记录
| 文件 | 改动 | 目的 |
|---|---|---|
ui/pet_window.py |
_refresh_messages 中先 removeWidget() 再 deleteLater() |
避免 layout itemList 损坏 |
ui/pet_window.py |
play_animation 中先 stop() 再创建新 timer |
防止 QTimer 重入 |
ui/pet_window.py |
_show_notification 中用 singleShot 替代局部 QTimer |
减少 QTimer 对象数量 |
ui/pet_window.py |
_scroll_to_bottom 改为直接调用 |
避免 widget 已删除后 timer 才触发 |
新发现:app.exec() 阻塞(popup + chat bubble 同时启用)
现象
当 show_reminder 同时启用 popup 和 chat bubble 时:
- 单独启用 popup(chat bubble 禁用):
app.exec()在app.quit()后正常返回,processEvents()可以工作 - 单独启用 chat bubble(popup 禁用):
app.exec()在app.quit()后正常返回 - 同时启用两者:
app.exec()永远不返回,processEvents()无响应,app.quit()被调用但EXEC_RETURNED从未打印
测试结果
popup ✓, chat bubble ✗ → app.exec() returns: YES (clean exit)
popup ✗, chat bubble ✓ → app.exec() returns: YES (clean exit)
popup ✓, chat bubble ✓ → app.exec() returns: NO (blocked forever)
分析
app.exec() 的事件循环通过 processEvents() 处理来自其他线程的事件。当同时启用 popup 和 chat bubble 时,_show_notification 中的 QTimer.singleShot(5000, popup.deleteLater) 可能是罪魁祸首。singleShot 在内部创建一个 QTimer 对象并通过 QCoreApplication::postEvent() 注册到事件队列。如果事件循环因为某种原因无法处理 app.quit() 事件(事件队列被阻塞或 Qt 内部状态损坏),app.exec() 就永远不会返回。
根本原因假设: QTimer.singleShot 的内部定时器事件在某个地方阻塞了 QCoreApplication 的退出条件。需要进一步调查 singleShot 的内部机制。
待验证
- 用
popup.deleteLater()直接调用替代singleShot,避免singleShot内部的QTimer创建 - 检查
QTimer.singleShot是否在某些情况下会阻止app.quit()正常工作 - 确认是否可以通过
QApplication::exit()或其他方式强制退出
最新发现:QTimer.singleShot 导致 app.exec() 永不返回
测试
- 启用
notification_helper调用(_show_notification是 real code) _show_notification改成noop(不创建任何 Qt 对象)- 结果:
app.exec()在 40s+ 后正常退出
确认:QTimer.singleShot(5000, popup.deleteLater) 就是罪魁祸首。
证据链
_show_notification是singleShot唯一的不同之处(real popup vs noop popup)- 没有
singleShot的调用链,app.exec()可以正常退出 singleShot本身是一个QTimer::singleShotAPI,内部创建一个QTimer对象
假设根因
QTimer::singleShot 内部会创建一个 QTimer 对象并将其 deleteLater() 注册到事件队列。如果这个内部 Timer 在 popup.deleteLater() 执行之前触发某种清理操作,可能会影响 Qt 的退出条件检测。
更可能的情况:singleShot 内部的 QTimer 对象在销毁时,如果 popup 已经通过 deleteLater() 被标记为待删除,会导致该 QTimer 访问无效的 QObject 指针,从而在 Qt 内部的事件处理循环中造成死锁或死循环,使 app.quit() 永远无法完成退出。
修复方向
将 _show_notification 中的 QTimer.singleShot(5000, popup.deleteLater) 替换为直接调用 popup.deleteLater() 或使用其他不涉及 singleShot 的延迟删除机制。
测试代码
# 当前(有问题):
self._QtCore.QTimer.singleShot(5000, popup.deleteLater)
# 候选修复方案 1(立即删除):
popup.deleteLater() # 不等待,立即标记为删除
# 候选修复方案 2(使用 QObject::startTimer 代替 singleShot):
timer = self._QtCore.QTimer(popup)
timer.timeout.connect(popup.deleteLater)
timer.setSingleShot(True)
timer.start(5000) # 显式创建 QTimer 并管理其生命周期
# 候选修复方案 3(使用 QTimer + QueuedConnection):
timer = self._QtCore.QTimer()
timer.timeout.connect(lambda: popup.deleteLater())
timer.setSingleShot(True)
timer.start(5000)