diff --git a/src/typings/gc-signals.d.ts b/src/typings/gc-signals.d.ts new file mode 100644 index 00000000000..cc982d1260c --- /dev/null +++ b/src/typings/gc-signals.d.ts @@ -0,0 +1,19 @@ +declare module 'gc-signals' { + export interface GCSignal { + } + /** + * Create a new GC signal. When being garbage collected the passed + * value is stored for later consumption. + */ + export const GCSignal: { + new (id: number): GCSignal; + }; + /** + * Consume ids of garbage collected signals. + */ + export function consumeSignals(): number[]; + export function onDidGarbageCollectSignals(callback: (ids: number[]) => any): { + dispose(): void; + }; + export function trackGarbageCollection(obj: any, id: number): number; +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHost.api.impl.ts b/src/vs/workbench/api/node/extHost.api.impl.ts index f4c1b144ae0..90236ba0053 100644 --- a/src/vs/workbench/api/node/extHost.api.impl.ts +++ b/src/vs/workbench/api/node/extHost.api.impl.ts @@ -17,6 +17,7 @@ import {ExtHostConfiguration} from 'vs/workbench/api/node/extHostConfiguration'; import {ExtHostDiagnostics} from 'vs/workbench/api/node/extHostDiagnostics'; import {ExtHostWorkspace} from 'vs/workbench/api/node/extHostWorkspace'; import {ExtHostQuickOpen} from 'vs/workbench/api/node/extHostQuickOpen'; +import {ExtHostHeapMonitor} from 'vs/workbench/api/node/extHostHeapMonitor'; import {ExtHostStatusBar} from 'vs/workbench/api/node/extHostStatusBar'; import {ExtHostCommands} from 'vs/workbench/api/node/extHostCommands'; import {ExtHostOutputService} from 'vs/workbench/api/node/extHostOutputService'; @@ -99,12 +100,13 @@ export class ExtHostAPIImplementation { // Addressable instances const col = new InstanceCollection(); + const extHostHeapMonitor = col.define(ExtHostContext.ExtHostHeapMonitor).set(new ExtHostHeapMonitor()); const extHostDocuments = col.define(ExtHostContext.ExtHostDocuments).set(new ExtHostDocuments(threadService)); const extHostEditors = col.define(ExtHostContext.ExtHostEditors).set(new ExtHostEditors(threadService, extHostDocuments)); const extHostCommands = col.define(ExtHostContext.ExtHostCommands).set(new ExtHostCommands(threadService, extHostEditors)); const extHostConfiguration = col.define(ExtHostContext.ExtHostConfiguration).set(new ExtHostConfiguration(threadService.get(MainContext.MainThreadConfiguration))); const extHostDiagnostics = col.define(ExtHostContext.ExtHostDiagnostics).set(new ExtHostDiagnostics(threadService)); - const languageFeatures = col.define(ExtHostContext.ExtHostLanguageFeatures).set(new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostDiagnostics)); + const languageFeatures = col.define(ExtHostContext.ExtHostLanguageFeatures).set(new ExtHostLanguageFeatures(threadService, extHostDocuments, extHostCommands, extHostHeapMonitor, extHostDiagnostics)); const extHostFileSystemEvent = col.define(ExtHostContext.ExtHostFileSystemEventService).set(new ExtHostFileSystemEventService()); const extHostQuickOpen = col.define(ExtHostContext.ExtHostQuickOpen).set(new ExtHostQuickOpen(threadService)); col.define(ExtHostContext.ExtHostExtensionService).set(extensionService); diff --git a/src/vs/workbench/api/node/extHost.contribution.ts b/src/vs/workbench/api/node/extHost.contribution.ts index 639227e8ee1..43fce942a35 100644 --- a/src/vs/workbench/api/node/extHost.contribution.ts +++ b/src/vs/workbench/api/node/extHost.contribution.ts @@ -32,6 +32,7 @@ import {MainThreadTerminalService} from './mainThreadTerminalService'; import {MainThreadWorkspace} from './mainThreadWorkspace'; import {MainProcessExtensionService} from './mainThreadExtensionService'; import {MainThreadFileSystemEventService} from './mainThreadFileSystemEventService'; +import {MainThreadHeapMonitor} from './mainThreadHeapMonitor'; // --- other interested parties import {MainProcessTextMateSyntax} from 'vs/editor/node/textMate/TMSyntax'; @@ -87,6 +88,7 @@ export class ExtHostContribution implements IWorkbenchContribution { create(JSONValidationExtensionPoint); create(LanguageConfigurationFileHandler); create(MainThreadFileSystemEventService); + create(MainThreadHeapMonitor); } } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 06a1389dc30..2f5fa81afee 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -265,6 +265,10 @@ export abstract class ExtHostFileSystemEventServiceShape { $onFileEvent(events: FileSystemEvents) { throw ni(); } } +export abstract class ExtHostHeapMonitorShape { + $onGarbageCollection(ids: number[]): void { throw ni(); } +} + export abstract class ExtHostLanguageFeaturesShape { $provideDocumentSymbols(handle: number, resource: URI): TPromise { throw ni(); } $provideCodeLenses(handle: number, resource: URI): TPromise { throw ni(); } @@ -321,6 +325,7 @@ export const ExtHostContext = { ExtHostDocuments: createExtId('ExtHostDocuments', ExtHostDocumentsShape), ExtHostEditors: createExtId('ExtHostEditors', ExtHostEditorsShape), ExtHostFileSystemEventService: createExtId('ExtHostFileSystemEventService', ExtHostFileSystemEventServiceShape), + ExtHostHeapMonitor: createExtId('ExtHostHeapMonitor', ExtHostHeapMonitorShape), ExtHostLanguageFeatures: createExtId('ExtHostLanguageFeatures', ExtHostLanguageFeaturesShape), ExtHostQuickOpen: createExtId('ExtHostQuickOpen', ExtHostQuickOpenShape), ExtHostExtensionService: createExtId('ExtHostExtensionService', ExtHostExtensionServiceShape), diff --git a/src/vs/workbench/api/node/extHostHeapMonitor.ts b/src/vs/workbench/api/node/extHostHeapMonitor.ts new file mode 100644 index 00000000000..7f6bfae86ea --- /dev/null +++ b/src/vs/workbench/api/node/extHostHeapMonitor.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import {ExtHostHeapMonitorShape} from './extHost.protocol'; + +export class ExtHostHeapMonitor extends ExtHostHeapMonitorShape { + + private static _idPool = 0; + + private _data: { [n: number]: any } = Object.create(null); + private _callbacks: { [n: number]: Function } = Object.create(null); + + private _mixinObjectIdentifier(obj: any): number { + const id = ExtHostHeapMonitor._idPool++; + + Object.defineProperties(obj, { + '$heap_ident': { + value: id, + enumerable: true, + configurable: false, + writable: false + }, + '$mid': { + value: 3, + enumerable: true, + configurable: false, + writable: false + } + }); + + return id; + } + + linkObjects(external: any, internal: any, callback?: () => any) { + const id = this._mixinObjectIdentifier(external); + this._data[id] = internal; + if (typeof callback === 'function') { + this._callbacks[id] = callback; + } + } + + getInternalObject(external: any): T { + const id = external.$heap_ident; + if (typeof id === 'number') { + return this._data[id]; + } + } + + $onGarbageCollection(ids: number[]): void { + for (const id of ids) { + delete this._data[id]; + const callback = this._callbacks[id]; + if (callback) { + delete this._callbacks[id]; + setTimeout(callback); + } + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index e0e5db8ee61..9852b157bb2 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -10,9 +10,10 @@ import {IDisposable, dispose} from 'vs/base/common/lifecycle'; import {IThreadService} from 'vs/workbench/services/thread/common/threadService'; import * as vscode from 'vscode'; import * as TypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; -import {Range, Disposable, CompletionList} from 'vs/workbench/api/node/extHostTypes'; +import {Range, Disposable, CompletionList, CompletionItem} from 'vs/workbench/api/node/extHostTypes'; import {IPosition, IRange, ISingleEditOperation} from 'vs/editor/common/editorCommon'; import * as modes from 'vs/editor/common/modes'; +import {ExtHostHeapMonitor} from 'vs/workbench/api/node/extHostHeapMonitor'; import {ExtHostDocuments} from 'vs/workbench/api/node/extHostDocuments'; import {ExtHostCommands} from 'vs/workbench/api/node/extHostCommands'; import {ExtHostDiagnostics} from 'vs/workbench/api/node/extHostDiagnostics'; @@ -503,11 +504,12 @@ interface ISuggestion2 extends modes.ISuggestion { class SuggestAdapter { private _documents: ExtHostDocuments; + private _heapMonitor: ExtHostHeapMonitor; private _provider: vscode.CompletionItemProvider; - private _cache: { [key: string]: { list: CompletionList; disposables: IDisposable[]; } } = Object.create(null); - constructor(documents: ExtHostDocuments, provider: vscode.CompletionItemProvider) { + constructor(documents: ExtHostDocuments, heapMonitor: ExtHostHeapMonitor, provider: vscode.CompletionItemProvider) { this._documents = documents; + this._heapMonitor = heapMonitor; this._provider = provider; } @@ -516,12 +518,6 @@ class SuggestAdapter { const doc = this._documents.getDocumentData(resource).document; const pos = TypeConverters.toPosition(position); - const key = resource.toString(); - if (this._cache[key]) { - dispose(this._cache[key].disposables); - delete this._cache[key]; - } - return asWinJsPromise(token => this._provider.provideCompletionItems(doc, pos, token)).then(value => { const result: modes.ISuggestResult = { @@ -533,7 +529,6 @@ class SuggestAdapter { const wordRangeBeforePos = (doc.getWordRangeAtPosition(pos) || new Range(pos, pos)) .with({ end: pos }); - const disposables: IDisposable[] = []; let list: CompletionList; if (!value) { // undefined and null are valid results @@ -550,8 +545,11 @@ class SuggestAdapter { for (let i = 0; i < list.items.length; i++) { const item = list.items[i]; + const disposables: IDisposable[] = []; const suggestion = TypeConverters.Suggest.from(item, disposables); + this._heapMonitor.linkObjects(suggestion, item, () => dispose(disposables)); + if (item.textEdit) { const editRange = item.textEdit.range; @@ -581,25 +579,22 @@ class SuggestAdapter { result.suggestions.push(suggestion); } - // cache for details call - this._cache[key] = { list, disposables }; - return result; }); } resolveCompletionItem(resource: URI, position: IPosition, suggestion: modes.ISuggestion): TPromise { - if (typeof this._provider.resolveCompletionItem !== 'function' || !this._cache[resource.toString()]) { + + if (typeof this._provider.resolveCompletionItem !== 'function') { return TPromise.as(suggestion); } - const {list, disposables} = this._cache[resource.toString()]; - const item = list.items[Number(( suggestion).id)]; + const item = this._heapMonitor.getInternalObject(suggestion); if (!item) { return TPromise.as(suggestion); } return asWinJsPromise(token => this._provider.resolveCompletionItem(item, token)).then(resolvedItem => { - return TypeConverters.Suggest.from(resolvedItem || item, disposables); + return TypeConverters.Suggest.from(resolvedItem || item, []); }); } } @@ -670,6 +665,7 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { private _proxy: MainThreadLanguageFeaturesShape; private _documents: ExtHostDocuments; private _commands: ExtHostCommands; + private _heapMonitor: ExtHostHeapMonitor; private _diagnostics: ExtHostDiagnostics; private _adapter: { [handle: number]: Adapter } = Object.create(null); @@ -677,12 +673,14 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { threadService: IThreadService, documents: ExtHostDocuments, commands: ExtHostCommands, + heapMonitor: ExtHostHeapMonitor, diagnostics: ExtHostDiagnostics ) { super(); this._proxy = threadService.get(MainContext.MainThreadLanguageFeatures); this._documents = documents; this._commands = commands; + this._heapMonitor = heapMonitor; this._diagnostics = diagnostics; } @@ -869,7 +867,7 @@ export class ExtHostLanguageFeatures extends ExtHostLanguageFeaturesShape { registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, triggerCharacters: string[]): vscode.Disposable { const handle = this._nextHandle(); - this._adapter[handle] = new SuggestAdapter(this._documents, provider); + this._adapter[handle] = new SuggestAdapter(this._documents, this._heapMonitor, provider); this._proxy.$registerSuggestSupport(handle, selector, triggerCharacters); return this._createDisposable(handle); } diff --git a/src/vs/workbench/api/node/mainThreadHeapMonitor.ts b/src/vs/workbench/api/node/mainThreadHeapMonitor.ts new file mode 100644 index 00000000000..101ca35d9ea --- /dev/null +++ b/src/vs/workbench/api/node/mainThreadHeapMonitor.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import {IDisposable} from 'vs/base/common/lifecycle'; +import {IThreadService} from 'vs/workbench/services/thread/common/threadService'; +import {ExtHostContext} from './extHost.protocol'; +import {onDidGarbageCollectSignals, consumeSignals, trackGarbageCollection} from 'gc-signals'; + +export class MainThreadHeapMonitor { + + private _subscription: IDisposable; + private _consumeHandle: number; + + constructor( @IThreadService threadService: IThreadService) { + const proxy = threadService.get(ExtHostContext.ExtHostHeapMonitor); + + this._subscription = onDidGarbageCollectSignals(ids => { + proxy.$onGarbageCollection(ids); + }); + + this._consumeHandle = setInterval(consumeSignals, 15 * 1000); + } + + dispose() { + clearInterval(this._consumeHandle); + this._subscription.dispose(); + } + + trackObject(obj: any) { + if (typeof obj.$heap_ident === 'number') { + trackGarbageCollection(obj, obj.$heap_ident); + } + } +} \ No newline at end of file diff --git a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts index 4df3d0f6dc9..f9776ca8a3e 100644 --- a/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/node/mainThreadLanguageFeatures.ts @@ -17,6 +17,7 @@ import {Position as EditorPosition} from 'vs/editor/common/core/position'; import {Range as EditorRange} from 'vs/editor/common/core/range'; import {ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape} from './extHost.protocol'; import {LanguageConfigurationRegistry, LanguageConfiguration} from 'vs/editor/common/modes/languageConfigurationRegistry'; +import {trackGarbageCollection} from 'gc-signals'; export class MainThreadLanguageFeatures extends MainThreadLanguageFeaturesShape { @@ -180,7 +181,12 @@ export class MainThreadLanguageFeatures extends MainThreadLanguageFeaturesShape this._registrations[handle] = modes.SuggestRegistry.register(selector, { triggerCharacters: triggerCharacters, provideCompletionItems: (model:IReadOnlyModel, position:EditorPosition, token:CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideCompletionItems(handle, model.uri, position)); + return wireCancellationToken(token, this._proxy.$provideCompletionItems(handle, model.uri, position)).then(result => { + for (const suggestion of result.suggestions) { + trackGarbageCollection(suggestion, (suggestion).$heap_ident); + } + return result; + }); }, resolveCompletionItem: (model:IReadOnlyModel, position:EditorPosition, suggestion: modes.ISuggestion, token: CancellationToken): Thenable => { return wireCancellationToken(token, this._proxy.$resolveCompletionItem(handle, model.uri, position, suggestion)); diff --git a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts index 168c41180da..f80a1bde549 100644 --- a/src/vs/workbench/test/node/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/node/api/extHostApiCommands.test.ts @@ -23,6 +23,7 @@ import {ExtHostLanguageFeatures} from 'vs/workbench/api/node/extHostLanguageFeat import {MainThreadLanguageFeatures} from 'vs/workbench/api/node/mainThreadLanguageFeatures'; import {registerApiCommands} from 'vs/workbench/api/node/extHostApiCommands'; import {ExtHostCommands} from 'vs/workbench/api/node/extHostCommands'; +import {ExtHostHeapMonitor} from 'vs/workbench/api/node/extHostHeapMonitor'; import {MainThreadCommands} from 'vs/workbench/api/node/mainThreadCommands'; import {ExtHostDocuments} from 'vs/workbench/api/node/extHostDocuments'; import * as ExtHostTypeConverters from 'vs/workbench/api/node/extHostTypeConverters'; @@ -108,7 +109,7 @@ suite('ExtHostLanguageFeatureCommands', function() { const diagnostics = new ExtHostDiagnostics(threadService); threadService.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(threadService, extHostDocuments, commands, diagnostics); + extHost = new ExtHostLanguageFeatures(threadService, extHostDocuments, commands, new ExtHostHeapMonitor(), diagnostics); threadService.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = threadService.setTestInstance(MainContext.MainThreadLanguageFeatures, instantiationService.createInstance(MainThreadLanguageFeatures)); diff --git a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts index f4e2808920f..7cdb832bbbc 100644 --- a/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/node/api/extHostLanguageFeatures.test.ts @@ -39,6 +39,7 @@ import {getLinks} from 'vs/editor/contrib/links/common/links'; import {asWinJsPromise} from 'vs/base/common/async'; import {MainContext, ExtHostContext} from 'vs/workbench/api/node/extHost.protocol'; import {ExtHostDiagnostics} from 'vs/workbench/api/node/extHostDiagnostics'; +import {ExtHostHeapMonitor} from 'vs/workbench/api/node/extHostHeapMonitor'; const defaultSelector = { scheme: 'far' }; const model: EditorCommon.IModel = EditorModel.createFromString( @@ -97,7 +98,7 @@ suite('ExtHostLanguageFeatures', function() { const diagnostics = new ExtHostDiagnostics(threadService); threadService.set(ExtHostContext.ExtHostDiagnostics, diagnostics); - extHost = new ExtHostLanguageFeatures(threadService, extHostDocuments, commands, diagnostics); + extHost = new ExtHostLanguageFeatures(threadService, extHostDocuments, commands, new ExtHostHeapMonitor(), diagnostics); threadService.set(ExtHostContext.ExtHostLanguageFeatures, extHost); mainThread = threadService.setTestInstance(MainContext.MainThreadLanguageFeatures, instantiationService.createInstance(MainThreadLanguageFeatures));