From fc4ac2ccb83337b653de0ed6eefaced65e77e293 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Tue, 27 Nov 2018 17:25:58 +0100 Subject: [PATCH] storage - scaffold a basic global storage service on the main side --- src/vs/code/electron-main/app.ts | 65 +++-- .../lifecycle/electron-main/lifecycleMain.ts | 246 +++++++++++------- .../electron-main/storageMainService.ts | 170 ++++++++++++ .../platform/storage/node/storageService.ts | 7 +- src/vs/workbench/electron-browser/shell.ts | 5 + .../performance/electron-browser/actions.ts | 8 +- .../timer/electron-browser/timerService.ts | 11 + 7 files changed, 388 insertions(+), 124 deletions(-) create mode 100644 src/vs/platform/storage/electron-main/storageMainService.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 0b39dac456a..7e0aaa44e39 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -73,6 +73,7 @@ import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts'; import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from 'vs/platform/remote/node/remoteAgentFileSystemChannel'; import { ResolvedAuthority } from 'vs/platform/remote/common/remoteAuthorityResolver'; import { SnapUpdateService } from 'vs/platform/update/electron-main/updateService.snap'; +import { IStorageMainService, StorageMainService } from 'vs/platform/storage/electron-main/storageMainService'; export class CodeApplication extends Disposable { @@ -425,30 +426,31 @@ export class CodeApplication extends Disposable { this.sharedProcessClient = this.sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main')); // Services - const appInstantiationService = this.initServices(machineId); + return this.initServices(machineId).then(appInstantiationService => { - // Create driver - if (this.environmentService.driverHandle) { - serveDriver(this.electronIpcServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService).then(server => { - this.logService.info('Driver started at:', this.environmentService.driverHandle); - this._register(server); - }); - } + // Create driver + if (this.environmentService.driverHandle) { + serveDriver(this.electronIpcServer, this.environmentService.driverHandle, this.environmentService, appInstantiationService).then(server => { + this.logService.info('Driver started at:', this.environmentService.driverHandle); + this._register(server); + }); + } - // Setup Auth Handler - const authHandler = appInstantiationService.createInstance(ProxyAuthHandler); - this._register(authHandler); + // Setup Auth Handler + const authHandler = appInstantiationService.createInstance(ProxyAuthHandler); + this._register(authHandler); - // Open Windows - const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor)); + // Open Windows + const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor)); - // Post Open Windows Tasks - appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); + // Post Open Windows Tasks + appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor)); - // Tracing: Stop tracing after windows are ready if enabled - if (this.environmentService.args.trace) { - this.stopTracingEventually(windows); - } + // Tracing: Stop tracing after windows are ready if enabled + if (this.environmentService.args.trace) { + this.stopTracingEventually(windows); + } + }); }); } @@ -502,7 +504,7 @@ export class CodeApplication extends Disposable { }); } - private initServices(machineId: string): IInstantiationService { + private initServices(machineId: string): Thenable { const services = new ServiceCollection(); if (process.platform === 'win32') { @@ -522,6 +524,7 @@ export class CodeApplication extends Disposable { services.set(ILaunchService, new SyncDescriptor(LaunchService)); services.set(IIssueService, new SyncDescriptor(IssueService, [machineId, this.userEnv])); services.set(IMenubarService, new SyncDescriptor(MenubarService)); + services.set(IStorageMainService, new SyncDescriptor(StorageMainService)); // Telemetry if (!this.environmentService.isExtensionDevelopment && !this.environmentService.args['disable-telemetry'] && !!product.enableTelemetry) { @@ -536,7 +539,22 @@ export class CodeApplication extends Disposable { services.set(ITelemetryService, NullTelemetryService); } - return this.instantiationService.createChild(services); + const appInstantiationService = this.instantiationService.createChild(services); + + return appInstantiationService.invokeFunction(accessor => this.initStorageService(accessor)).then(() => appInstantiationService); + } + + private initStorageService(accessor: ServicesAccessor): Thenable { + const storageService = accessor.get(IStorageMainService) as StorageMainService; + + // Ensure to close storage on shutdown + this.lifecycleService.onWillShutdown(e => e.join(storageService.close())); + + // Initialize storage service + return storageService.initialize().then(void 0, error => { + errors.onUnexpectedError(error); + this.logService.error(error); + }); } private openFirstWindow(accessor: ServicesAccessor): ICodeWindow[] { @@ -684,8 +702,7 @@ export class CodeApplication extends Disposable { this.historyMainService.onRecentlyOpenedChange(() => this.historyMainService.updateWindowsJumpList()); // Start shared process after a while - const sharedProcess = new RunOnceScheduler(() => getShellEnvironment().then(userEnv => this.sharedProcess.spawn(userEnv)), 3000); - sharedProcess.schedule(); - this._register(sharedProcess); + const sharedProcessSpawn = this._register(new RunOnceScheduler(() => getShellEnvironment().then(userEnv => this.sharedProcess.spawn(userEnv)), 3000)); + sharedProcessSpawn.schedule(); } } diff --git a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts index ceafc989e90..27e8ff891b8 100644 --- a/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts +++ b/src/vs/platform/lifecycle/electron-main/lifecycleMain.ts @@ -12,6 +12,7 @@ import { ICodeWindow } from 'vs/platform/windows/electron-main/windows'; import { handleVetos } from 'vs/platform/lifecycle/common/lifecycle'; import { isMacintosh, isWindows } from 'vs/base/common/platform'; import { Disposable } from 'vs/base/common/lifecycle'; +import { always } from 'vs/base/common/async'; export const ILifecycleService = createDecorator('lifecycleService'); @@ -28,6 +29,15 @@ export interface IWindowUnloadEvent { veto(value: boolean | Thenable): void; } +export interface ShutdownEvent { + + /** + * Allows to join the shutdown. The promise can be a long running operation but it + * will block the application from closing. + */ + join(promise: Thenable): void; +} + export interface ILifecycleService { _serviceBrand: any; @@ -42,9 +52,8 @@ export interface ILifecycleService { quitRequested: boolean; /** - * Due to the way we handle lifecycle with eventing, the general app.on('before-quit') - * event cannot be used because it can be called twice on shutdown. Instead the onBeforeShutdown - * handler in this module can be used and it is only called once on a shutdown sequence. + * An event that fires when the application is about to shutdown before any window is closed. + * The shutdown can still be prevented by any window that vetos this event. */ onBeforeShutdown: Event; @@ -53,37 +62,37 @@ export interface ILifecycleService { * vetoed the shutdown sequence. At this point listeners are ensured that the application will * quit without veto. */ - onWillShutdown: Event; + onWillShutdown: Event; /** - * We provide our own event when we close a window because the general window.on('close') - * is called even when the window prevents the closing. We want an event that truly fires - * before the window gets closed for real. + * An event that fires before a window closes. This event is fired after any veto has been dealt + * with so that listeners know for sure that the window will close without veto. */ onBeforeWindowClose: Event; /** - * An even that can be vetoed to prevent a window from being unloaded. + * An event that fires before a window is about to unload. Listeners can veto this event to prevent + * the window from unloading. */ onBeforeWindowUnload: Event; /** - * Close a window for the provided reason. Shutdown handlers are triggered. + * Unload a window for the provided reason. All lifecycle event handlers are triggered. */ unload(window: ICodeWindow, reason: UnloadReason): Thenable; /** - * Restart the application with optional arguments (CLI). Shutdown handlers are triggered. + * Restart the application with optional arguments (CLI). All lifecycle event handlers are triggered. */ relaunch(options?: { addArgs?: string[], removeArgs?: string[] }): void; /** - * Shutdown the application normally. Shutdown handlers are triggered. + * Shutdown the application normally. All lifecycle event handlers are triggered. */ quit(fromUpdate?: boolean): Thenable; /** - * Forcefully shutdown the application. No shutdown handlers are triggered. + * Forcefully shutdown the application. No livecycle event handlers are triggered. */ kill(code?: number): void; } @@ -95,11 +104,14 @@ export class LifecycleService extends Disposable implements ILifecycleService { private static readonly QUIT_FROM_RESTART_MARKER = 'quit.from.restart'; // use a marker to find out if the session was restarted private windowToCloseRequest: { [windowId: string]: boolean } = Object.create(null); - private pendingQuitPromise: Thenable | null; - private pendingQuitPromiseResolve: { (veto: boolean): void } | null; private oneTimeListenerTokenGenerator = 0; private windowCounter = 0; + private pendingQuitPromise: Thenable | null; + private pendingQuitPromiseResolve: { (veto: boolean): void } | null; + + private pendingWillShutdownPromise: Thenable | null; + private _quitRequested = false; get quitRequested(): boolean { return this._quitRequested; } @@ -109,8 +121,8 @@ export class LifecycleService extends Disposable implements ILifecycleService { private readonly _onBeforeShutdown = this._register(new Emitter()); readonly onBeforeShutdown: Event = this._onBeforeShutdown.event; - private readonly _onWillShutdown = this._register(new Emitter()); - readonly onWillShutdown: Event = this._onWillShutdown.event; + private readonly _onWillShutdown = this._register(new Emitter()); + readonly onWillShutdown: Event = this._onWillShutdown.event; private readonly _onBeforeWindowClose = this._register(new Emitter()); readonly onBeforeWindowClose: Event = this._onBeforeWindowClose.event; @@ -141,15 +153,14 @@ export class LifecycleService extends Disposable implements ILifecycleService { private registerListeners(): void { - // before-quit - app.on('before-quit', e => { - this.logService.trace('Lifecycle#before-quit'); - + // before-quit: an event that is fired if application quit was + // requested but before any window was closed. + const beforeQuitListener = () => { if (this._quitRequested) { - this.logService.trace('Lifecycle#before-quit - returning because quit was already requested'); return; } + this.logService.trace('Lifecycle#app.on(before-quit)'); this._quitRequested = true; // Emit event to indicate that we are about to shutdown @@ -157,25 +168,77 @@ export class LifecycleService extends Disposable implements ILifecycleService { this._onBeforeShutdown.fire(); // macOS: can run without any window open. in that case we fire - // the onShutdown() event directly because there is no veto to be expected. + // the onWillShutdown() event directly because there is no veto + // to be expected. if (isMacintosh && this.windowCounter === 0) { - this.logService.trace('Lifecycle#onShutdown.fire()'); - this._onWillShutdown.fire(); + this.beginOnWillShutdown(); } - }); + }; + app.addListener('before-quit', beforeQuitListener); - // window-all-closed - app.on('window-all-closed', () => { - this.logService.trace('Lifecycle#window-all-closed'); + // window-all-closed: an event that only fires when the last window + // was closed. We override this event to be in charge if app.quit() + // should be called or not. + const windowAllClosedListener = () => { + this.logService.trace('Lifecycle#app.on(window-all-closed)'); // Windows/Linux: we quit when all windows have closed // Mac: we only quit when quit was requested - if (this._quitRequested || process.platform !== 'darwin') { + if (this._quitRequested || !isMacintosh) { app.quit(); } + }; + app.addListener('window-all-closed', windowAllClosedListener); + + // will-quit: an event that is fired after all windows have been + // closed, but before actually quitting. + app.once('will-quit', e => { + this.logService.trace('Lifecycle#app.on(will-quit)'); + + // Prevent the quit until the shutdown promise was resolved + e.preventDefault(); + + // Start shutdown sequence + const shutdownPromise = this.beginOnWillShutdown(); + + // Wait until shutdown is signaled to be complete + always(shutdownPromise, () => { + + // Resolve pending quit promise now without veto + this.resolvePendingQuitPromise(false /* no veto */); + + // Quit again, this time do not prevent this, since our + // will-quit listener is only installed "once". Also + // remove any listener we have that is no longer needed + app.removeListener('before-quit', beforeQuitListener); + app.removeListener('window-all-closed', windowAllClosedListener); + app.quit(); + }); }); } + private beginOnWillShutdown(): Thenable { + if (this.pendingWillShutdownPromise) { + return this.pendingWillShutdownPromise; // shutdown is already running + } + + this.logService.trace('Lifecycle#onWillShutdown.fire()'); + + const joiners: Thenable[] = []; + + this._onWillShutdown.fire({ + join(promise) { + if (promise) { + joiners.push(promise); + } + } + }); + + this.pendingWillShutdownPromise = Promise.all(joiners).then(null, err => this.logService.error(err)); + + return this.pendingWillShutdownPromise; + } + registerWindow(window: ICodeWindow): void { // track window count @@ -183,49 +246,48 @@ export class LifecycleService extends Disposable implements ILifecycleService { // Window Before Closing: Main -> Renderer window.win.on('close', e => { - const windowId = window.id; - this.logService.trace('Lifecycle#window-before-close', windowId); // The window already acknowledged to be closed + const windowId = window.id; if (this.windowToCloseRequest[windowId]) { - this.logService.trace('Lifecycle#window-close', windowId); - delete this.windowToCloseRequest[windowId]; return; } + this.logService.trace(`Lifecycle#window.on('close') - window ID ${window.id}`); + // Otherwise prevent unload and handle it from window e.preventDefault(); this.unload(window, UnloadReason.CLOSE).then(veto => { - if (!veto) { - this.windowToCloseRequest[windowId] = true; - - this.logService.trace('Lifecycle#onBeforeWindowClose.fire()'); - this._onBeforeWindowClose.fire(window); - - window.close(); - } else { - this._quitRequested = false; + if (veto) { delete this.windowToCloseRequest[windowId]; + return; } + + this.windowToCloseRequest[windowId] = true; + + // Fire onBeforeWindowClose before actually closing + this.logService.trace(`Lifecycle#onBeforeWindowClose.fire() - window ID ${windowId}`); + this._onBeforeWindowClose.fire(window); + + // No veto, close window now + window.close(); }); }); // Window After Closing window.win.on('closed', e => { - const windowId = window.id; - this.logService.trace('Lifecycle#window-closed', windowId); + this.logService.trace(`Lifecycle#window.on('closed') - window ID ${window.id}`); // update window count this.windowCounter--; - // if there are no more code windows opened, fire the onShutdown event, unless + // if there are no more code windows opened, fire the onWillShutdown event, unless // we are on macOS where it is perfectly fine to close the last window and // the application continues running (unless quit was actually requested) if (this.windowCounter === 0 && (!isMacintosh || this._quitRequested)) { - this.logService.trace('Lifecycle#onShutdown.fire()'); - this._onWillShutdown.fire(); + this.beginOnWillShutdown(); } }); } @@ -237,44 +299,53 @@ export class LifecycleService extends Disposable implements ILifecycleService { return Promise.resolve(false); } - this.logService.trace('Lifecycle#unload()', window.id); - - const windowUnloadReason = this._quitRequested ? UnloadReason.QUIT : reason; + this.logService.trace(`Lifecycle#unload() - window ID ${window.id}`); // first ask the window itself if it vetos the unload + const windowUnloadReason = this._quitRequested ? UnloadReason.QUIT : reason; return this.onBeforeUnloadWindowInRenderer(window, windowUnloadReason).then(veto => { if (veto) { - this.logService.trace('Lifecycle#unload(): veto in renderer', window.id); + this.logService.trace(`Lifecycle#unload() - veto in renderer (window ID ${window.id})`); - return this.handleVeto(veto); + return this.handleWindowUnloadVeto(veto); } // then check for vetos in the main side return this.onBeforeUnloadWindowInMain(window, windowUnloadReason).then(veto => { if (veto) { - this.logService.trace('Lifecycle#unload(): veto in main', window.id); + this.logService.trace(`Lifecycle#unload() - veto in main (window ID ${window.id})`); - return this.handleVeto(veto); - } else { - this.logService.trace('Lifecycle#unload(): unload continues without veto', window.id); + return this.handleWindowUnloadVeto(veto); } + this.logService.trace(`Lifecycle#unload() - no veto (window ID ${window.id})`); + // finally if there are no vetos, unload the renderer return this.onWillUnloadWindowInRenderer(window, windowUnloadReason).then(() => false); }); }); } - private handleVeto(veto: boolean): boolean { + private handleWindowUnloadVeto(veto: boolean): boolean { + if (!veto) { + return false; // no veto + } - // Any cancellation also cancels a pending quit if present - if (veto && this.pendingQuitPromiseResolve) { - this.pendingQuitPromiseResolve(true /* veto */); + // a veto resolves any pending quit with veto + this.resolvePendingQuitPromise(true /* veto */); + + // a veto resets the pending quit request flag + this._quitRequested = false; + + return true; // veto + } + + private resolvePendingQuitPromise(veto: boolean): void { + if (this.pendingQuitPromiseResolve) { + this.pendingQuitPromiseResolve(veto); this.pendingQuitPromiseResolve = null; this.pendingQuitPromise = null; } - - return veto; } private onBeforeUnloadWindowInRenderer(window: ICodeWindow, reason: UnloadReason): Thenable { @@ -325,38 +396,28 @@ export class LifecycleService extends Disposable implements ILifecycleService { * by the user or not. */ quit(fromUpdate?: boolean): Thenable { - this.logService.trace('Lifecycle#quit()'); - - if (!this.pendingQuitPromise) { - this.pendingQuitPromise = new Promise(resolve => { - - // Store as field to access it from a window cancellation - this.pendingQuitPromiseResolve = resolve; - - // The will-quit event is fired when all windows have closed without veto - app.once('will-quit', () => { - this.logService.trace('Lifecycle#will-quit'); - - if (this.pendingQuitPromiseResolve) { - if (fromUpdate) { - this.stateService.setItem(LifecycleService.QUIT_FROM_RESTART_MARKER, true); - } - - this.pendingQuitPromiseResolve(false /* no veto */); - this.pendingQuitPromiseResolve = null; - this.pendingQuitPromise = null; - } - }); - - // Calling app.quit() will trigger the close handlers of each opened window - // and only if no window vetoed the shutdown, we will get the will-quit event - this.logService.trace('Lifecycle#quit() - calling app.quit()'); - app.quit(); - }); - } else { - this.logService.trace('Lifecycle#quit() - a pending quit was found'); + if (this.pendingQuitPromise) { + return this.pendingQuitPromise; } + this.logService.trace(`Lifecycle#quit() - from update: ${fromUpdate}`); + + // Remember the reason for quit was to restart + if (fromUpdate) { + this.stateService.setItem(LifecycleService.QUIT_FROM_RESTART_MARKER, true); + } + + this.pendingQuitPromise = new Promise(resolve => { + + // Store as field to access it from a window cancellation + this.pendingQuitPromiseResolve = resolve; + + // Calling app.quit() will trigger the close handlers of each opened window + // and only if no window vetoed the shutdown, we will get the will-quit event + this.logService.trace('Lifecycle#quit() - calling app.quit()'); + app.quit(); + }); + return this.pendingQuitPromise; } @@ -400,6 +461,7 @@ export class LifecycleService extends Disposable implements ILifecycleService { } // relaunch after we are sure there is no veto + this.logService.trace('Lifecycle#relaunch() - calling app.relaunch()'); app.relaunch({ args }); } }); diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts new file mode 100644 index 00000000000..09be51db953 --- /dev/null +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -0,0 +1,170 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { Event, Emitter } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { ILogService, LogLevel } from 'vs/platform/log/common/log'; +import { IEnvironmentService } from 'vs/platform/environment/common/environment'; +import { IStorage, Storage, IStorageLoggingOptions, NullStorage } from 'vs/base/node/storage'; +import { join } from 'path'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { mark } from 'vs/base/common/performance'; + +export const IStorageMainService = createDecorator('storageMainService'); + +export interface IStorageMainService { + + _serviceBrand: any; + + /** + * Emitted whenever data is updated or deleted. + */ + readonly onDidChangeStorage: Event; + + /** + * Emitted when the storage is about to persist. This is the right time + * to persist data to ensure it is stored before the application shuts + * down. + */ + readonly onWillSaveState: Event; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. + */ + get(key: string, fallbackValue: string): string; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. The element + * will be converted to a boolean. + */ + getBoolean(key: string, fallbackValue: boolean): boolean; + + /** + * Retrieve an element stored with the given key from storage. Use + * the provided defaultValue if the element is null or undefined. The element + * will be converted to a number using parseInt with a base of 10. + */ + getInteger(key: string, fallbackValue: number): number; + + /** + * Store a string value under the given key to storage. The value will + * be converted to a string. + */ + store(key: string, value: any): void; + + /** + * Delete an element stored under the provided key from storage. + */ + remove(key: string): void; +} + +export interface IStorageChangeEvent { + key: string; +} + +export class StorageMainService extends Disposable implements IStorageMainService { + + _serviceBrand: any; + + private static STORAGE_NAME = 'temp.vscdb'; + + private _onDidChangeStorage: Emitter = this._register(new Emitter()); + get onDidChangeStorage(): Event { return this._onDidChangeStorage.event; } + + private _onWillSaveState: Emitter = this._register(new Emitter()); + get onWillSaveState(): Event { return this._onWillSaveState.event; } + + private storage: IStorage; + + constructor( + @ILogService private logService: ILogService, + @IEnvironmentService environmentService: IEnvironmentService, + @ITelemetryService private telemetryService: ITelemetryService + ) { + super(); + + const useInMemoryStorage = !!environmentService.extensionTestsPath; // no storage during extension tests! + + this.storage = new NullStorage() || new Storage({ + path: useInMemoryStorage ? Storage.IN_MEMORY_PATH : join(environmentService.globalStorageHome, StorageMainService.STORAGE_NAME), + logging: this.createLogginOptions() + }); + + this.registerListeners(); + } + + private createLogginOptions(): IStorageLoggingOptions { + const loggedStorageErrors = new Set(); + + return { + logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : void 0, + logError: error => { + this.logService.error(error); + + const errorStr = `${error}`; + if (!loggedStorageErrors.has(errorStr)) { + loggedStorageErrors.add(errorStr); + + /* __GDPR__ + "sqliteMainStorageError" : { + "storageError": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetryService.publicLog('sqliteMainStorageError', { + 'storageError': errorStr + }); + } + } + } as IStorageLoggingOptions; + } + + private registerListeners(): void { + this._register(this.storage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key }))); + } + + initialize(): Thenable { + mark('main:willInitGlobalStorage'); + return this.storage.init().then(() => { + mark('main:didInitGlobalStorage'); + }, error => { + mark('main:didInitGlobalStorage'); + + return Promise.reject(error); + }); + } + + get(key: string, fallbackValue: string): string { + return this.storage.get(key, fallbackValue); + } + + getBoolean(key: string, fallbackValue: boolean): boolean { + return this.storage.getBoolean(key, fallbackValue); + } + + getInteger(key: string, fallbackValue: number): number { + return this.storage.getInteger(key, fallbackValue); + } + + store(key: string, value: any): Thenable { + return this.storage.set(key, value); + } + + remove(key: string): Thenable { + return this.storage.delete(key); + } + + close(): Thenable { + this.logService.trace('StorageMainService#close() - begin'); + + // Signal as event so that clients can still store data + this._onWillSaveState.fire(); + + // Do it + return this.storage.close().then(() => this.logService.trace('StorageMainService#close() - finished')); + } +} \ No newline at end of file diff --git a/src/vs/platform/storage/node/storageService.ts b/src/vs/platform/storage/node/storageService.ts index d2716f71976..024118b626f 100644 --- a/src/vs/platform/storage/node/storageService.ts +++ b/src/vs/platform/storage/node/storageService.ts @@ -147,10 +147,7 @@ export class StorageService extends Disposable implements IStorageService { } return migrationPromise.then(() => { - mark('willInitGlobalStorage'); - return this.globalStorage.init().then(() => { - mark('didInitGlobalStorage'); - }); + return this.globalStorage.init(); }); }); }); @@ -415,7 +412,7 @@ export class StorageService extends Disposable implements IStorageService { workspaceItemsParsed.set(key, safeParse(value)); }); - console.group(`Storage: Global (integrity: ${result[2]}, load: ${getDuration('willInitGlobalStorage', 'didInitGlobalStorage')}, path: ${this.globalStorageWorkspacePath})`); + console.group(`Storage: Global (integrity: ${result[2]}, load: ${getDuration('main:willInitGlobalStorage', 'main:didInitGlobalStorage')}, path: ${this.globalStorageWorkspacePath})`); let globalValues: { key: string, value: string }[] = []; globalItems.forEach((value, key) => { globalValues.push({ key, value }); diff --git a/src/vs/workbench/electron-browser/shell.ts b/src/vs/workbench/electron-browser/shell.ts index c83e600553e..382df363dd3 100644 --- a/src/vs/workbench/electron-browser/shell.ts +++ b/src/vs/workbench/electron-browser/shell.ts @@ -292,6 +292,7 @@ export class WorkbenchShell extends Disposable { const workbenchReadyDuration = perf.getDuration(initialStartup ? 'main:started' : 'main:loadWindow', 'didStartWorkbench'); const workspaceStorageRequireDuration = perf.getDuration('willRequireSQLite', 'didRequireSQLite'); const workspaceStorageSchemaDuration = perf.getDuration('willSetupSQLiteSchema', 'didSetupSQLiteSchema'); + const globalStorageInitDuration = perf.getDuration('main:willInitGlobalStorage', 'main:didInitGlobalStorage'); const workspaceStorageInitDuration = perf.getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage'); const workspaceStorageFileExistsDuration = perf.getDuration('willCheckWorkspaceStorageExists', 'didCheckWorkspaceStorageExists'); const workspaceStorageMigrationDuration = perf.getDuration('willMigrateWorkspaceStorageKeys', 'didMigrateWorkspaceStorageKeys'); @@ -314,6 +315,7 @@ export class WorkbenchShell extends Disposable { "workspaceMigrationTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceRequireTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceSchemaTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "globalReadTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceReadTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "localStorageTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workbenchRequireTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -329,6 +331,7 @@ export class WorkbenchShell extends Disposable { 'workspaceMigrationTime': workspaceStorageMigrationDuration, 'workspaceRequireTime': workspaceStorageRequireDuration, 'workspaceSchemaTime': workspaceStorageSchemaDuration, + 'globalReadTime': globalStorageInitDuration, 'workspaceReadTime': workspaceStorageInitDuration, 'localStorageTime': localStorageDuration, 'workbenchRequireTime': workbenchLoadDuration, @@ -356,6 +359,7 @@ export class WorkbenchShell extends Disposable { "workspaceMigrationTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceRequireTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceSchemaTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, + "globalReadTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workspaceReadTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "localStorageTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, "workbenchRequireTime" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }, @@ -370,6 +374,7 @@ export class WorkbenchShell extends Disposable { 'workspaceMigrationTime': workspaceStorageMigrationDuration, 'workspaceRequireTime': workspaceStorageRequireDuration, 'workspaceSchemaTime': workspaceStorageSchemaDuration, + 'globalReadTime': globalStorageInitDuration, 'workspaceReadTime': workspaceStorageInitDuration, 'localStorageTime': localStorageDuration, 'workbenchRequireTime': workbenchLoadDuration, diff --git a/src/vs/workbench/parts/performance/electron-browser/actions.ts b/src/vs/workbench/parts/performance/electron-browser/actions.ts index ca7faa0975a..501b2020d7c 100644 --- a/src/vs/workbench/parts/performance/electron-browser/actions.ts +++ b/src/vs/workbench/parts/performance/electron-browser/actions.ts @@ -26,9 +26,11 @@ class Info { static getTimerInfo(metrics: IStartupMetrics, nodeModuleLoadTime?: number): { [name: string]: Info } { const table: { [name: string]: Info } = Object.create(null); - table['start => app.isReady'] = new Info(metrics.timers.ellapsedAppReady, '[main]', metrics.initialStartup); - table['nls:start => nls:end'] = new Info(metrics.timers.ellapsedNlsGeneration, '[main]', metrics.initialStartup); - table['app.isReady => window.loadUrl()'] = new Info(metrics.timers.ellapsedWindowLoad, '[main]', metrics.initialStartup); + table['start => app.isReady'] = new Info(metrics.timers.ellapsedAppReady, '[main]', `initial startup: ${metrics.initialStartup}`); + table['nls:start => nls:end'] = new Info(metrics.timers.ellapsedNlsGeneration, '[main]', `initial startup: ${metrics.initialStartup}`); + table['app.isReady => window.loadUrl()'] = new Info(metrics.timers.ellapsedWindowLoad, '[main]', `initial startup: ${metrics.initialStartup}`); + + table['init global storage'] = new Info(metrics.timers.ellapsedGlobalStorageInit, '[main]', `initial startup: ${metrics.initialStartup}`); table['window.loadUrl() => begin to require(workbench.main.js)'] = new Info(metrics.timers.ellapsedWindowLoadToRequire, '[main->renderer]', StartupKindToString(metrics.windowKind)); table['require(workbench.main.js)'] = new Info(metrics.timers.ellapsedRequire, '[renderer]', `cached data: ${(metrics.didUseCachedData ? 'YES' : 'NO')}${nodeModuleLoadTime ? `, node_modules took ${nodeModuleLoadTime}ms` : ''}`); diff --git a/src/vs/workbench/services/timer/electron-browser/timerService.ts b/src/vs/workbench/services/timer/electron-browser/timerService.ts index 4dfa2575cdc..cf1eb40fbda 100644 --- a/src/vs/workbench/services/timer/electron-browser/timerService.ts +++ b/src/vs/workbench/services/timer/electron-browser/timerService.ts @@ -53,6 +53,7 @@ export interface IMemoryInfo { "timers.ellapsedExtensions" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedExtensionsReady" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, + "timers.ellapsedGlobalStorageInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedWorkspaceStorageRequire" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedWorkspaceStorageInit" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, "timers.ellapsedViewletRestore" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }, @@ -194,6 +195,15 @@ export interface IStartupMetrics { */ ellapsedWindowLoadToRequire: number; + /** + * The time it took to require the global storage DB, connect to it + * and load the initial set of values. + * + * * Happens in the main-process + * * Measured with the `main:willInitGlobalStorage` and `main:didInitGlobalStorage` performance marks. + */ + ellapsedGlobalStorageInit: number; + /** * The time it took to require the workspace storage DB. * @@ -388,6 +398,7 @@ class TimerService implements ITimerService { ellapsedWindowLoad: initialStartup ? perf.getDuration('main:appReady', 'main:loadWindow') : undefined, ellapsedWindowLoadToRequire: perf.getDuration('main:loadWindow', 'willLoadWorkbenchMain'), ellapsedRequire: perf.getDuration('willLoadWorkbenchMain', 'didLoadWorkbenchMain'), + ellapsedGlobalStorageInit: perf.getDuration('main:willInitGlobalStorage', 'main:didInitGlobalStorage'), ellapsedWorkspaceStorageRequire: perf.getDuration('willRequireSQLite', 'didRequireSQLite'), ellapsedWorkspaceStorageInit: perf.getDuration('willInitWorkspaceStorage', 'didInitWorkspaceStorage'), ellapsedExtensions: perf.getDuration('willLoadExtensions', 'didLoadExtensions'),