From 85f166e05360f40ff9c97cbe5255375f0d428f39 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Fri, 25 Aug 2023 15:04:59 +0200 Subject: [PATCH] voice - use `LimitedQueue` to prevent parallel transcriptions (#191311) --- src/vs/base/common/async.ts | 116 +++++++++----- src/vs/base/test/common/async.test.ts | 150 +++++++++++------- .../sharedProcess/contrib/voiceTranscriber.ts | 25 ++- .../textfile/common/textFileEditorModel.ts | 36 ++--- .../common/storedFileWorkingCopy.ts | 36 ++--- 5 files changed, 215 insertions(+), 148 deletions(-) diff --git a/src/vs/base/common/async.ts b/src/vs/base/common/async.ts index 6b87b06b21a..152a6af7e4e 100644 --- a/src/vs/base/common/async.ts +++ b/src/vs/base/common/async.ts @@ -682,6 +682,31 @@ export class Queue extends Limiter { } } +/** + * Same as `Queue`, ensures that only 1 task is executed at the same time. The difference to `Queue` is that + * there is only 1 task about to be scheduled next. As such, calling `queue` while a task is executing will + * replace the currently queued task until it executes. + * + * As such, the returned promise may not be from the factory that is passed in but from the next factory that + * is running after having called `queue`. + */ +export class LimitedQueue { + + private readonly sequentializer = new TaskSequentializer(); + + private tasks = 0; + + queue(factory: ITask>): Promise { + if (!this.sequentializer.isRunning()) { + return this.sequentializer.run(this.tasks++, factory()); + } + + return this.sequentializer.queue(() => { + return this.sequentializer.run(this.tasks++, factory()); + }); + } +} + /** * A helper to organize queues per resource. The ResourceQueue makes sure to manage queues per resource * by disposing them once the queue is empty. @@ -1267,83 +1292,92 @@ export async function retry(task: ITask>, delay: number, retries: //#region Task Sequentializer -interface IPendingTask { +interface IRunningTask { readonly taskId: number; readonly cancel: () => void; readonly promise: Promise; } -interface INextTask { +interface IQueuedTask { readonly promise: Promise; readonly promiseResolve: () => void; readonly promiseReject: (error: Error) => void; - run: () => Promise; + run: ITask>; } -export interface ITaskSequentializerWithPendingTask { - readonly pending: Promise; +export interface ITaskSequentializerWithRunningTask { + readonly running: Promise; } -export interface ITaskSequentializerWithNextTask { - readonly next: INextTask; +export interface ITaskSequentializerWithQueuedTask { + readonly queued: IQueuedTask; } +/** + * @deprecated use `LimitedQueue` instead for an easier to use API + */ export class TaskSequentializer { - private _pending?: IPendingTask; - private _next?: INextTask; + private _running?: IRunningTask; + private _queued?: IQueuedTask; - hasPending(taskId?: number): this is ITaskSequentializerWithPendingTask { + isRunning(taskId?: number): this is ITaskSequentializerWithRunningTask { if (typeof taskId === 'number') { - return this._pending?.taskId === taskId; + return this._running?.taskId === taskId; } - return !!this._pending; + return !!this._running; } - get pending(): Promise | undefined { - return this._pending?.promise; + get running(): Promise | undefined { + return this._running?.promise; } - cancelPending(): void { - this._pending?.cancel(); + cancelRunning(): void { + this._running?.cancel(); } - setPending(taskId: number, promise: Promise, onCancel?: () => void,): Promise { - this._pending = { taskId, cancel: () => onCancel?.(), promise }; + run(taskId: number, promise: Promise, onCancel?: () => void,): Promise { + this._running = { taskId, cancel: () => onCancel?.(), promise }; - promise.then(() => this.donePending(taskId), () => this.donePending(taskId)); + promise.then(() => this.doneRunning(taskId), () => this.doneRunning(taskId)); return promise; } - private donePending(taskId: number): void { - if (this._pending && taskId === this._pending.taskId) { + private doneRunning(taskId: number): void { + if (this._running && taskId === this._running.taskId) { - // only set pending to done if the promise finished that is associated with that taskId - this._pending = undefined; + // only set running to done if the promise finished that is associated with that taskId + this._running = undefined; - // schedule the next task now that we are free if we have any - this.triggerNext(); + // schedule the queued task now that we are free if we have any + this.runQueued(); } } - private triggerNext(): void { - if (this._next) { - const next = this._next; - this._next = undefined; + private runQueued(): void { + if (this._queued) { + const queued = this._queued; + this._queued = undefined; - // Run next task and complete on the associated promise - next.run().then(next.promiseResolve, next.promiseReject); + // Run queued task and complete on the associated promise + queued.run().then(queued.promiseResolve, queued.promiseReject); } } - setNext(run: () => Promise): Promise { + /** + * Note: the promise to schedule as next run MUST itself call `run`. + * Otherwise, this sequentializer will report `false` for `isRunning` + * even when this task is running. Missing this detail means that + * suddenly multiple tasks will run in parallel. + */ + queue(run: ITask>): Promise { - // this is our first next task, so we create associated promise with it + // this is our first queued task, so we create associated promise with it // so that we can return a promise that completes when the task has // completed. - if (!this._next) { + if (!this._queued) { let promiseResolve: () => void; let promiseReject: (error: Error) => void; const promise = new Promise((resolve, reject) => { @@ -1351,7 +1385,7 @@ export class TaskSequentializer { promiseReject = reject; }); - this._next = { + this._queued = { run, promise, promiseResolve: promiseResolve!, @@ -1359,20 +1393,20 @@ export class TaskSequentializer { }; } - // we have a previous next task, just overwrite it + // we have a previous queued task, just overwrite it else { - this._next.run = run; + this._queued.run = run; } - return this._next.promise; + return this._queued.promise; } - hasNext(): this is ITaskSequentializerWithNextTask { - return !!this._next; + hasQueued(): this is ITaskSequentializerWithQueuedTask { + return !!this._queued; } async join(): Promise { - return this._next?.promise ?? this._pending?.promise; + return this._queued?.promise ?? this._running?.promise; } } diff --git a/src/vs/base/test/common/async.test.ts b/src/vs/base/test/common/async.test.ts index 144c119389a..467708aa2c2 100644 --- a/src/vs/base/test/common/async.test.ts +++ b/src/vs/base/test/common/async.test.ts @@ -664,119 +664,119 @@ suite('Async', () => { }); suite('TaskSequentializer', () => { - test('pending basics', async function () { + test('execution basics', async function () { const sequentializer = new async.TaskSequentializer(); - assert.ok(!sequentializer.hasPending()); - assert.ok(!sequentializer.hasNext()); - assert.ok(!sequentializer.hasPending(2323)); - assert.ok(!sequentializer.pending); + assert.ok(!sequentializer.isRunning()); + assert.ok(!sequentializer.hasQueued()); + assert.ok(!sequentializer.isRunning(2323)); + assert.ok(!sequentializer.running); // pending removes itself after done - await sequentializer.setPending(1, Promise.resolve()); - assert.ok(!sequentializer.hasPending()); - assert.ok(!sequentializer.hasPending(1)); - assert.ok(!sequentializer.pending); - assert.ok(!sequentializer.hasNext()); + await sequentializer.run(1, Promise.resolve()); + assert.ok(!sequentializer.isRunning()); + assert.ok(!sequentializer.isRunning(1)); + assert.ok(!sequentializer.running); + assert.ok(!sequentializer.hasQueued()); // 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); + sequentializer.run(2, async.timeout(1)); + assert.ok(sequentializer.isRunning()); + assert.ok(sequentializer.isRunning(2)); + assert.ok(!sequentializer.hasQueued()); + assert.strictEqual(sequentializer.isRunning(1), false); + assert.ok(sequentializer.running); await async.timeout(2); - assert.strictEqual(sequentializer.hasPending(), false); - assert.strictEqual(sequentializer.hasPending(2), false); - assert.ok(!sequentializer.pending); + assert.strictEqual(sequentializer.isRunning(), false); + assert.strictEqual(sequentializer.isRunning(2), false); + assert.ok(!sequentializer.running); }); - test('pending and next (finishes instantly)', async function () { + test('executing and queued (finishes instantly)', async function () { const sequentializer = new async.TaskSequentializer(); let pendingDone = false; - sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + sequentializer.run(1, async.timeout(1).then(() => { pendingDone = true; return; })); - // next finishes instantly - let nextDone = false; - const res = sequentializer.setNext(() => Promise.resolve(null).then(() => { nextDone = true; return; })); + // queued finishes instantly + let queuedDone = false; + const res = sequentializer.queue(() => Promise.resolve(null).then(() => { queuedDone = true; return; })); - assert.ok(sequentializer.hasNext()); + assert.ok(sequentializer.hasQueued()); await res; assert.ok(pendingDone); - assert.ok(nextDone); - assert.ok(!sequentializer.hasNext()); + assert.ok(queuedDone); + assert.ok(!sequentializer.hasQueued()); }); - test('pending and next (finishes after timeout)', async function () { + test('executing and queued (finishes after timeout)', async function () { const sequentializer = new async.TaskSequentializer(); let pendingDone = false; - sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + sequentializer.run(1, async.timeout(1).then(() => { pendingDone = true; return; })); - // next finishes after async.timeout - let nextDone = false; - const res = sequentializer.setNext(() => async.timeout(1).then(() => { nextDone = true; return; })); + // queued finishes after async.timeout + let queuedDone = false; + const res = sequentializer.queue(() => async.timeout(1).then(() => { queuedDone = true; return; })); await res; assert.ok(pendingDone); - assert.ok(nextDone); - assert.ok(!sequentializer.hasNext()); + assert.ok(queuedDone); + assert.ok(!sequentializer.hasQueued()); }); - test('join (without next or pending)', async function () { + test('join (without executing or queued)', async function () { const sequentializer = new async.TaskSequentializer(); await sequentializer.join(); - assert.ok(!sequentializer.hasNext()); + assert.ok(!sequentializer.hasQueued()); }); - test('join (without next)', async function () { + test('join (without queued)', async function () { const sequentializer = new async.TaskSequentializer(); let pendingDone = false; - sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + sequentializer.run(1, async.timeout(1).then(() => { pendingDone = true; return; })); await sequentializer.join(); assert.ok(pendingDone); - assert.ok(!sequentializer.hasPending()); + assert.ok(!sequentializer.isRunning()); }); - test('join (with next and pending)', async function () { + test('join (with executing and queued)', async function () { const sequentializer = new async.TaskSequentializer(); let pendingDone = false; - sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + sequentializer.run(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; })); + // queued finishes after async.timeout + let queuedDone = false; + sequentializer.queue(() => async.timeout(1).then(() => { queuedDone = true; return; })); await sequentializer.join(); assert.ok(pendingDone); - assert.ok(nextDone); - assert.ok(!sequentializer.hasPending()); - assert.ok(!sequentializer.hasNext()); + assert.ok(queuedDone); + assert.ok(!sequentializer.isRunning()); + assert.ok(!sequentializer.hasQueued()); }); - test('pending and multiple next (last one wins)', async function () { + test('executing and multiple queued (last one wins)', async function () { const sequentializer = new async.TaskSequentializer(); let pendingDone = false; - sequentializer.setPending(1, async.timeout(1).then(() => { pendingDone = true; return; })); + sequentializer.run(1, async.timeout(1).then(() => { pendingDone = true; return; })); - // next finishes after async.timeout + // queued finishes after async.timeout let firstDone = false; - const firstRes = sequentializer.setNext(() => async.timeout(2).then(() => { firstDone = true; return; })); + const firstRes = sequentializer.queue(() => async.timeout(2).then(() => { firstDone = true; return; })); let secondDone = false; - const secondRes = sequentializer.setNext(() => async.timeout(3).then(() => { secondDone = true; return; })); + const secondRes = sequentializer.queue(() => async.timeout(3).then(() => { secondDone = true; return; })); let thirdDone = false; - const thirdRes = sequentializer.setNext(() => async.timeout(4).then(() => { thirdDone = true; return; })); + const thirdRes = sequentializer.queue(() => async.timeout(4).then(() => { thirdDone = true; return; })); await Promise.all([firstRes, secondRes, thirdRes]); assert.ok(pendingDone); @@ -785,12 +785,12 @@ suite('Async', () => { assert.ok(thirdDone); }); - test('cancel pending', async function () { + test('cancel executing', async function () { const sequentializer = new async.TaskSequentializer(); let pendingCancelled = false; - sequentializer.setPending(1, async.timeout(1), () => pendingCancelled = true); - sequentializer.cancelPending(); + sequentializer.run(1, async.timeout(1), () => pendingCancelled = true); + sequentializer.cancelRunning(); assert.ok(pendingCancelled); }); @@ -1281,4 +1281,42 @@ suite('Async', () => { assert.strictEqual(worked, false); }); }); + + suite('LimitedQueue', () => { + + test('basics (with long running task)', async () => { + const limitedQueue = new async.LimitedQueue(); + + let counter = 0; + const promises = []; + for (let i = 0; i < 5; i++) { + promises.push(limitedQueue.queue(async () => { + counter = i; + await async.timeout(1); + })); + } + + await Promise.all(promises); + + // only the last task executed + assert.strictEqual(counter, 4); + }); + + test('basics (with sync running task)', async () => { + const limitedQueue = new async.LimitedQueue(); + + let counter = 0; + const promises = []; + for (let i = 0; i < 5; i++) { + promises.push(limitedQueue.queue(async () => { + counter = i; + })); + } + + await Promise.all(promises); + + // only the last task executed + assert.strictEqual(counter, 4); + }); + }); }); diff --git a/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts b/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts index 210570ef952..11299d0848e 100644 --- a/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts +++ b/src/vs/code/node/sharedProcess/contrib/voiceTranscriber.ts @@ -9,7 +9,7 @@ import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; import { IVoiceRecognitionService } from 'vs/platform/voiceRecognition/node/voiceRecognitionService'; import { ILogService } from 'vs/platform/log/common/log'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { TaskSequentializer } from 'vs/base/common/async'; +import { LimitedQueue } from 'vs/base/common/async'; export class VoiceTranscriptionManager extends Disposable { @@ -34,9 +34,7 @@ class VoiceTranscriber extends Disposable { private static MAX_DATA_LENGTH = 30 /* seconds */ * 16000 /* sampling rate */ * 16 /* bith depth */ * 1 /* channels */ / 8; - private readonly transcriptionSequentializer = new TaskSequentializer(); - - private requests = 0; + private readonly transcriptionQueue = new LimitedQueue(); private data: Float32Array | undefined = undefined; @@ -67,7 +65,6 @@ class VoiceTranscriber extends Disposable { this.logService.info(`[voice] transcriber: closed connection`); cts.dispose(true); - this.transcriptionSequentializer.cancelPending(); }); } @@ -91,23 +88,21 @@ class VoiceTranscriber extends Disposable { } this.data = dataCandidate; - const data = this.data.slice(0); - this.requests++; - - if (!this.transcriptionSequentializer.hasPending()) { - this.transcriptionSequentializer.setPending(this.requests, this.transcribe(data, cancellation)); - } else { - this.transcriptionSequentializer.setNext(() => this.transcribe(data, cancellation)); - } + this.transcriptionQueue.queue(() => this.transcribe(cancellation)); } - private async transcribe(channelData: Float32Array, cancellation: CancellationToken): Promise { + private async transcribe(cancellation: CancellationToken): Promise { if (cancellation.isCancellationRequested) { return; } - const result = await this.voiceRecognitionService.transcribe(channelData, cancellation); + const data = this.data?.slice(0); + if (!data) { + return; + } + + const result = await this.voiceRecognitionService.transcribe(data, cancellation); if (cancellation.isCancellationRequested) { return; diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index 51af0329394..a60dc9a4560 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -290,7 +290,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Unless there are explicit contents provided, it is important that we do not // resolve a model that is dirty or is in the process of saving to prevent data // loss. - if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { + if (!options?.contents && (this.dirty || this.saveSequentializer.isRunning())) { this.trace('resolve() - exit - without resolving because model is dirty or being saved'); return; @@ -767,15 +767,15 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil return; } - // Lookup any running pending save for this versionId and return it if found + // Lookup any running save for this versionId and return it if found // // Scenario: user invoked the save action multiple times quickly for the same contents // while the save was not yet finished to disk // - if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + if (this.saveSequentializer.isRunning(versionId)) { + this.trace(`doSave(${versionId}) - exit - found a running save for versionId ${versionId}`); - return this.saveSequentializer.pending; + return this.saveSequentializer.running; } // Return early if not dirty (unless forced) @@ -795,18 +795,18 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Scenario B: save is very slow (e.g. network share) and the user manages to change the buffer and trigger another save // while the first save has not returned yet. // - if (this.saveSequentializer.hasPending()) { + if (this.saveSequentializer.isRunning()) { this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to - // cancel the pending operation so that ours can run - // before the pending one finishes. - // Currently this will try to cancel pending save - // participants but never a pending save. - this.saveSequentializer.cancelPending(); + // cancel the running operation so that ours can run + // before the running one finishes. + // Currently this will try to cancel running save + // participants but never a running save. + this.saveSequentializer.cancelRunning(); - // Register this as the next upcoming save and return - return this.saveSequentializer.setNext(() => this.doSave(options)); + // Queue this as the upcoming save and return + return this.saveSequentializer.queue(() => this.doSave(options)); } // Push all edit operations to the undo stack so that the user has a chance to @@ -817,7 +817,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil const saveCancellation = new CancellationTokenSource(); - return this.saveSequentializer.setPending(versionId, (async () => { + return this.saveSequentializer.run(versionId, (async () => { // A save participant can still change the model now and since we are so close to saving // we do not want to trigger another auto save or similar, so we block this @@ -894,13 +894,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil // Clear error flag since we are trying to save again this.inErrorMode = false; - // Save to Disk. We mark the save operation as currently pending with + // Save to Disk. We mark the save operation as currently running with // the latest versionId because it might have changed from a save // participant triggering this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedTextFileEditorModel = this; - return this.saveSequentializer.setPending(versionId, (async () => { + return this.saveSequentializer.run(versionId, (async () => { try { const stat = await this.textFileService.write(lastResolvedFileStat.resource, resolvedTextFileEditorModel.createSnapshot(), { mtime: lastResolvedFileStat.mtime, @@ -1013,14 +1013,14 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil case TextFileEditorModelState.ORPHAN: return this.inOrphanMode; case TextFileEditorModelState.PENDING_SAVE: - return this.saveSequentializer.hasPending(); + return this.saveSequentializer.isRunning(); case TextFileEditorModelState.SAVED: return !this.dirty; } } async joinState(state: TextFileEditorModelState.PENDING_SAVE): Promise { - return this.saveSequentializer.pending; + return this.saveSequentializer.running; } override getLanguageId(this: IResolvedTextFileEditorModel): string; diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index d3fff96bb86..f96b3de0015 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -433,7 +433,7 @@ export class StoredFileWorkingCopy extend // Unless there are explicit contents provided, it is important that we do not // resolve a working copy that is dirty or is in the process of saving to prevent // data loss. - if (!options?.contents && (this.dirty || this.saveSequentializer.hasPending())) { + if (!options?.contents && (this.dirty || this.saveSequentializer.isRunning())) { this.trace('resolve() - exit - without resolving because file working copy is dirty or being saved'); return; @@ -851,15 +851,15 @@ export class StoredFileWorkingCopy extend return; } - // Lookup any running pending save for this versionId and return it if found + // Lookup any running save for this versionId and return it if found // // Scenario: user invoked the save action multiple times quickly for the same contents // while the save was not yet finished to disk // - if (this.saveSequentializer.hasPending(versionId)) { - this.trace(`doSave(${versionId}) - exit - found a pending save for versionId ${versionId}`); + if (this.saveSequentializer.isRunning(versionId)) { + this.trace(`doSave(${versionId}) - exit - found a running save for versionId ${versionId}`); - return this.saveSequentializer.pending; + return this.saveSequentializer.running; } // Return early if not dirty (unless forced) @@ -879,20 +879,20 @@ export class StoredFileWorkingCopy extend // Scenario B: save is very slow (e.g. network share) and the user manages to change the working copy and trigger another save // while the first save has not returned yet. // - if (this.saveSequentializer.hasPending()) { + if (this.saveSequentializer.isRunning()) { this.trace(`doSave(${versionId}) - exit - because busy saving`); // Indicate to the save sequentializer that we want to - // cancel the pending operation so that ours can run - // before the pending one finishes. - // Currently this will try to cancel pending save - // participants and pending snapshots from the + // cancel the running operation so that ours can run + // before the running one finishes. + // Currently this will try to cancel running save + // participants and running snapshots from the // save operation, but not the actual save which does // not support cancellation yet. - this.saveSequentializer.cancelPending(); + this.saveSequentializer.cancelRunning(); - // Register this as the next upcoming save and return - return this.saveSequentializer.setNext(() => this.doSave(options)); + // Queue this as the upcoming save and return + return this.saveSequentializer.queue(() => this.doSave(options)); } // Push all edit operations to the undo stack so that the user has a chance to @@ -903,7 +903,7 @@ export class StoredFileWorkingCopy extend const saveCancellation = new CancellationTokenSource(); - return this.saveSequentializer.setPending(versionId, (async () => { + return this.saveSequentializer.run(versionId, (async () => { // A save participant can still change the working copy now // and since we are so close to saving we do not want to trigger @@ -975,13 +975,13 @@ export class StoredFileWorkingCopy extend // Clear error flag since we are trying to save again this.inErrorMode = false; - // Save to Disk. We mark the save operation as currently pending with + // Save to Disk. We mark the save operation as currently running with // the latest versionId because it might have changed from a save // participant triggering this.trace(`doSave(${versionId}) - before write()`); const lastResolvedFileStat = assertIsDefined(this.lastResolvedFileStat); const resolvedFileWorkingCopy = this; - return this.saveSequentializer.setPending(versionId, (async () => { + return this.saveSequentializer.run(versionId, (async () => { try { const writeFileOptions: IWriteFileOptions = { mtime: lastResolvedFileStat.mtime, @@ -1256,14 +1256,14 @@ export class StoredFileWorkingCopy extend case StoredFileWorkingCopyState.ORPHAN: return this.isOrphaned(); case StoredFileWorkingCopyState.PENDING_SAVE: - return this.saveSequentializer.hasPending(); + return this.saveSequentializer.isRunning(); case StoredFileWorkingCopyState.SAVED: return !this.dirty; } } async joinState(state: StoredFileWorkingCopyState.PENDING_SAVE): Promise { - return this.saveSequentializer.pending; + return this.saveSequentializer.running; } //#endregion