Files
lucky-cat/docs/knowledge vault/bugfix-cloud-chouqian-system-event-stack-overflow.zh-CN.md
T

258 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 云端抽签触发系统事件任务栈溢出:问题与修复说明
## 1. 对应提交
```text
d386de2 fix(multimodal): prepare session for cloud fortune draw
```
本文说明首次抽签能够自动创建多模态会话后,按下 SW2 导致 `cp_system_event` 任务栈溢出的问题,以及最终采用的异步 worker 方案。
## 2. 问题现象
修复“首次抽签缺少会话”后,设备按下 SW2 可以进入 `Start` 发送,但随即触发 FreeRTOS 栈保护:
```text
control_plane_mm_rt: Cloud chouqian request queued
cp_mm_session: Start sent (...)
***ERROR*** A stack overflow in task cp_system_event has been detected.
```
回溯显示:
```text
panic_abort
esp_system_abort
vApplicationStackOverflowHook
vTaskSwitchContext
```
栈已经被破坏,因此回溯无法继续还原完整的业务调用链,但任务名明确指向 `cp_system_event`。
## 3. 问题原因
### 3.1 系统事件任务栈较小
系统事件任务配置为 4096 字节:
```c
#ifndef CONFIG_TQ_SYSTEM_EVENT_TASK_STACK
#define CONFIG_TQ_SYSTEM_EVENT_TASK_STACK 4096
#endif
xTaskCreatePinnedToCore(control_plane_system_event_task,
"cp_system_event",
CONFIG_TQ_SYSTEM_EVENT_TASK_STACK,
NULL,
4,
&s_system_event_task,
CONTROL_PLANE_SYSTEM_EVENT_TASK_CORE);
```
该任务原本只负责读取事件队列、识别按键类型并分发轻量控制命令,不适合承担协议报文构造和同步网络发送。
### 3.2 首版修复在按键调用栈内执行了 Start
首版会话准备逻辑在登记请求后立即调用推进函数:
```c
ESP_LOGI(TAG, "Cloud chouqian request queued");
control_plane_try_cloud_chouqian_request();
return ESP_OK;
```
实际调用链变成:
```text
cp_system_event
-> control_plane_dispatch_key_event
-> control_plane_request_cloud_chouqian
-> control_plane_try_cloud_chouqian_request
-> control_plane_multimodal_send_start
-> 创建 cJSON 协议对象
-> 序列化 Start JSON
-> WebSocket 文本发送与日志格式化
```
`Start` 包含音频、下行、voice、client info、workspace 和 app 等参数。虽然 JSON 节点和序列化文本主要使用 heap,但多层函数调用、局部结构体、格式化和网络发送仍会消耗任务栈,最终超过 `cp_system_event` 的 4096 字节限制。
### 3.3 单纯扩大系统事件栈不是合适的职责划分
直接扩大 `cp_system_event` 栈可以缓解当前崩溃,但仍有以下问题:
- 所有按键和系统事件都被迫为少数网络操作长期预留大栈;
- WebSocket 发送可能等待锁或网络超时,阻塞其他系统事件;
- 以后 Start 协议字段增加后仍可能再次逼近栈上限;
- 事件分发任务承担了不属于它的会话编排职责。
因此最终方案是迁移执行上下文,而不是只增加系统事件任务的栈。
## 4. 解决方式
### 4.1 按键任务只登记状态并通知 worker
最终实现不再从 `control_plane_request_cloud_chouqian()` 直接执行 Start 或 Prompt:
```c
esp_err_t control_plane_request_cloud_chouqian(void)
{
uint32_t requested_ms = control_plane_now_ms();
portENTER_CRITICAL(&s_multimodal_runtime_lock);
if (s_cloud_chouqian_pending) {
portEXIT_CRITICAL(&s_multimodal_runtime_lock);
return ESP_ERR_INVALID_STATE;
}
s_cloud_chouqian_pending = true;
s_cloud_chouqian_action_in_progress = false;
s_cloud_chouqian_prompt_sent = false;
s_cloud_chouqian_requested_ms = requested_ms;
s_model_command_dispatched = false;
portEXIT_CRITICAL(&s_multimodal_runtime_lock);
ESP_LOGI(TAG, "Cloud chouqian request queued");
control_plane_wake_multimodal_watchdog();
return ESP_OK;
}
```
此时 `cp_system_event` 只执行:
```text
写入少量受锁保护的状态
-> 打印 queued 日志
-> 发送 FreeRTOS task notification
-> 返回事件循环
```
### 4.2 使用 FreeRTOS task notification 唤醒 watchdog
```c
static void control_plane_wake_multimodal_watchdog(void)
{
TaskHandle_t task =
get_multimodal_task_handle(&s_multimodal_watchdog_task);
if (task != NULL) {
xTaskNotifyGive(task);
}
}
```
任务通知不携带大对象,也不需要新建队列。抽签请求的真实状态保存在受 `s_multimodal_runtime_lock` 保护的静态状态中,通知只表示“需要立即检查一次”。
### 4.3 watchdog 同时支持通知唤醒和周期检查
原 watchdog 每秒固定延时:
```c
vTaskDelay(tick_interval);
```
修改后使用带超时的通知等待:
```c
while (!get_multimodal_stop_flag(&s_multimodal_watchdog_stop_requested)) {
(void)ulTaskNotifyTake(pdTRUE, tick_interval);
control_plane_multimodal_runtime_state_t state = {0};
if (control_plane_multimodal_get_runtime_state(&state) != ESP_OK) {
idle_transport_started_ms = 0U;
continue;
}
uint32_t now_ms = control_plane_now_ms();
control_plane_try_cloud_chouqian_request();
control_plane_try_server_success_prompt(&state, now_ms);
/* 原有 heartbeat、stall 和 idle transport 检查继续执行。 */
}
```
其语义为:
- 收到抽签或会话事件通知时立即运行;
- 没有通知时最多等待 1 秒,继续执行原有周期 watchdog 工作;
- 通知在 worker 忙碌时会被记录,下一轮循环不会丢失唤醒;
- `pending` 状态是业务事实来源,不依赖通知次数表达请求数量。
### 4.4 会话回调也不直接发送网络请求
`TASK_STARTED` 和 `SERVER_LISTENING` 可能发生在 WebSocket 消息处理上下文。最终实现让它们同样只唤醒 watchdog:
```c
case CONTROL_PLANE_MULTIMODAL_EVENT_TASK_STARTED:
control_plane_wake_multimodal_watchdog();
break;
case CONTROL_PLANE_MULTIMODAL_EVENT_SERVER_LISTENING:
control_plane_wake_multimodal_watchdog();
break;
```
这样 `Start` 和 `RequestToRespond` 始终由同一个 control-plane worker 执行,不会把同步 WebSocket 发送压力转移到另一个事件回调任务。
### 4.5 增加 watchdog 栈余量
watchdog 现在负责构造和发送 Start、RequestToRespond,因此栈从 6144 字节增加到 8192 字节:
```c
#define CONTROL_PLANE_WATCHDOG_TASK_STACK_SIZE 8192
```
增加的 2048 字节用于覆盖多模态协议 JSON 构造、日志格式化和 WebSocket 发送调用链。系统事件任务仍保持原来的 4096 字节,不为网络工作长期扩容。
## 5. 修复后的执行上下文
```text
cp_system_event(4096 字节)
-> 识别 SW2 SHORT_PRESS
-> 设置抽签 pending
-> xTaskNotifyGive(mm_watchdog)
-> 立即返回
mm_watchdog(8192 字节)
-> 被通知唤醒
-> 查询多模态会话状态
-> 必要时构造并发送 Start
-> 等待服务端 Listening 通知
-> 构造并发送 RequestToRespond
```
系统事件任务与网络发送调用栈已经完全分离。
## 6. 解决成果
- `cp_system_event` 不再执行 Start JSON 构造和 WebSocket 发送;
- SW2 按键分发能够快速返回,不会阻塞其他系统事件;
- task-started 和 Listening 回调不再直接执行抽签网络请求;
- watchdog 通过任务通知即时响应,不引入固定 1 秒操作延迟;
- watchdog 栈增加到 8192 字节,为新增职责提供明确余量;
- 原有 heartbeat、下行 stall 恢复和空闲 transport 管理仍按 1 秒周期运行;
- ESP-IDF v5.5.4 完整构建通过;
- 生成的应用固件大小为 `0x20f450` 字节,最小应用分区剩余约 41%。
## 7. 后续影响与边界
### 7.1 内存影响
watchdog 栈增加 2048 字节,通常来自 FreeRTOS 可用内部内存。这是本修复最明确的常驻内存成本。相比扩大通用系统事件任务栈,额外内存被分配给实际执行网络协议工作的 owner task,职责更匹配。
### 7.2 watchdog 唤醒频率
每次抽签排队、task started 和 Listening 事件都会通知 watchdog。通知只增加一次轻量调度,不创建新任务,不分配业务消息内存,也不会改变原有任务优先级。
### 7.3 原有周期任务行为
`ulTaskNotifyTake(pdTRUE, tick_interval)` 在无通知时仍以 1 秒超时返回,所以 heartbeat、无响应保护、stall 恢复和 idle transport 关闭逻辑的最大检查间隔没有变长。有通知时这些检查可能提前执行,这是安全的。
### 7.4 请求可靠性
task notification 只负责唤醒,不负责保存业务数据。即使多次通知合并,`s_cloud_chouqian_pending` 仍保持为 true,watchdog 的周期检查也会继续推进请求,因此不会因为通知合并而丢失抽签状态。
### 7.5 仍需关注的验证项
- 冷启动后首次按 SW2,确认不再出现 `cp_system_event` 栈溢出;
- 通过运行时 stack high-water mark 检查 `mm_watchdog` 的实际余量;
- 弱网和 WebSocket 发送超时场景下,确认系统事件队列仍能持续处理其他按键;
- 高频 SW2 和 PTT 交错操作下,确认不会发生重复 Prompt 或异常会话抢占。