Explicitly dispose of the SimulationWorkspace (#929)

Looking into why this is being retained as well during testing but hopefully this reduces the memory usage
This commit is contained in:
Matt Bierner
2025-09-05 22:03:08 -07:00
committed by GitHub
parent 34973cebb0
commit bf8ec6c11d
3 changed files with 173 additions and 159 deletions

View File

@@ -14,6 +14,7 @@ import { createTextDocumentData, IExtHostDocumentData, setDocText } from '../../
import { ExtHostTextEditor } from '../../../util/common/test/shims/textEditor';
import { isUri } from '../../../util/common/types';
import { Emitter } from '../../../util/vs/base/common/event';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { ResourceMap } from '../../../util/vs/base/common/map';
import { Schemas } from '../../../util/vs/base/common/network';
import * as path from '../../../util/vs/base/common/path';
@@ -113,9 +114,9 @@ export function isNotebook(file: string | vscode.Uri | vscode.TextDocument) {
return file.uri.scheme === Schemas.vscodeNotebookCell || file.uri.fsPath.endsWith('.ipynb');
}
export class SimulationWorkspace {
export class SimulationWorkspace extends Disposable {
private readonly _onDidChangeDiagnostics = new Emitter<vscode.DiagnosticChangeEvent>();
private readonly _onDidChangeDiagnostics = this._register(new Emitter<vscode.DiagnosticChangeEvent>());
public readonly onDidChangeDiagnostics = this._onDidChangeDiagnostics.event;
private _workspaceState: IDeserializedWorkspaceState | undefined;
@@ -163,6 +164,12 @@ export class SimulationWorkspace {
}
constructor() {
super();
this._clear();
}
public override dispose(): void {
super.dispose();
this._clear();
}

View File

@@ -23,6 +23,7 @@ import { SpyChatResponseStream } from '../../src/util/common/test/mockChatRespon
import { ChatRequestTurn, ChatResponseTurn } from '../../src/util/common/test/shims/chatTypes';
import { CancellationToken } from '../../src/util/vs/base/common/cancellation';
import { Event } from '../../src/util/vs/base/common/event';
import { DisposableStore } from '../../src/util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../src/util/vs/platform/instantiation/common/instantiation';
import { ChatLocation, ChatRequest, ChatResponseAnchorPart, ChatResponseMarkdownPart } from '../../src/vscodeTypes';
import { SimulationWorkspaceExtHost } from '../base/extHostContext/simulationWorkspaceExtHost';
@@ -53,188 +54,193 @@ export function fetchConversationOptions() {
export function generateScenarioTestRunner(scenario: Scenario, evaluator: ScenarioEvaluator): SimulationTestFunction {
return async function (testingServiceCollection) {
testingServiceCollection.define(IConversationOptions, fetchConversationOptions());
const simulationWorkspace = isInExtensionHost ? new SimulationWorkspaceExtHost() : new SimulationWorkspace();
simulationWorkspace.setupServices(testingServiceCollection);
const accessor = testingServiceCollection.createTestingAccessor();
const disposables = new DisposableStore();
try {
testingServiceCollection.define(IConversationOptions, fetchConversationOptions());
const simulationWorkspace = disposables.add(isInExtensionHost ? new SimulationWorkspaceExtHost() : new SimulationWorkspace());
simulationWorkspace.setupServices(testingServiceCollection);
const accessor = testingServiceCollection.createTestingAccessor();
const testContext = accessor.get(ISimulationTestRuntime);
const log = (message: string, err?: any) => testContext.log(message, err);
const testContext = accessor.get(ISimulationTestRuntime);
const log = (message: string, err?: any) => testContext.log(message, err);
const history: (ChatRequestTurn | ChatResponseTurn)[] = [];
for (let i = 0; i < scenario.length; i++) {
const testCase = scenario[i];
simulationWorkspace.resetFromDeserializedWorkspaceState(testCase.getState?.());
await testCase.setupCase?.(accessor, simulationWorkspace);
const mockProgressReporter = new SpyChatResponseStream();
log(`> Query "${testCase.question}"\n`);
const history: (ChatRequestTurn | ChatResponseTurn)[] = [];
for (let i = 0; i < scenario.length; i++) {
const testCase = scenario[i];
simulationWorkspace.resetFromDeserializedWorkspaceState(testCase.getState?.());
await testCase.setupCase?.(accessor, simulationWorkspace);
const mockProgressReporter = new SpyChatResponseStream();
log(`> Query "${testCase.question}"\n`);
const parsedQuery = await parseQueryForScenarioTest(accessor, testCase, simulationWorkspace);
const participantId = (parsedQuery.participantName && getChatParticipantIdFromName(parsedQuery.participantName)) ?? '';
const request: ChatRequest = { prompt: parsedQuery.query, references: parsedQuery.variables, command: parsedQuery.command, location: ChatLocation.Panel, location2: undefined, attempt: 0, enableCommandDetection: false, isParticipantDetected: false, toolReferences: parsedQuery.toolReferences, toolInvocationToken: undefined as never, model: null!, tools: new Map(), id: '1' };
if (testCase.tools) {
for (const [toolName, shouldUse] of Object.entries(testCase.tools)) {
request.tools.set(getContributedToolName(toolName), shouldUse);
const parsedQuery = await parseQueryForScenarioTest(accessor, testCase, simulationWorkspace);
const participantId = (parsedQuery.participantName && getChatParticipantIdFromName(parsedQuery.participantName)) ?? '';
const request: ChatRequest = { prompt: parsedQuery.query, references: parsedQuery.variables, command: parsedQuery.command, location: ChatLocation.Panel, location2: undefined, attempt: 0, enableCommandDetection: false, isParticipantDetected: false, toolReferences: parsedQuery.toolReferences, toolInvocationToken: undefined as never, model: null!, tools: new Map(), id: '1' };
if (testCase.tools) {
for (const [toolName, shouldUse] of Object.entries(testCase.tools)) {
request.tools.set(getContributedToolName(toolName), shouldUse);
}
}
}
const interactiveSession = accessor.get(IInstantiationService).createInstance(
ChatParticipantRequestHandler,
history,
request,
mockProgressReporter,
CancellationToken.None,
{
agentId: participantId,
agentName: parsedQuery.participantName || '',
intentId: (!parsedQuery.participantName && parsedQuery.command) ? parsedQuery.command :
parsedQuery.command ? agentsToCommands[parsedQuery.participantName as Intent]![parsedQuery.command] :
parsedQuery.participantName,
},
Event.None,
);
const result = await interactiveSession.getResult();
assert.ok(!result.errorDetails, result.errorDetails?.message);
const interactiveSession = accessor.get(IInstantiationService).createInstance(
ChatParticipantRequestHandler,
history,
request,
mockProgressReporter,
CancellationToken.None,
{
agentId: participantId,
agentName: parsedQuery.participantName || '',
intentId: (!parsedQuery.participantName && parsedQuery.command) ? parsedQuery.command :
parsedQuery.command ? agentsToCommands[parsedQuery.participantName as Intent]![parsedQuery.command] :
parsedQuery.participantName,
},
Event.None,
);
const result = await interactiveSession.getResult();
assert.ok(!result.errorDetails, result.errorDetails?.message);
history.push(new ChatRequestTurn(request.prompt, request.command, [...request.references], getChatParticipantIdFromName(participantId), []));
history.push(new ChatResponseTurn(mockProgressReporter.items.filter(x => x instanceof ChatResponseMarkdownPart || x instanceof ChatResponseAnchorPart), result, participantId, request.command));
history.push(new ChatRequestTurn(request.prompt, request.command, [...request.references], getChatParticipantIdFromName(participantId), []));
history.push(new ChatResponseTurn(mockProgressReporter.items.filter(x => x instanceof ChatResponseMarkdownPart || x instanceof ChatResponseAnchorPart), result, participantId, request.command));
testCase.answer = mockProgressReporter.currentProgress;
testCase.answer = mockProgressReporter.currentProgress;
const turn = interactiveSession.conversation.getLatestTurn();
const fullResponse = turn?.responseMessage?.message ?? '';
const turn = interactiveSession.conversation.getLatestTurn();
const fullResponse = turn?.responseMessage?.message ?? '';
accessor.get(ISimulationTestRuntime).setOutcome({
kind: 'answer',
content: fullResponse
});
accessor.get(ISimulationTestRuntime).setOutcome({
kind: 'answer',
content: fullResponse
});
// Use the evaluator passed to us to evaluate if the response is correct
log(`## Response:\n${fullResponse}\n`);
log(`## Commands:\n`);
const commands = mockProgressReporter.commandButtons;
for (const command of commands) {
log(`- ${JSON.stringify(command)}\n`);
}
// Use the evaluator passed to us to evaluate if the response is correct
log(`## Response:\n${fullResponse}\n`);
log(`## Commands:\n`);
const commands = mockProgressReporter.commandButtons;
for (const command of commands) {
log(`- ${JSON.stringify(command)}\n`);
}
if (scenario[i].applyChatCodeBlocks) {
const codeBlocks = turn?.getMetadata(CodeBlocksMetadata)?.codeBlocks ?? [];
const testRuntime = accessor.get(ISimulationTestRuntime);
if (scenario[i].applyChatCodeBlocks) {
const codeBlocks = turn?.getMetadata(CodeBlocksMetadata)?.codeBlocks ?? [];
const testRuntime = accessor.get(ISimulationTestRuntime);
if (codeBlocks.length !== 0) {
const codeMapper = accessor.get(IInstantiationService).createInstance(CodeMapper);
const changedDocs: Map<string, { document: TextDocument; originalContent: string; postContent: string }> = new Map();
if (codeBlocks.length !== 0) {
const codeMapper = accessor.get(IInstantiationService).createInstance(CodeMapper);
const changedDocs: Map<string, { document: TextDocument; originalContent: string; postContent: string }> = new Map();
// Apply Code Block Changes
let codeBlockApplyErrorDetails: ChatErrorDetails | undefined = undefined;
for (const codeBlock of codeBlocks) {
const prevDocument = simulationWorkspace.activeTextEditor?.document!;
// Set the active document if the code resource has a uri
if (codeBlock.resource) {
simulationWorkspace.setCurrentDocument(codeBlock.resource);
}
const editor = accessor.get(ITabsAndEditorsService).activeTextEditor!;
const codeMap = codeBlock.code;
const document = simulationWorkspace.activeTextEditor!.document;
const documentContext = IDocumentContext.fromEditor(editor);
const workspacePath = simulationWorkspace.getFilePath(document.uri);
// Apply Code Block Changes
let codeBlockApplyErrorDetails: ChatErrorDetails | undefined = undefined;
for (const codeBlock of codeBlocks) {
const prevDocument = simulationWorkspace.activeTextEditor?.document!;
// Set the active document if the code resource has a uri
if (codeBlock.resource) {
simulationWorkspace.setCurrentDocument(codeBlock.resource);
}
const editor = accessor.get(ITabsAndEditorsService).activeTextEditor!;
const codeMap = codeBlock.code;
const document = simulationWorkspace.activeTextEditor!.document;
const documentContext = IDocumentContext.fromEditor(editor);
const workspacePath = simulationWorkspace.getFilePath(document.uri);
const previousTextContent = document.getText();
const response: MappedEditsResponseStream = {
textEdit(target, edits) {
simulationWorkspace.applyEdits(target, Array.isArray(edits) ? edits : [edits]);
},
notebookEdit(target, edits) {
simulationWorkspace.applyNotebookEdits(target, Array.isArray(edits) ? edits : [edits]);
},
};
const input: ICodeMapperExistingDocument = { createNew: false, codeBlock: codeMap, uri: document.uri, markdownBeforeBlock: undefined, existingDocument: documentContext.document };
const result = await codeMapper.mapCode(input, response, undefined, CancellationToken.None);
if (!result) {
codeBlockApplyErrorDetails = {
message: `Code block changes failed to apply to ${document.uri.toString()}`,
const previousTextContent = document.getText();
const response: MappedEditsResponseStream = {
textEdit(target, edits) {
simulationWorkspace.applyEdits(target, Array.isArray(edits) ? edits : [edits]);
},
notebookEdit(target, edits) {
simulationWorkspace.applyNotebookEdits(target, Array.isArray(edits) ? edits : [edits]);
},
};
break;
}
const input: ICodeMapperExistingDocument = { createNew: false, codeBlock: codeMap, uri: document.uri, markdownBeforeBlock: undefined, existingDocument: documentContext.document };
const result = await codeMapper.mapCode(input, response, undefined, CancellationToken.None);
if (result.errorDetails) {
result.errorDetails.message = `Code block changes failed to apply to ${document.uri.toString()}:\n${result.errorDetails.message}`;
codeBlockApplyErrorDetails = result.errorDetails;
break;
}
if (!result) {
codeBlockApplyErrorDetails = {
message: `Code block changes failed to apply to ${document.uri.toString()}`,
};
break;
}
const postEditTextContent = editor.document.getText();
if (previousTextContent !== postEditTextContent) {
const previousChange = changedDocs.get(workspacePath);
if (previousChange) {
previousChange.postContent = postEditTextContent;
changedDocs.set(workspacePath, previousChange);
} else {
changedDocs.set(workspacePath, { document, originalContent: previousTextContent, postContent: postEditTextContent });
if (result.errorDetails) {
result.errorDetails.message = `Code block changes failed to apply to ${document.uri.toString()}:\n${result.errorDetails.message}`;
codeBlockApplyErrorDetails = result.errorDetails;
break;
}
const postEditTextContent = editor.document.getText();
if (previousTextContent !== postEditTextContent) {
const previousChange = changedDocs.get(workspacePath);
if (previousChange) {
previousChange.postContent = postEditTextContent;
changedDocs.set(workspacePath, previousChange);
} else {
changedDocs.set(workspacePath, { document, originalContent: previousTextContent, postContent: postEditTextContent });
}
}
if (prevDocument) {
simulationWorkspace.setCurrentDocument(prevDocument.uri);
}
}
if (prevDocument) {
simulationWorkspace.setCurrentDocument(prevDocument.uri);
}
}
// Log the changed files
const changedFilePaths: IWorkspaceStateFile[] = [];
if (!codeBlockApplyErrorDetails && changedDocs.size > 0) {
const seenDoc = new Set<string>();
for (const [workspacePath, changes] of changedDocs.entries()) {
if (seenDoc.has(workspacePath)) {
continue;
}
seenDoc.add(workspacePath);
// Log the changed files
const changedFilePaths: IWorkspaceStateFile[] = [];
if (!codeBlockApplyErrorDetails && changedDocs.size > 0) {
const seenDoc = new Set<string>();
for (const [workspacePath, changes] of changedDocs.entries()) {
if (seenDoc.has(workspacePath)) {
continue;
if (isNotebook(changes.document.uri)) {
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
changedFilePaths.push({
workspacePath,
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
languageId: changes.document.languageId
});
} else {
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
changedFilePaths.push({
workspacePath,
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
languageId: changes.document.languageId
});
}
}
seenDoc.add(workspacePath);
if (isNotebook(changes.document.uri)) {
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
changedFilePaths.push({
workspacePath,
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
languageId: changes.document.languageId
});
} else {
await testRuntime.writeFile(workspacePath + '.txt', changes.originalContent, INLINE_INITIAL_DOC_TAG); // using .txt instead of real file extension to avoid breaking automation scripts
changedFilePaths.push({
workspacePath,
relativeDiskPath: await testRuntime.writeFile(workspacePath, changes.postContent, INLINE_CHANGED_DOC_TAG),
languageId: changes.document.languageId
});
}
testRuntime.setOutcome({
kind: 'edit',
files: changedFilePaths.map(f => ({ srcUri: f.workspacePath, post: f.relativeDiskPath }))
});
} else if (codeBlockApplyErrorDetails) {
testRuntime.setOutcome({
kind: 'failed',
error: codeBlockApplyErrorDetails.message,
hitContentFilter: codeBlockApplyErrorDetails.responseIsFiltered ?? false,
critical: false
});
}
testRuntime.setOutcome({
kind: 'edit',
files: changedFilePaths.map(f => ({ srcUri: f.workspacePath, post: f.relativeDiskPath }))
});
} else if (codeBlockApplyErrorDetails) {
testRuntime.setOutcome({
kind: 'failed',
error: codeBlockApplyErrorDetails.message,
hitContentFilter: codeBlockApplyErrorDetails.responseIsFiltered ?? false,
critical: false
});
}
}
}
const evaluatedResponse = await evaluator(
accessor,
testCase.question,
mockProgressReporter.currentProgress,
fullResponse,
turn,
i,
commands,
mockProgressReporter.confirmations,
mockProgressReporter.fileTrees,
);
assert.ok(evaluatedResponse.success, evaluatedResponse.errorMessage);
const evaluatedResponse = await evaluator(
accessor,
testCase.question,
mockProgressReporter.currentProgress,
fullResponse,
turn,
i,
commands,
mockProgressReporter.confirmations,
mockProgressReporter.fileTrees,
);
assert.ok(evaluatedResponse.success, evaluatedResponse.errorMessage);
}
} finally {
disposables.dispose();
}
};
}

View File

@@ -64,11 +64,12 @@ export function setupSimulationWorkspace(testingServiceCollection: TestingServic
return workspace;
}
export async function teardownSimulationWorkspace(accessor: ITestingServicesAccessor, _workbench: SimulationWorkspace): Promise<void> {
export async function teardownSimulationWorkspace(accessor: ITestingServicesAccessor, workbench: SimulationWorkspace): Promise<void> {
const ls = accessor.get(ILanguageFeaturesService);
if (ls instanceof SimulationLanguageFeaturesService) {
await ls.teardown();
}
workbench.dispose();
}
function isDeserializedWorkspaceStateBasedScenario(scenario: IScenario): scenario is IDeserializedWorkspaceStateBasedScenario {