`ChatEngine.handleTextMessageContentEvent` is O(n²) in answer length: `startsWith(before)` copies the whole accumulated text on every chunk
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
Issue này chưa được đánh giá.
Mô tả
What happens
handleTextMessageContentEvent in packages/ai/src/activities/chat/index.ts (main @ a7eb584, L2405–L2431) runs this for every TEXT_MESSAGE_CONTENT chunk:
const before = this.accumulatedContent
if (typeof extra.content === 'string' && extra.content !== '') {
this.accumulatedContent = extra.content
} else {
this.accumulatedContent += chunk.delta
}
// ...
if (!this.accumulatedContent.startsWith(before)) {
this.turnParts = null
return
}
const delta = this.accumulatedContent.slice(before.length)
After +=, V8 doesn't copy the text. It stores the result as a string that points to the two parts. startsWith then forces V8 to copy the whole accumulated answer into one string, on every chunk. So the total cost grows with the square of the answer length, and none of it yields to the event loop.
Reproduction
This uses the same string operations, with no TanStack code involved. Node 22, Apple Silicon:
const N = 195_000
const chunk = 'abcd'
let acc = ''
const t0 = performance.now()
for (let i = 0; i < N / chunk.length; i++) {
const before = acc
acc += chunk
if (!acc.startsWith(before)) {
throw new Error('unreachable')
}
acc.slice(before.length)
}
console.log(performance.now() - t0)
- With the
startsWith, it takes about 10.9 s. - With only the
+=, it takes about 0 ms.
In a real chat() run, a CPU profile of a stream of about 195k characters in 4-character chunks showed about 15.7 s inside handleStreamChunk. A model that returns a long answer in small chunks blocks the server's event loop for that whole time.
We noticed it in a test that streams a large fake answer four characters at a time. Writing the same bytes as 3-byte CJK characters cut the character count to a quarter and the runtime by more than ten times, about what O(n²) in string length predicts. A model that streams a long answer in small deltas hits the same cost in production.
Affected versions: @tanstack/ai 1.0.0 and 1.0.1, and current main.
Suggested fix
The prefix check only matters when an adapter sends a cumulative content snapshot. When the chunk is a plain delta, the new text starts with before by definition, and the new text is chunk.delta. So the check can move into the snapshot branch:
const extra = chunk as AdapterYieldChunk
const before = this.accumulatedContent
let delta: string
if (typeof extra.content === 'string' && extra.content !== '') {
this.accumulatedContent = extra.content
if (this.turnParts && !extra.content.startsWith(before)) {
// A cumulative `content` snapshot rewrote earlier text; order unknown.
this.turnParts = null
}
delta = extra.content.slice(before.length)
} else {
this.accumulatedContent += chunk.delta
delta = chunk.delta
}
this.middlewareCtx.accumulatedContent = this.accumulatedContent
if (!this.turnParts) return
if (delta === '') return
const last = this.turnParts[this.turnParts.length - 1]
if (last && last.type === 'text') {
last.content += delta
} else {
this.turnParts.push({
type: 'text',
content: delta,
})
}
With this change, a stream of deltas does constant work per chunk. Snapshot chunks still copy the text, but they are the rare fallback path.
A regression test could stream about 200k characters of 4-character deltas through chat() with a mock adapter, then check that the turn's text part matches the input and that the run takes well under a second. If you'd rather not test timing, assert on the result and treat the benchmark above as the guard.
- Ngôn ngữ chính
- TypeScript
- Star
- 3.3k
- Fork
- 378
- Merge trung bình
- 1 ngày 1 phút
- Pull request đã merge (30 ngày)
- 208
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của TanStack/ai
-
openai-base: a Responses max_output_tokens cutoff ends as RUN_ERROR without usage instead of finishReason 'length'Có thể đã có người làm @tombeckenham đã nhận 4 ngày trước. Đang mởhas-pr waiting-on: maintainer
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
TanStack/ai#1916 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
openai-base: leaving a Responses stream at `response.completed` aborts its request, so complete calls are logged as client-closed (499)Có thể đã có người làm @AlemTuzlak đã nhận hôm nay. Đang mởhas-pr waiting-on: maintainer
TanStack/ai#1965 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
StreamProcessor folds long text and tool-call streams in quadratic memory and timeCó thể đã có người làm @jherr đã nhận 1 ngày trước. Đang mởwaiting-on: maintainer
TanStack/ai#1963 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ai-harness: view.dispose() doesn't cancel snapshot, transcript or describe requestsCó thể đã có người làm @AlemTuzlak đã nhận 1 ngày trước. Đang mởneeds-repro waiting-on: maintainer
TanStack/ai#1960 · 2 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
ai-anthropic: 400 on signed thinking blocks after compaction rewrites history (missing block_binding drop_block)Có thể đã có người làm @tombeckenham đã nhận 2 ngày trước. Đang mởwaiting-on: maintainer
TanStack/ai#1959 · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Issue tương tự
-
[Bug]: Fix for unicode filenamesĐang mở🐛 bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 68/100
LibreChat-AI/LibreChat#17733 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 70/100
TypeStrong/typedoc#3150 ·
-
accessibility bug good first issue
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 85/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Normalize image-source MIME types before SVG dispatchCó thể đã có người làm @HarsH-1877 đã nhận hôm nay. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 75/100
tscircuit/image-utils#58 · 1 bình luận ·
-
Add API: BlockVectra Data APIĐang mở
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 74/100
Maintainer thường phản hồi trong vòng 2 ngày