Skip to content

Commit

Permalink
files - use ternary search tree for file changes events
Browse files Browse the repository at this point in the history
  • Loading branch information
bpasero committed Oct 6, 2020
1 parent a781d8f commit bb3aaa7
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 65 deletions.
8 changes: 6 additions & 2 deletions src/vs/platform/files/common/fileService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export class FileService extends Disposable implements IFileService {

// Forward events from provider
const providerDisposables = new DisposableStore();
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, this.getExtUri(provider).extUri))));
providerDisposables.add(provider.onDidChangeFile(changes => this._onDidFilesChange.fire(new FileChangesEvent(changes, !this.isPathCaseSensitive(provider)))));
providerDisposables.add(provider.onDidChangeCapabilities(() => this._onDidChangeFileSystemProviderCapabilities.fire({ provider, scheme })));
if (typeof provider.onDidErrorOccur === 'function') {
providerDisposables.add(provider.onDidErrorOccur(error => this._onError.fire(new Error(error))));
Expand Down Expand Up @@ -761,14 +761,18 @@ export class FileService extends Disposable implements IFileService {
}

private getExtUri(provider: IFileSystemProvider): { extUri: IExtUri, isPathCaseSensitive: boolean } {
const isPathCaseSensitive = !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
const isPathCaseSensitive = this.isPathCaseSensitive(provider);

return {
extUri: isPathCaseSensitive ? extUri : extUriIgnorePathCase,
isPathCaseSensitive
};
}

private isPathCaseSensitive(provider: IFileSystemProvider): boolean {
return !!(provider.capabilities & FileSystemProviderCapabilities.PathCaseSensitive);
}

async createFolder(resource: URI): Promise<IFileStatWithMetadata> {
const provider = this.throwIfFileSystemIsReadonly(await this.withProvider(resource), resource);

Expand Down
109 changes: 88 additions & 21 deletions src/vs/platform/files/common/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
import { Event } from 'vs/base/common/event';
import { startsWithIgnoreCase } from 'vs/base/common/strings';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IExtUri } from 'vs/base/common/resources';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from 'vs/base/common/buffer';
import { ReadableStreamEvents } from 'vs/base/common/stream';
import { CancellationToken } from 'vs/base/common/cancellation';
import { TernarySearchTree } from 'vs/base/common/map';

export const IFileService = createDecorator<IFileService>('fileService');

Expand Down Expand Up @@ -502,7 +502,44 @@ export interface IFileChange {

export class FileChangesEvent {

constructor(public readonly changes: readonly IFileChange[], private readonly extUri: IExtUri) { }
private readonly added: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private readonly updated: TernarySearchTree<URI, IFileChange> | undefined = undefined;
private readonly deleted: TernarySearchTree<URI, IFileChange> | undefined = undefined;

/**
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
readonly changes: readonly IFileChange[];

constructor(changes: readonly IFileChange[], private readonly ignorePathCasing: boolean) {
this.changes = changes;

for (const change of changes) {
switch (change.type) {
case FileChangeType.ADDED:
if (!this.added) {
this.added = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
}
this.added.set(change.resource, change);
break;
case FileChangeType.UPDATED:
if (!this.updated) {
this.updated = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
}
this.updated.set(change.resource, change);
break;
case FileChangeType.DELETED:
if (!this.deleted) {
this.deleted = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
}
this.deleted.set(change.resource, change);
break;
}
}
}

/**
* Returns true if this change event contains the provided file
Expand All @@ -517,22 +554,35 @@ export class FileChangesEvent {

const hasTypesFilter = types.length > 0;

return this.changes.some(change => {
if (hasTypesFilter && !types.includes(change.type)) {
return false;
// Added
if (!hasTypesFilter || types.includes(FileChangeType.ADDED)) {
if (this.added?.get(resource)) {
return true;
}
}

// For deleted also return true when deleted folder is parent of target path
if (change.type === FileChangeType.DELETED) {
return this.extUri.isEqualOrParent(resource, change.resource);
// Updated
if (!hasTypesFilter || types.includes(FileChangeType.UPDATED)) {
if (this.updated?.get(resource)) {
return true;
}
}

return this.extUri.isEqual(resource, change.resource);
});
// Deleted
if (!hasTypesFilter || types.includes(FileChangeType.DELETED)) {
if (this.deleted?.findSubstr(resource) /* deleted also considers parent folders */) {
return true;
}
}

return false;
}

/**
* Returns the changes that describe added files.
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
getAdded(): IFileChange[] {
return this.getOfType(FileChangeType.ADDED);
Expand All @@ -542,11 +592,14 @@ export class FileChangesEvent {
* Returns if this event contains added files.
*/
gotAdded(): boolean {
return this.hasType(FileChangeType.ADDED);
return !!this.added;
}

/**
* Returns the changes that describe deleted files.
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
getDeleted(): IFileChange[] {
return this.getOfType(FileChangeType.DELETED);
Expand All @@ -556,11 +609,14 @@ export class FileChangesEvent {
* Returns if this event contains deleted files.
*/
gotDeleted(): boolean {
return this.hasType(FileChangeType.DELETED);
return !!this.deleted;
}

/**
* Returns the changes that describe updated files.
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
getUpdated(): IFileChange[] {
return this.getOfType(FileChangeType.UPDATED);
Expand All @@ -570,19 +626,30 @@ export class FileChangesEvent {
* Returns if this event contains updated files.
*/
gotUpdated(): boolean {
return this.hasType(FileChangeType.UPDATED);
return !!this.updated;
}

private getOfType(type: FileChangeType): IFileChange[] {
return this.changes.filter(change => change.type === type);
}
const changes: IFileChange[] = [];

const eventsForType = type === FileChangeType.ADDED ? this.added : type === FileChangeType.UPDATED ? this.updated : this.deleted;
if (eventsForType) {
for (const [, change] of eventsForType) {
changes.push(change);
}
}

private hasType(type: FileChangeType): boolean {
return this.changes.some(change => change.type === type);
return changes;
}

/**
* @deprecated use the `contains()` method to efficiently find out if the event
* relates to a given resource. this method ensures:
* - that there is no expensive lookup needed by using a `TernarySearchTree`
* - correctly handles `FileChangeType.DELETED` events
*/
filter(filterFn: (change: IFileChange) => boolean): FileChangesEvent {
return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.extUri);
return new FileChangesEvent(this.changes.filter(change => filterFn(change)), this.ignorePathCasing);
}
}

Expand Down
104 changes: 78 additions & 26 deletions src/vs/platform/files/test/common/files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,41 +9,93 @@ import { isEqual, isEqualOrParent } from 'vs/base/common/extpath';
import { FileChangeType, FileChangesEvent, isParent } from 'vs/platform/files/common/files';
import { isLinux, isMacintosh, isWindows } from 'vs/base/common/platform';
import { toResource } from 'vs/base/test/common/utils';
import { extUri } from 'vs/base/common/resources';

suite('Files', () => {

test('FileChangesEvent', function () {
let changes = [
test('FileChangesEvent - basics', function () {
const changes = [
{ resource: toResource.call(this, '/foo/updated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/foo/otherupdated.txt'), type: FileChangeType.UPDATED },
{ resource: toResource.call(this, '/added.txt'), type: FileChangeType.ADDED },
{ resource: toResource.call(this, '/bar/deleted.txt'), type: FileChangeType.DELETED },
{ resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED }
{ resource: toResource.call(this, '/bar/folder'), type: FileChangeType.DELETED },
{ resource: toResource.call(this, '/BAR/FOLDER'), type: FileChangeType.DELETED }
];

let r1 = new FileChangesEvent(changes, extUri);

assert(!r1.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED));
assert(r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
assert(r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
assert(r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED));
assert(!r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED, FileChangeType.DELETED));
assert(!r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED));
assert(!r1.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));

assert(r1.contains(toResource.call(this, '/bar/folder'), FileChangeType.DELETED));
assert(r1.contains(toResource.call(this, '/bar/folder/somefile'), FileChangeType.DELETED));
assert(r1.contains(toResource.call(this, '/bar/folder/somefile/test.txt'), FileChangeType.DELETED));
assert(!r1.contains(toResource.call(this, '/bar/folder2/somefile'), FileChangeType.DELETED));

assert.strictEqual(5, r1.changes.length);
assert.strictEqual(1, r1.getAdded().length);
assert.strictEqual(true, r1.gotAdded());
assert.strictEqual(2, r1.getUpdated().length);
assert.strictEqual(true, r1.gotUpdated());
assert.strictEqual(2, r1.getDeleted().length);
assert.strictEqual(true, r1.gotDeleted());
for (const ignorePathCasing of [false, true]) {
const event = new FileChangesEvent(changes, ignorePathCasing);

assert(!event.contains(toResource.call(this, '/foo'), FileChangeType.UPDATED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED));
assert(event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.UPDATED, FileChangeType.ADDED, FileChangeType.DELETED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED, FileChangeType.DELETED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.ADDED));
assert(!event.contains(toResource.call(this, '/foo/updated.txt'), FileChangeType.DELETED));

assert(event.contains(toResource.call(this, '/bar/folder'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/BAR/FOLDER'), FileChangeType.DELETED));
if (ignorePathCasing) {
assert(event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
} else {
assert(!event.contains(toResource.call(this, '/BAR/folder'), FileChangeType.DELETED));
}
assert(event.contains(toResource.call(this, '/bar/folder/somefile'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/bar/folder/somefile/test.txt'), FileChangeType.DELETED));
assert(event.contains(toResource.call(this, '/BAR/FOLDER/somefile/test.txt'), FileChangeType.DELETED));
if (ignorePathCasing) {
assert(event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
} else {
assert(!event.contains(toResource.call(this, '/BAR/folder/somefile/test.txt'), FileChangeType.DELETED));
}
assert(!event.contains(toResource.call(this, '/bar/folder2/somefile'), FileChangeType.DELETED));

assert.strictEqual(6, event.changes.length);
assert.strictEqual(1, event.getAdded().length);
assert.strictEqual(true, event.gotAdded());
assert.strictEqual(2, event.getUpdated().length);
assert.strictEqual(true, event.gotUpdated());
assert.strictEqual(ignorePathCasing ? 2 : 3, event.getDeleted().length);
assert.strictEqual(true, event.gotDeleted());
}
});

test('FileChangesEvent - supports multiple changes on file tree', function () {
for (const type of [FileChangeType.ADDED, FileChangeType.UPDATED, FileChangeType.DELETED]) {
const changes = [
{ resource: toResource.call(this, '/foo/bar/updated.txt'), type },
{ resource: toResource.call(this, '/foo/bar/otherupdated.txt'), type },
{ resource: toResource.call(this, '/foo/bar'), type },
{ resource: toResource.call(this, '/foo'), type },
{ resource: toResource.call(this, '/bar'), type },
{ resource: toResource.call(this, '/bar/foo'), type },
{ resource: toResource.call(this, '/bar/foo/updated.txt'), type },
{ resource: toResource.call(this, '/bar/foo/otherupdated.txt'), type }
];

for (const ignorePathCasing of [false, true]) {
const event = new FileChangesEvent(changes, ignorePathCasing);

for (const change of changes) {
assert(event.contains(change.resource, type));
}

assert(!event.contains(toResource.call(this, '/some/foo/bar'), type));
assert(!event.contains(toResource.call(this, '/some/bar'), type));

switch (type) {
case FileChangeType.ADDED:
assert.strictEqual(8, event.getAdded().length);
break;
case FileChangeType.UPDATED:
assert.strictEqual(8, event.getUpdated().length);
break;
case FileChangeType.DELETED:
assert.strictEqual(8, event.getDeleted().length);
break;
}
}
}
});

function testIsEqual(testMethod: (pA: string, pB: string, ignoreCase: boolean) => boolean): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import { FileChangeType, FileChangesEvent } from 'vs/platform/files/common/files
import { URI as uri } from 'vs/base/common/uri';
import { IDiskFileChange, normalizeFileChanges, toFileChanges } from 'vs/platform/files/node/watcher/watcher';
import { Event, Emitter } from 'vs/base/common/event';
import { ExtUri } from 'vs/base/common/resources';

function toFileChangesEvent(changes: IDiskFileChange[]): FileChangesEvent {
return new FileChangesEvent(toFileChanges(changes), new ExtUri(() => !platform.isLinux));
return new FileChangesEvent(toFileChanges(changes), !platform.isLinux);
}

class TestFileWatcher {
Expand Down
23 changes: 14 additions & 9 deletions src/vs/workbench/contrib/debug/browser/debugService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { URI as uri } from 'vs/base/common/uri';
import { URI, URI as uri } from 'vs/base/common/uri';
import { distinct } from 'vs/base/common/arrays';
import * as errors from 'vs/base/common/errors';
import severity from 'vs/base/common/severity';
Expand Down Expand Up @@ -68,7 +68,7 @@ export class DebugService implements IDebugService {
private inDebugMode!: IContextKey<boolean>;
private debugUx!: IContextKey<string>;
private breakpointsExist!: IContextKey<boolean>;
private breakpointsToSendOnResourceSaved: Set<string>;
private breakpointsToSendOnResourceSaved: Set<URI>;
private initializing = false;
private previousState: State | undefined;
private sessionCancellationTokens = new Map<string, CancellationTokenSource>();
Expand Down Expand Up @@ -96,7 +96,7 @@ export class DebugService implements IDebugService {
) {
this.toDispose = [];

this.breakpointsToSendOnResourceSaved = new Set<string>();
this.breakpointsToSendOnResourceSaved = new Set<URI>();

this._onDidChangeState = new Emitter<State>();
this._onDidNewSession = new Emitter<IDebugSession>();
Expand Down Expand Up @@ -878,7 +878,7 @@ export class DebugService implements IDebugService {
this.model.updateBreakpoints(data);
this.debugStorage.storeBreakpoints(this.model);
if (sendOnResourceSaved) {
this.breakpointsToSendOnResourceSaved.add(uri.toString());
this.breakpointsToSendOnResourceSaved.add(uri);
} else {
await this.sendBreakpoints(uri);
this.debugStorage.storeBreakpoints(this.model);
Expand Down Expand Up @@ -983,12 +983,17 @@ export class DebugService implements IDebugService {
this.model.removeBreakpoints(toRemove);
}

fileChangesEvent.getUpdated().forEach(event => {

if (this.breakpointsToSendOnResourceSaved.delete(event.resource.toString())) {
this.sendBreakpoints(event.resource, true);
const toSend: URI[] = [];
for (const uri of this.breakpointsToSendOnResourceSaved) {
if (fileChangesEvent.contains(uri, FileChangeType.UPDATED)) {
toSend.push(uri);
}
});
}

for (const uri of toSend) {
this.breakpointsToSendOnResourceSaved.delete(uri);
this.sendBreakpoints(uri, true);
}
}
}

Expand Down
Loading

0 comments on commit bb3aaa7

Please sign in to comment.