feat(12B): latencyprobe 延迟验收工具 + 会话建立耗时观测 + 滑动窗口只查不记修复
- cmd/latencyprobe:复刻桌面端行为(单连接串行、100ms/帧实时推流)实测 网关全链路 first_partial / release_to_commit P50/P95,口径对齐 dictation.rs - gummy/gateway 增加会话建立耗时日志(dial / task-started / start 总耗时), 作为 first_partial 延迟分解的常驻观测点 - 修复 slideScript:val=0 的只查请求(AudioWindowExhausted)不再落 0 值成员, 与注释「只查不记」一致,避免污染窗口 ZSET 并空耗 seq 12B 实测结论(gummy 真实 provider,本机网关): - first_partial P50≈940ms:其中 DashScope 需收到 ~0.8s 音频内容才出首个 partial(直连同样),网关暖路径开销仅 ~130ms、冷启 dial ~600ms - release_to_commit:短会话 ~170ms 达标;8s 会话 350~540ms 超标(final flush 随内容量增长);偶发 DashScope 抖动可到秒级 - stop 后周期 usage 帧常先于尾部 final 到达,桌面端会以 partial 文本提前 注入丢失 final 修正(iOS 已在 21A 修过同类问题,桌面端待修) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -34,10 +35,12 @@ func (g *GummyProvider) StartSession(ctx context.Context, cfg SessionConfig) (Se
|
||||
"Authorization": {"bearer " + g.APIKey},
|
||||
"X-DashScope-DataInspection": {"enable"},
|
||||
}
|
||||
tDial := time.Now()
|
||||
conn, _, err := websocket.DefaultDialer.DialContext(ctx, dashscopeWS, header)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dashscope dial: %w", err)
|
||||
}
|
||||
dialMs := time.Since(tDial).Milliseconds()
|
||||
|
||||
taskID := uuid.NewString()
|
||||
runTask := map[string]any{
|
||||
@@ -75,8 +78,13 @@ func (g *GummyProvider) StartSession(ctx context.Context, cfg SessionConfig) (Se
|
||||
go s.readLoop()
|
||||
|
||||
// 协议要求:必须等 task-started 后才能推音频,否则服务端静默丢弃
|
||||
tWait := time.Now()
|
||||
select {
|
||||
case <-s.started:
|
||||
// 会话建立耗时是 first_partial 延迟的组成部分(12B):dial 冷启(DNS+TLS)
|
||||
// 可达 ~600ms、暖路径 ~60ms,task-started 通常 ~60ms。持续观测供延迟排查。
|
||||
slog.Info("gummy: session ready", "task", taskID,
|
||||
"dial_ms", dialMs, "task_started_wait_ms", time.Since(tWait).Milliseconds())
|
||||
return s, nil
|
||||
case <-s.done:
|
||||
return nil, fmt.Errorf("dashscope: closed before task-started")
|
||||
|
||||
@@ -137,6 +137,12 @@ func (h *Handler) HandleWS(c *gin.Context) {
|
||||
// startSession 执行 start 预检并建立 Provider 会话;失败时返回 nil(已下发 error 帧)。
|
||||
func (h *Handler) startSession(ctx context.Context, ws *wsConn, uid, deviceID string, msg protocol.ClientMsg) *session {
|
||||
now := time.Now()
|
||||
// start 处理耗时(预检 + provider 握手)串行阻塞在读循环里,直接叠加进客户端
|
||||
// 可感知的 first_partial 延迟(12B),持续观测供延迟排查。
|
||||
defer func() {
|
||||
slog.Info("gateway: start handled", "session", msg.SessionID,
|
||||
"total_ms", time.Since(now).Milliseconds())
|
||||
}()
|
||||
|
||||
// ① 设备 30min 窗口:次数 + 累计时长
|
||||
if ok, err := store.AllowSession(ctx, h.RDB, deviceID, now); err != nil || !ok {
|
||||
|
||||
@@ -46,10 +46,14 @@ else
|
||||
end
|
||||
end
|
||||
if sum + val > limit then return 0 end
|
||||
local seq = redis.call('INCR', key .. ':seq')
|
||||
redis.call('EXPIRE', key .. ':seq', window + 60) -- 17C:seq 计数器与 ZSET 同寿命,避免按设备永久泄漏
|
||||
redis.call('ZADD', key, now, now .. '-' .. seq .. ':' .. val)
|
||||
redis.call('EXPIRE', key, window + 60)
|
||||
-- val=0 为"只查不记"(AudioWindowExhausted 的 start 预检):不落 0 值成员,
|
||||
-- 避免污染窗口 ZSET 并空耗 seq;count 模式 val 恒为 1,不受影响。
|
||||
if val > 0 then
|
||||
local seq = redis.call('INCR', key .. ':seq')
|
||||
redis.call('EXPIRE', key .. ':seq', window + 60) -- 17C:seq 计数器与 ZSET 同寿命,避免按设备永久泄漏
|
||||
redis.call('ZADD', key, now, now .. '-' .. seq .. ':' .. val)
|
||||
redis.call('EXPIRE', key, window + 60)
|
||||
end
|
||||
return 1
|
||||
`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user