Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf - no longer block for backup metadata writes #160152

Merged
merged 11 commits into from Sep 7, 2022
31 changes: 22 additions & 9 deletions src/vs/base/common/async.ts
Expand Up @@ -1227,25 +1227,30 @@ export async function retry<T>(task: ITask<Promise<T>>, delay: number, retries:
//#region Task Sequentializer

interface IPendingTask {
taskId: number;
cancel: () => void;
promise: Promise<void>;
readonly taskId: number;
readonly cancel: () => void;
readonly promise: Promise<void>;
}

interface ISequentialTask {
promise: Promise<void>;
promiseResolve: () => void;
promiseReject: (error: Error) => void;
interface INextTask {
readonly promise: Promise<void>;
readonly promiseResolve: () => void;
readonly promiseReject: (error: Error) => void;
run: () => Promise<void>;
}

export interface ITaskSequentializerWithPendingTask {
readonly pending: Promise<void>;
}

export interface ITaskSequentializerWithNextTask {
readonly next: INextTask;
}

export class TaskSequentializer {

private _pending?: IPendingTask;
private _next?: ISequentialTask;
private _next?: INextTask;

hasPending(taskId?: number): this is ITaskSequentializerWithPendingTask {
if (!this._pending) {
Expand All @@ -1260,7 +1265,7 @@ export class TaskSequentializer {
}

get pending(): Promise<void> | undefined {
return this._pending ? this._pending.promise : undefined;
return this._pending?.promise;
}

cancelPending(): void {
Expand Down Expand Up @@ -1324,6 +1329,14 @@ export class TaskSequentializer {

return this._next.promise;
}

hasNext(): this is ITaskSequentializerWithNextTask {
return !!this._next;
}

async join(): Promise<void> {
return this._next?.promise ?? this._pending?.promise;
}
}

//#endregion
Expand Down
6 changes: 5 additions & 1 deletion src/vs/base/node/pfs.ts
Expand Up @@ -66,7 +66,11 @@ async function rimrafMove(path: string): Promise<void> {
// https://github.com/microsoft/vscode/issues/139908
await fs.promises.rename(path, pathInTemp);
} catch (error) {
return rimrafUnlink(path); // if rename fails, delete without tmp dir
if (error.code === 'ENOENT') {
return; // ignore - path to delete did not exist
}

return rimrafUnlink(path); // otherwise fallback to unlink
}

// Delete but do not return as promise
Expand Down
42 changes: 42 additions & 0 deletions src/vs/base/test/common/async.test.ts
Expand Up @@ -667,6 +667,7 @@ suite('Async', () => {
const sequentializer = new async.TaskSequentializer();

assert.ok(!sequentializer.hasPending());
assert.ok(!sequentializer.hasNext());
assert.ok(!sequentializer.hasPending(2323));
assert.ok(!sequentializer.pending);

Expand All @@ -675,11 +676,13 @@ suite('Async', () => {
assert.ok(!sequentializer.hasPending());
assert.ok(!sequentializer.hasPending(1));
assert.ok(!sequentializer.pending);
assert.ok(!sequentializer.hasNext());

// pending removes itself after done (use async.timeout)
sequentializer.setPending(2, async.timeout(1));
assert.ok(sequentializer.hasPending());
assert.ok(sequentializer.hasPending(2));
assert.ok(!sequentializer.hasNext());
assert.strictEqual(sequentializer.hasPending(1), false);
assert.ok(sequentializer.pending);

Expand All @@ -699,9 +702,12 @@ suite('Async', () => {
let nextDone = false;
const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; }));

assert.ok(sequentializer.hasNext());

await res;
assert.ok(pendingDone);
assert.ok(nextDone);
assert.ok(!sequentializer.hasNext());
});

test('pending and next (finishes after timeout)', async function () {
Expand All @@ -717,6 +723,42 @@ suite('Async', () => {
await res;
assert.ok(pendingDone);
assert.ok(nextDone);
assert.ok(!sequentializer.hasNext());
});

test('join (without next or pending)', async function () {
const sequentializer = new async.TaskSequentializer();

await sequentializer.join();
assert.ok(!sequentializer.hasNext());
});

test('join (without next)', async function () {
const sequentializer = new async.TaskSequentializer();

let pendingDone = false;
sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; }));

await sequentializer.join();
assert.ok(pendingDone);
assert.ok(!sequentializer.hasPending());
});

test('join (with next and pending)', async function () {
const sequentializer = new async.TaskSequentializer();

let pendingDone = false;
sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; }));

// next finishes after async.timeout
let nextDone = false;
sequentializer.setNext(() => async.timeout(1).then(() => { nextDone = true; return; }));

await sequentializer.join();
assert.ok(pendingDone);
assert.ok(nextDone);
assert.ok(!sequentializer.hasPending());
assert.ok(!sequentializer.hasNext());
});

test('pending and multiple next (last one wins)', async function () {
Expand Down
10 changes: 10 additions & 0 deletions src/vs/base/test/node/pfs/pfs.test.ts
Expand Up @@ -90,6 +90,16 @@ flakySuite('PFS', function () {
assert.ok(!fs.existsSync(testDir));
});

test('rimraf - path does not exist - move', async () => {
const nonExistingDir = join(testDir, 'unknown-move');
await Promises.rm(nonExistingDir, RimRafMode.MOVE);
});

test('rimraf - path does not exist - unlink', async () => {
const nonExistingDir = join(testDir, 'unknown-unlink');
await Promises.rm(nonExistingDir, RimRafMode.UNLINK);
});

test('rimraf - recursive folder structure - unlink', async () => {
fs.writeFileSync(join(testDir, 'somefile.txt'), 'Contents');
fs.writeFileSync(join(testDir, 'someOtherFile.txt'), 'Contents');
Expand Down
Expand Up @@ -39,7 +39,6 @@ export class ExtensionsCleaner extends Disposable {
ExtensionStorageService.removeOutdatedExtensionVersions(extensionManagementService, storageService);
this._register(instantiationService.createInstance(ProfileExtensionsCleaner));
}

}

class ProfileExtensionsCleaner extends Disposable {
Expand Down Expand Up @@ -184,5 +183,4 @@ class ProfileExtensionsCleaner extends Disposable {
const [id, version] = getIdAndVersion(key);
return version ? { identifier: { id }, version } : undefined;
}

}
Expand Up @@ -8,22 +8,20 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { Disposable } from 'vs/base/common/lifecycle';
import { join } from 'vs/base/common/path';
import { Promises } from 'vs/base/node/pfs';
import { IBackupWorkspacesFormat } from 'vs/platform/backup/node/backup';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { ILogService } from 'vs/platform/log/common/log';
import { INativeHostService } from 'vs/platform/native/electron-sandbox/native';
import { EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE } from 'vs/platform/workspace/common/workspace';

export class StorageDataCleaner extends Disposable {
export class UnusedWorkspaceStorageDataCleaner extends Disposable {

// Workspace/Folder storage names are MD5 hashes (128bits / 4 due to hex presentation)
private static readonly NON_EMPTY_WORKSPACE_ID_LENGTH = 128 / 4;

// Reserved empty window workspace storage name when in extension development
private static readonly EXTENSION_DEV_EMPTY_WINDOW_ID = 'ext-dev';

constructor(
private readonly backupWorkspacesPath: string,
@INativeEnvironmentService private readonly environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService
@ILogService private readonly logService: ILogService,
@INativeHostService private readonly nativeHostService: INativeHostService
) {
super();

Expand All @@ -34,29 +32,26 @@ export class StorageDataCleaner extends Disposable {
}

private async cleanUpStorage(): Promise<void> {
this.logService.trace('[storage cleanup]: Starting to clean up storage folders.');
this.logService.trace('[storage cleanup]: Starting to clean up workspace storage folders for unused empty workspaces.');

try {
const workspaceStorageFolders = await Promises.readdir(this.environmentService.workspaceStorageHome.fsPath);

// Leverage the backup workspace file to find out which empty workspace is currently in use to
// determine which empty workspace storage can safely be deleted
const contents = await Promises.readFile(this.backupWorkspacesPath, 'utf8');
await Promise.all(workspaceStorageFolders.map(async workspaceStorageFolder => {
if (workspaceStorageFolder.length === UnusedWorkspaceStorageDataCleaner.NON_EMPTY_WORKSPACE_ID_LENGTH) {
return; // keep workspace storage for folders/workspaces that can be accessed still
}

const workspaces = JSON.parse(contents) as IBackupWorkspacesFormat;
const emptyWorkspaces = workspaces.emptyWorkspaceInfos.map(emptyWorkspace => emptyWorkspace.backupFolder);
if (workspaceStorageFolder === EXTENSION_DEVELOPMENT_EMPTY_WINDOW_WORKSPACE.id) {
return; // keep workspace storage for empty extension development workspaces
}

// Read all workspace storage folders that exist & cleanup unused
const workspaceStorageFolders = await Promises.readdir(this.environmentService.workspaceStorageHome.fsPath);
await Promise.all(workspaceStorageFolders.map(async workspaceStorageFolder => {
if (
workspaceStorageFolder.length === StorageDataCleaner.NON_EMPTY_WORKSPACE_ID_LENGTH || // keep non-empty workspaces
workspaceStorageFolder === StorageDataCleaner.EXTENSION_DEV_EMPTY_WINDOW_ID || // keep empty extension dev workspaces
emptyWorkspaces.indexOf(workspaceStorageFolder) >= 0 // keep empty workspaces that are in use
) {
return;
const windows = await this.nativeHostService.getWindows();
if (windows.some(window => window.workspace?.id === workspaceStorageFolder)) {
return; // keep workspace storage for empty workspaces opened as window
}

this.logService.trace(`[storage cleanup]: Deleting workspace storage folder ${workspaceStorageFolder}.`);
this.logService.trace(`[storage cleanup]: Deleting workspace storage folder ${workspaceStorageFolder} as it seems to be an unused empty workspace.`);

await Promises.rm(join(this.environmentService.workspaceStorageHome.fsPath, workspaceStorageFolder));
}));
Expand Down
Expand Up @@ -13,10 +13,10 @@ export class UserDataProfilesCleaner extends Disposable {
@IUserDataProfilesService userDataProfilesService: IUserDataProfilesService
) {
super();

const scheduler = this._register(new RunOnceScheduler(() => {
userDataProfilesService.cleanUp();
}, 10 * 1000 /* after 10s */));
scheduler.schedule();
}

}
Expand Up @@ -18,7 +18,7 @@ import { ExtensionsCleaner } from 'vs/code/electron-browser/sharedProcess/contri
import { LanguagePackCachedDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/languagePackCachedDataCleaner';
import { LocalizationsUpdater } from 'vs/code/electron-browser/sharedProcess/contrib/localizationsUpdater';
import { LogsDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/logsDataCleaner';
import { StorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner';
import { UnusedWorkspaceStorageDataCleaner } from 'vs/code/electron-browser/sharedProcess/contrib/storageDataCleaner';
import { IChecksumService } from 'vs/platform/checksum/common/checksumService';
import { ChecksumService } from 'vs/platform/checksum/node/checksumService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
Expand Down Expand Up @@ -167,7 +167,7 @@ class SharedProcessMain extends Disposable {
this._register(combinedDisposable(
instantiationService.createInstance(CodeCacheCleaner, this.configuration.codeCachePath),
instantiationService.createInstance(LanguagePackCachedDataCleaner),
instantiationService.createInstance(StorageDataCleaner, this.configuration.backupWorkspacesPath),
instantiationService.createInstance(UnusedWorkspaceStorageDataCleaner),
instantiationService.createInstance(LogsDataCleaner),
instantiationService.createInstance(LocalizationsUpdater),
instantiationService.createInstance(ExtensionsCleaner),
Expand Down
2 changes: 1 addition & 1 deletion src/vs/code/electron-main/app.ts
Expand Up @@ -692,7 +692,7 @@ export class CodeApplication extends Disposable {
}

// Backups
const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService);
const backupMainService = new BackupMainService(this.environmentMainService, this.configurationService, this.logService, this.lifecycleMainService);
services.set(IBackupMainService, backupMainService);

// URL handling
Expand Down
10 changes: 6 additions & 4 deletions src/vs/platform/backup/common/backup.ts
Expand Up @@ -6,14 +6,16 @@
import { URI } from 'vs/base/common/uri';
import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';

export interface IWorkspaceBackupInfo {
readonly workspace: IWorkspaceIdentifier;
export interface IBaseBackupInfo {
readonly remoteAuthority?: string;
}

export interface IFolderBackupInfo {
export interface IWorkspaceBackupInfo extends IBaseBackupInfo {
readonly workspace: IWorkspaceIdentifier;
}

export interface IFolderBackupInfo extends IBaseBackupInfo {
readonly folderUri: URI;
readonly remoteAuthority?: string;
}

export function isFolderBackupInfo(curr: IWorkspaceBackupInfo | IFolderBackupInfo): curr is IFolderBackupInfo {
Expand Down
17 changes: 8 additions & 9 deletions src/vs/platform/backup/electron-main/backup.ts
Expand Up @@ -12,21 +12,20 @@ import { IWorkspaceIdentifier } from 'vs/platform/workspace/common/workspace';
export const IBackupMainService = createDecorator<IBackupMainService>('backupMainService');

export interface IBackupMainService {

readonly _serviceBrand: undefined;

isHotExitEnabled(): boolean;

getWorkspaceBackups(): IWorkspaceBackupInfo[];
getFolderBackupPaths(): IFolderBackupInfo[];
getEmptyWindowBackupPaths(): IEmptyWindowBackupInfo[];
getEmptyWindowBackups(): IEmptyWindowBackupInfo[];

registerWorkspaceBackupSync(workspace: IWorkspaceBackupInfo, migrateFrom?: string): string;
registerFolderBackupSync(folderUri: IFolderBackupInfo): string;
registerEmptyWindowBackupSync(backupFolder?: string, remoteAuthority?: string): string;
registerWorkspaceBackup(workspace: IWorkspaceBackupInfo, migrateFrom?: string): string;
registerFolderBackup(folderUri: IFolderBackupInfo): string;
registerEmptyWindowBackup(backupFolder?: string, remoteAuthority?: string): string;

unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackupSync(folderUri: URI): void;
unregisterEmptyWindowBackupSync(backupFolder: string): void;
unregisterWorkspaceBackup(workspace: IWorkspaceIdentifier): void;
unregisterFolderBackup(folderUri: URI): void;
unregisterEmptyWindowBackup(backupFolder: string): void;

/**
* All folders or workspaces that are known to have
Expand Down