Skip to content

Commit 8d1c7cc

Browse files
doctarockedelauna
authored andcommitted
fix: cancel usage tracker debounce on dispose
1 parent c991104 commit 8d1c7cc

4 files changed

Lines changed: 50 additions & 1 deletion

File tree

src/core/task/Task.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2333,6 +2333,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
23332333
console.error("Error disposing file context tracker:", error)
23342334
}
23352335

2336+
try {
2337+
this.usageTracker.dispose()
2338+
} catch (error) {
2339+
console.error("Error disposing usage tracker:", error)
2340+
}
2341+
23362342
try {
23372343
// If we're not streaming then `abortStream` won't be called.
23382344
if (this.isStreaming && this.diffViewProvider.isEditing) {

src/core/task/UsageTracker.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ export class UsageTracker {
7474
this.debouncedEmitTokenUsage.flush()
7575
}
7676

77+
public dispose(): void {
78+
this.debouncedEmitTokenUsage.cancel()
79+
}
80+
7781
public recordToolUsage(toolName: ToolName): void {
7882
const usage = this.ensureToolUsageEntry(toolName)
7983
usage.attempts++

src/core/task/__tests__/Task.dispose.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ describe("Task dispose method", () => {
141141
consoleLogSpy.mockRestore()
142142
})
143143

144+
test("should dispose usage tracker to cancel pending usage emissions", () => {
145+
const usageTrackerDisposeSpy = vi.spyOn((task as any).usageTracker, "dispose")
146+
147+
task.dispose()
148+
149+
expect(usageTrackerDisposeSpy).toHaveBeenCalledOnce()
150+
})
151+
144152
test("should prevent memory leaks by removing listeners before other cleanup", () => {
145153
// Add multiple listeners of different types using type assertion for testing
146154
const listeners = {

src/core/task/__tests__/UsageTracker.spec.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, expect, it, vi } from "vitest"
1+
import { afterEach, describe, expect, it, vi } from "vitest"
22
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
33

44
import { UsageTracker } from "../UsageTracker.js"
@@ -11,6 +11,10 @@ const createUsageMessage = (text: string, ts = 1000): ClineMessage => ({
1111
})
1212

1313
describe("UsageTracker", () => {
14+
afterEach(() => {
15+
vi.useRealTimers()
16+
})
17+
1418
it("recomputes tokenUsage on each read after an emitted snapshot exists", () => {
1519
const messages: ClineMessage[] = [
1620
{ type: "say", say: "text", text: "task", ts: 1 },
@@ -53,4 +57,31 @@ describe("UsageTracker", () => {
5357
)
5458
expect((tracker as any).tokenUsageSnapshot.totalTokensIn).toBe(100)
5559
})
60+
61+
it("cancels pending debounced token usage emissions on dispose", () => {
62+
vi.useFakeTimers()
63+
64+
const emit = vi.fn()
65+
const messages: ClineMessage[] = [
66+
{ type: "say", say: "text", text: "task", ts: 1 },
67+
createUsageMessage('{"tokensIn":100,"tokensOut":50,"cost":0.01}', 1000),
68+
]
69+
const tracker = new UsageTracker({
70+
taskId: "task-id",
71+
getMessages: () => messages,
72+
emit,
73+
emitIntervalMs: 1000,
74+
})
75+
76+
tracker.emitTokenUsageUpdate(tracker.getTokenUsage())
77+
messages[1] = createUsageMessage('{"tokensIn":250,"tokensOut":50,"cost":0.02}', 1000)
78+
tracker.emitTokenUsageUpdate(tracker.getTokenUsage())
79+
80+
expect(emit).toHaveBeenCalledTimes(1)
81+
82+
tracker.dispose()
83+
vi.advanceTimersByTime(1000)
84+
85+
expect(emit).toHaveBeenCalledTimes(1)
86+
})
5687
})

0 commit comments

Comments
 (0)