Skip to content

Commit 54ab792

Browse files
author
Benjamin Pasero
authored
state - periodically save state to prevent loss on crash or shutdown (microsoft#87158)
1 parent 8a78c06 commit 54ab792

7 files changed

Lines changed: 123 additions & 46 deletions

File tree

src/vs/editor/contrib/multicursor/test/multicursor.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ suite('Multicursor selection', () => {
6969
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
7070
remove: (key) => undefined,
7171
logStorage: () => undefined,
72-
migrate: (toWorkspace) => Promise.resolve(undefined)
72+
migrate: (toWorkspace) => Promise.resolve(undefined),
73+
flush: () => undefined
7374
} as IStorageService);
7475

7576
test('issue #8817: Cursor position changes when you cancel multicursor', () => {

src/vs/platform/storage/browser/storageService.ts

Lines changed: 54 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { Disposable } from 'vs/base/common/lifecycle';
6+
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
77
import { Event, Emitter } from 'vs/base/common/event';
88
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
99
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -21,11 +21,11 @@ export class BrowserStorageService extends Disposable implements IStorageService
2121

2222
_serviceBrand: undefined;
2323

24-
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
25-
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent> = this._onDidChangeStorage.event;
24+
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
25+
readonly onDidChangeStorage = this._onDidChangeStorage.event;
2626

27-
private readonly _onWillSaveState: Emitter<IWillSaveStateEvent> = this._register(new Emitter<IWillSaveStateEvent>());
28-
readonly onWillSaveState: Event<IWillSaveStateEvent> = this._onWillSaveState.event;
27+
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
28+
readonly onWillSaveState = this._onWillSaveState.event;
2929

3030
private globalStorage: IStorage | undefined;
3131
private workspaceStorage: IStorage | undefined;
@@ -37,45 +37,15 @@ export class BrowserStorageService extends Disposable implements IStorageService
3737
private workspaceStorageFile: URI | undefined;
3838

3939
private initializePromise: Promise<void> | undefined;
40-
private periodicSaveScheduler = this._register(new RunOnceScheduler(() => this.collectState(), 5000));
4140

42-
get hasPendingUpdate(): boolean {
43-
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
44-
}
41+
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */));
42+
private runWhenIdleDisposable: IDisposable | undefined = undefined;
4543

4644
constructor(
4745
@IEnvironmentService private readonly environmentService: IEnvironmentService,
4846
@IFileService private readonly fileService: IFileService
4947
) {
5048
super();
51-
52-
// In the browser we do not have support for long running unload sequences. As such,
53-
// we cannot ask for saving state in that moment, because that would result in a
54-
// long running operation.
55-
// Instead, periodically ask customers to save save. The library will be clever enough
56-
// to only save state that has actually changed.
57-
this.periodicSaveScheduler.schedule();
58-
}
59-
60-
private collectState(): void {
61-
runWhenIdle(() => {
62-
63-
// this event will potentially cause new state to be stored
64-
// since new state will only be created while the document
65-
// has focus, one optimization is to not run this when the
66-
// document has no focus, assuming that state has not changed
67-
//
68-
// another optimization is to not collect more state if we
69-
// have a pending update already running which indicates
70-
// that the connection is either slow or disconnected and
71-
// thus unhealthy.
72-
if (document.hasFocus() && !this.hasPendingUpdate) {
73-
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
74-
}
75-
76-
// repeat
77-
this.periodicSaveScheduler.schedule();
78-
});
7949
}
8050

8151
initialize(payload: IWorkspaceInitializationPayload): Promise<void> {
@@ -109,6 +79,13 @@ export class BrowserStorageService extends Disposable implements IStorageService
10979
this.workspaceStorage.init(),
11080
this.globalStorage.init()
11181
]);
82+
83+
// In the browser we do not have support for long running unload sequences. As such,
84+
// we cannot ask for saving state in that moment, because that would result in a
85+
// long running operation.
86+
// Instead, periodically ask customers to save save. The library will be clever enough
87+
// to only save state that has actually changed.
88+
this.periodicFlushScheduler.schedule();
11289
}
11390

11491
get(key: string, scope: StorageScope, fallbackValue: string): string;
@@ -156,6 +133,40 @@ export class BrowserStorageService extends Disposable implements IStorageService
156133
throw new Error('Migrating storage is currently unsupported in Web');
157134
}
158135

136+
private doFlushWhenIdle(): void {
137+
138+
// Dispose any previous idle runner
139+
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
140+
141+
// Run when idle
142+
this.runWhenIdleDisposable = runWhenIdle(() => {
143+
144+
// this event will potentially cause new state to be stored
145+
// since new state will only be created while the document
146+
// has focus, one optimization is to not run this when the
147+
// document has no focus, assuming that state has not changed
148+
//
149+
// another optimization is to not collect more state if we
150+
// have a pending update already running which indicates
151+
// that the connection is either slow or disconnected and
152+
// thus unhealthy.
153+
if (document.hasFocus() && !this.hasPendingUpdate) {
154+
this.flush();
155+
}
156+
157+
// repeat
158+
this.periodicFlushScheduler.schedule();
159+
});
160+
}
161+
162+
get hasPendingUpdate(): boolean {
163+
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
164+
}
165+
166+
flush(): void {
167+
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
168+
}
169+
159170
close(): void {
160171
// We explicitly do not close our DBs because writing data onBeforeUnload()
161172
// can result in unexpected results. Namely, it seems that - even though this
@@ -167,6 +178,12 @@ export class BrowserStorageService extends Disposable implements IStorageService
167178
// get triggered in this phase.
168179
this.dispose();
169180
}
181+
182+
dispose(): void {
183+
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
184+
185+
super.dispose();
186+
}
170187
}
171188

172189
export class FileStorageDatabase extends Disposable implements IStorageDatabase {

src/vs/platform/storage/common/storage.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,13 @@ export interface IStorageService {
102102
* Migrate the storage contents to another workspace.
103103
*/
104104
migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
105+
106+
/**
107+
* Allows to flush state, e.g. in cases where a shutdown is
108+
* imminent. This will send out the onWillSaveState to ask
109+
* everyone for latest state.
110+
*/
111+
flush(): void;
105112
}
106113

107114
export const enum StorageScope {
@@ -126,10 +133,11 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
126133

127134
_serviceBrand: undefined;
128135

129-
private readonly _onDidChangeStorage: Emitter<IWorkspaceStorageChangeEvent> = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
130-
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent> = this._onDidChangeStorage.event;
136+
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
137+
readonly onDidChangeStorage = this._onDidChangeStorage.event;
131138

132-
readonly onWillSaveState = Event.None;
139+
protected readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
140+
readonly onWillSaveState = this._onWillSaveState.event;
133141

134142
private globalCache: Map<string, string> = new Map<string, string>();
135143
private workspaceCache: Map<string, string> = new Map<string, string>();
@@ -215,6 +223,10 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
215223
async migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void> {
216224
// not supported
217225
}
226+
227+
flush(): void {
228+
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
229+
}
218230
}
219231

220232
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {

src/vs/platform/storage/node/storageService.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
1616
import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
1717
import { onUnexpectedError } from 'vs/base/common/errors';
1818
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
19+
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
1920

2021
export class NativeStorageService extends Disposable implements IStorageService {
2122

@@ -38,6 +39,9 @@ export class NativeStorageService extends Disposable implements IStorageService
3839

3940
private initializePromise: Promise<void> | undefined;
4041

42+
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 60000 /* every minute */));
43+
private runWhenIdleDisposable: IDisposable | undefined = undefined;
44+
4145
constructor(
4246
globalStorageDatabase: IStorageDatabase,
4347
@ILogService private readonly logService: ILogService,
@@ -63,10 +67,17 @@ export class NativeStorageService extends Disposable implements IStorageService
6367
}
6468

6569
private async doInitialize(payload: IWorkspaceInitializationPayload): Promise<void> {
70+
71+
// Init all storage locations
6672
await Promise.all([
6773
this.initializeGlobalStorage(),
6874
this.initializeWorkspaceStorage(payload)
6975
]);
76+
77+
// On some OS we do not get enough time to persist state on shutdown (e.g. when
78+
// Windows restarts after applying updates). In other cases, VSCode might crash,
79+
// so we periodically save state to reduce the chance of loosing any state.
80+
this.periodicFlushScheduler.schedule();
7081
}
7182

7283
private initializeGlobalStorage(): Promise<void> {
@@ -185,8 +196,32 @@ export class NativeStorageService extends Disposable implements IStorageService
185196
this.getStorage(scope).delete(key);
186197
}
187198

199+
private doFlushWhenIdle(): void {
200+
201+
// Dispose any previous idle runner
202+
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
203+
204+
// Run when idle
205+
this.runWhenIdleDisposable = runWhenIdle(() => {
206+
207+
// send event to collect state
208+
this.flush();
209+
210+
// repeat
211+
this.periodicFlushScheduler.schedule();
212+
});
213+
}
214+
215+
flush(): void {
216+
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
217+
}
218+
188219
async close(): Promise<void> {
189220

221+
// Stop periodic scheduler and idle runner as we now collect state normally
222+
this.periodicFlushScheduler.dispose();
223+
this.runWhenIdleDisposable = dispose(this.runWhenIdleDisposable);
224+
190225
// Signal as event so that clients can still store data
191226
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
192227

src/vs/workbench/browser/parts/editor/textEditor.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ import { IEditorGroupsService, IEditorGroup } from 'vs/workbench/services/editor
2323
import { CancellationToken } from 'vs/base/common/cancellation';
2424
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2525

26-
const TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState';
27-
2826
export interface IEditorConfiguration {
2927
editor: object;
3028
diffEditor: object;
@@ -35,6 +33,9 @@ export interface IEditorConfiguration {
3533
* be subclassed and not instantiated.
3634
*/
3735
export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
36+
37+
static readonly TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'textEditorViewState';
38+
3839
private editorControl: IEditor | undefined;
3940
private editorContainer: HTMLElement | undefined;
4041
private hasPendingConfigurationChange: boolean | undefined;
@@ -53,7 +54,7 @@ export abstract class BaseTextEditor extends BaseEditor implements ITextEditor {
5354
) {
5455
super(id, telemetryService, themeService, storageService);
5556

56-
this.editorMemento = this.getEditorMemento<IEditorViewState>(editorGroupService, TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100);
57+
this.editorMemento = this.getEditorMemento<IEditorViewState>(editorGroupService, BaseTextEditor.TEXT_EDITOR_VIEW_STATE_PREFERENCE_KEY, 100);
5758

5859
this._register(this.configurationService.onDidChangeConfiguration(e => {
5960
const resource = this.getResource();

src/vs/workbench/browser/workbench.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { WorkbenchContextKeysHandler } from 'vs/workbench/browser/contextkeys';
4444
import { coalesce } from 'vs/base/common/arrays';
4545
import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService';
4646
import { Layout } from 'vs/workbench/browser/layout';
47+
import { IHostService } from 'vs/workbench/services/host/browser/host';
4748

4849
export class Workbench extends Layout {
4950

@@ -140,6 +141,7 @@ export class Workbench extends Layout {
140141
const lifecycleService = accessor.get(ILifecycleService);
141142
const storageService = accessor.get(IStorageService);
142143
const configurationService = accessor.get(IConfigurationService);
144+
const hostService = accessor.get(IHostService);
143145

144146
// Layout
145147
this.initLayout(accessor);
@@ -151,7 +153,7 @@ export class Workbench extends Layout {
151153
this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));
152154

153155
// Register Listeners
154-
this.registerListeners(lifecycleService, storageService, configurationService);
156+
this.registerListeners(lifecycleService, storageService, configurationService, hostService);
155157

156158
// Render Workbench
157159
this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
@@ -224,7 +226,8 @@ export class Workbench extends Layout {
224226
private registerListeners(
225227
lifecycleService: ILifecycleService,
226228
storageService: IStorageService,
227-
configurationService: IConfigurationService
229+
configurationService: IConfigurationService,
230+
hostService: IHostService
228231
): void {
229232

230233
// Configuration changes
@@ -248,6 +251,13 @@ export class Workbench extends Layout {
248251
this._onShutdown.fire();
249252
this.dispose();
250253
}));
254+
255+
// In some environments we do not get enough time to persist state on shutdown.
256+
// In other cases, VSCode might crash, so we periodically save state to reduce
257+
// the chance of loosing any state.
258+
// The window loosing focus is a good indication that the user has stopped working
259+
// in that window so we pick that at a time to collect state.
260+
this._register(hostService.onDidChangeFocus(focus => { if (!focus) { storageService.flush(); } }));
251261
}
252262

253263
private fontAliasing: 'default' | 'antialiased' | 'none' | 'auto' | undefined;

src/vs/workbench/electron-browser/window.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,7 @@ export class ElectronWindow extends Disposable {
283283
}));
284284
}
285285

286+
// Detect minimize / maximize
286287
this._register(Event.any(
287288
Event.map(Event.filter(this.electronService.onWindowMaximize, id => id === this.electronEnvironmentService.windowId), () => true),
288289
Event.map(Event.filter(this.electronService.onWindowUnmaximize, id => id === this.electronEnvironmentService.windowId), () => false)

0 commit comments

Comments
 (0)