Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit d48b206

Browse files
SibidharanSibidharan
authored andcommitted
Improve request log tree performance
1 parent 5dc4178 commit d48b206

2 files changed

Lines changed: 305 additions & 40 deletions

File tree

src/extension/log/vscode-node/requestLogTree.ts

Lines changed: 127 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { IVSCodeExtensionContext } from '../../../platform/extContext/common/ext
1414
import { OutputChannelName } from '../../../platform/log/vscode/outputChannelLogTarget';
1515
import { ChatRequestScheme, ILoggedElementInfo, ILoggedRequestInfo, ILoggedToolCall, IRequestLogger, LoggedInfo, LoggedInfoKind, LoggedRequestKind } from '../../../platform/requestLogger/node/requestLogger';
1616
import { assertNever } from '../../../util/vs/base/common/assert';
17+
import { RunOnceScheduler } from '../../../util/vs/base/common/async';
1718
import { Disposable, toDisposable } from '../../../util/vs/base/common/lifecycle';
1819
import { LRUCache } from '../../../util/vs/base/common/map';
1920
import { isDefined } from '../../../util/vs/base/common/types';
@@ -32,14 +33,17 @@ const showRawRequestBodyCommand = 'github.copilot.chat.debug.showRawRequestBody'
3233
export class RequestLogTree extends Disposable implements IExtensionContribution {
3334
readonly id = 'requestLogTree';
3435
private readonly chatRequestProvider: ChatRequestProvider;
36+
private readonly treeView: vscode.TreeView<TreeItem>;
3537

3638
constructor(
3739
@IInstantiationService instantiationService: IInstantiationService,
3840
@IRequestLogger requestLogger: IRequestLogger,
3941
) {
4042
super();
4143
this.chatRequestProvider = this._register(instantiationService.createInstance(ChatRequestProvider));
42-
this._register(vscode.window.registerTreeDataProvider('copilot-chat', this.chatRequestProvider));
44+
this.treeView = this._register(vscode.window.createTreeView('copilot-chat', { treeDataProvider: this.chatRequestProvider }));
45+
this.chatRequestProvider.setVisible(this.treeView.visible);
46+
this._register(this.treeView.onDidChangeVisibility(e => this.chatRequestProvider.setVisible(e.visible)));
4347

4448
let server: RequestServer | undefined;
4549

@@ -559,6 +563,16 @@ type TreeItem = ChatPromptItem | ChatRequestItem | ChatElementItem | ToolCallIte
559563

560564
class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<TreeItem> {
561565
private readonly filters: LogTreeFilters;
566+
private rootItems: (ChatPromptItem | TreeChildItem)[] = [];
567+
private seenChatRequests = new WeakSet<ChatRequest>();
568+
private processedCount = 0;
569+
private currentPrompt: ChatPromptItem | undefined;
570+
private readonly refreshScheduler: RunOnceScheduler;
571+
private pendingRootRefresh = false;
572+
private readonly pendingPromptRefresh = new Set<ChatPromptItem>();
573+
private readonly refreshDelay = 50;
574+
private isVisible = false;
575+
private needsRefreshWhenVisible = false;
562576

563577
constructor(
564578
@IRequestLogger private readonly requestLogger: IRequestLogger,
@@ -567,8 +581,10 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
567581
super();
568582
this.filters = this._register(instantiationService.createInstance(LogTreeFilters));
569583
this._register(new LogTreeFilterCommands(this.filters));
570-
this._register(this.requestLogger.onDidChangeRequests(() => this._onDidChangeTreeData.fire()));
571-
this._register(this.filters.onDidChangeFilters(() => this._onDidChangeTreeData.fire()));
584+
this._register(this.requestLogger.onDidChangeRequests(() => this.handleRequestChange()));
585+
this._register(this.filters.onDidChangeFilters(() => this.handleFilterChange()));
586+
this.refreshScheduler = this._register(new RunOnceScheduler(() => this.flushRefreshQueue(), 0));
587+
this.rebuildFromScratch();
572588
}
573589

574590
private readonly _onDidChangeTreeData = new vscode.EventEmitter<TreeItem | undefined | void>();
@@ -580,48 +596,104 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
580596

581597
getChildren(element?: TreeItem | undefined): vscode.ProviderResult<TreeItem[]> {
582598
if (element instanceof ChatPromptItem) {
583-
return element.children;
584-
} else if (element) {
599+
return element.children.filter(child => this.filters.itemIncluded(child));
600+
}
601+
602+
if (element) {
585603
return [];
604+
}
605+
606+
return this.rootItems.filter(item => this.filters.itemIncluded(item));
607+
}
608+
609+
private rebuildFromScratch(): void {
610+
this.rootItems = [];
611+
this.seenChatRequests = new WeakSet<ChatRequest>();
612+
this.processedCount = 0;
613+
this.currentPrompt = undefined;
614+
this.appendNewEntries();
615+
}
616+
617+
private handleRequestChange(): void {
618+
this.appendNewEntries();
619+
}
620+
621+
private handleFilterChange(): void {
622+
if (this.isVisible) {
623+
this._onDidChangeTreeData.fire(undefined);
586624
} else {
587-
let lastPrompt: ChatPromptItem | undefined;
588-
const result: (ChatPromptItem | TreeChildItem)[] = [];
589-
const seen = new Set<ChatRequest | undefined>();
590-
591-
for (const r of this.requestLogger.getRequests()) {
592-
const item = this.logToTreeItem(r);
593-
if (r.chatRequest !== lastPrompt?.request) {
594-
if (lastPrompt) {
595-
result.push(lastPrompt);
596-
}
597-
lastPrompt = r.chatRequest ? ChatPromptItem.create(r, r.chatRequest, seen.has(r.chatRequest)) : undefined;
598-
seen.add(r.chatRequest);
625+
this.pendingRootRefresh = true;
626+
this.needsRefreshWhenVisible = true;
627+
}
628+
}
629+
630+
setVisible(visible: boolean): void {
631+
this.isVisible = visible;
632+
if (visible) {
633+
if (this.pendingRootRefresh || this.pendingPromptRefresh.size || this.needsRefreshWhenVisible) {
634+
this.refreshScheduler.schedule(0);
635+
}
636+
}
637+
}
638+
639+
private appendNewEntries(): void {
640+
const requests = this.requestLogger.getRequests();
641+
let rootChanged = false;
642+
const promptsToRefresh = new Set<ChatPromptItem>();
643+
const newPrompts = new Set<ChatPromptItem>();
644+
645+
if (requests.length < this.processedCount) {
646+
this.rootItems = [];
647+
this.seenChatRequests = new WeakSet<ChatRequest>();
648+
this.processedCount = 0;
649+
this.currentPrompt = undefined;
650+
rootChanged = true;
651+
}
652+
653+
for (let i = this.processedCount; i < requests.length; i++) {
654+
const info = requests[i];
655+
const child = this.logToTreeItem(info);
656+
const request = info.chatRequest;
657+
658+
if (request) {
659+
const hasSeen = this.seenChatRequests.has(request);
660+
if (!this.currentPrompt || this.currentPrompt.request !== request) {
661+
this.currentPrompt = ChatPromptItem.create(info, request, hasSeen);
662+
this.currentPrompt.children.length = 0;
663+
this.rootItems.push(this.currentPrompt);
664+
this.seenChatRequests.add(request);
665+
rootChanged = true;
666+
newPrompts.add(this.currentPrompt);
599667
}
600668

601-
if (lastPrompt) {
602-
if (!lastPrompt.children.find(c => c.id === item.id)) {
603-
lastPrompt.children.push(item);
604-
}
605-
if (!lastPrompt.children.find(c => c.id === item.id)) {
606-
lastPrompt.children.push(item);
607-
}
608-
} else {
609-
result.push(item);
669+
if (!this.currentPrompt.children.some(c => c.id === child.id)) {
670+
this.currentPrompt.children.push(child);
671+
promptsToRefresh.add(this.currentPrompt);
610672
}
673+
} else {
674+
this.currentPrompt = undefined;
675+
this.rootItems.push(child);
676+
rootChanged = true;
611677
}
678+
}
612679

613-
if (lastPrompt) {
614-
result.push(lastPrompt);
615-
}
680+
this.processedCount = requests.length;
616681

617-
return result.map(r => {
618-
if (r instanceof ChatPromptItem) {
619-
return r.withFilteredChildren(child => this.filters.itemIncluded(child));
620-
}
682+
if (rootChanged) {
683+
this.pendingRootRefresh = true;
684+
}
685+
686+
for (const prompt of promptsToRefresh) {
687+
if (rootChanged && newPrompts.has(prompt)) {
688+
continue;
689+
}
690+
this.pendingPromptRefresh.add(prompt);
691+
}
621692

622-
return r;
623-
})
624-
.filter(r => this.filters.itemIncluded(r));
693+
if (this.isVisible) {
694+
this.refreshScheduler.schedule(this.refreshDelay);
695+
} else {
696+
this.needsRefreshWhenVisible = true;
625697
}
626698
}
627699

@@ -637,6 +709,24 @@ class ChatRequestProvider extends Disposable implements vscode.TreeDataProvider<
637709
assertNever(r);
638710
}
639711
}
712+
713+
private flushRefreshQueue(): void {
714+
if (!this.isVisible) {
715+
return;
716+
}
717+
718+
if (this.pendingRootRefresh) {
719+
this._onDidChangeTreeData.fire(undefined);
720+
} else {
721+
for (const prompt of this.pendingPromptRefresh) {
722+
this._onDidChangeTreeData.fire(prompt);
723+
}
724+
}
725+
726+
this.pendingRootRefresh = false;
727+
this.pendingPromptRefresh.clear();
728+
this.needsRefreshWhenVisible = false;
729+
}
640730
}
641731

642732
type TreeChildItem = ChatRequestItem | ChatElementItem | ToolCallItem;
@@ -824,4 +914,4 @@ class LogTreeFilterCommands extends Disposable {
824914
this._register(vscode.commands.registerCommand('github.copilot.chat.debug.showNesRequests', () => filters.setNesRequestsShown(true)));
825915
this._register(vscode.commands.registerCommand('github.copilot.chat.debug.hideNesRequests', () => filters.setNesRequestsShown(false)));
826916
}
827-
}
917+
}

0 commit comments

Comments
 (0)