Skip to content

Commit 94d4cf6

Browse files
authored
Fix run all cells to wait for the previous cell (#10219)
* Fix run all cells to wait for the previous cell * Add support for canceling in the middle and modify the run all test to verify * Fix finishedPos on cancel. Allow more than one execute chain to be in progress * Add trace for execution * Review feedback * Sonar code smells * Fix functional tests - storage was messing up next test
1 parent d66b6da commit 94d4cf6

File tree

9 files changed

+178
-90
lines changed

9 files changed

+178
-90
lines changed

news/2 Fixes/10016.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix run all cells to force each cell to finish before running the next one.

src/client/datascience/interactive-common/interactiveBase.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ import {
5555
IGotoCode,
5656
IInteractiveWindowMapping,
5757
InteractiveWindowMessages,
58-
IReExecuteCell,
58+
IReExecuteCells,
5959
IRemoteAddCode,
6060
IRemoteReexecuteCode,
6161
IShowDataViewer,
@@ -230,8 +230,8 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
230230
this.handleMessage(message, payload, this.submitNewCell);
231231
break;
232232

233-
case InteractiveWindowMessages.ReExecuteCell:
234-
this.handleMessage(message, payload, this.reexecuteCell);
233+
case InteractiveWindowMessages.ReExecuteCells:
234+
this.handleMessage(message, payload, this.reexecuteCells);
235235
break;
236236

237237
case InteractiveWindowMessages.DeleteAllCells:
@@ -462,8 +462,8 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
462462
// Submits a new cell to the window
463463
protected abstract submitNewCell(info: ISubmitNewCell): void;
464464

465-
// Re-executes a cell already in the window
466-
protected reexecuteCell(_info: IReExecuteCell): void {
465+
// Re-executes cells already in the window
466+
protected reexecuteCells(_info: IReExecuteCells): void {
467467
// Default is not to do anything. This only works in the native editor
468468
}
469469

@@ -499,7 +499,8 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
499499
line: number,
500500
id?: string,
501501
data?: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell,
502-
debug?: boolean
502+
debug?: boolean,
503+
cancelToken?: CancellationToken
503504
): Promise<boolean> {
504505
traceInfo(`Submitting code for ${this.id}`);
505506
const stopWatch =
@@ -567,7 +568,7 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
567568
file,
568569
line,
569570
uuid(),
570-
undefined,
571+
cancelToken,
571572
true
572573
);
573574
}
@@ -1437,7 +1438,7 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
14371438
this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse, s).ignoreErrors();
14381439
})
14391440
.catch(_e => {
1440-
this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse, undefined).ignoreErrors();
1441+
this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse).ignoreErrors();
14411442
});
14421443
}
14431444

@@ -1458,13 +1459,13 @@ export abstract class InteractiveBase extends WebViewHost<IInteractiveWindowMapp
14581459
this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, contents).ignoreErrors();
14591460
} else {
14601461
traceWarning('Onigasm file not found. Colorization will not be available.');
1461-
this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, undefined).ignoreErrors();
1462+
this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse).ignoreErrors();
14621463
}
14631464
}
14641465
} else {
14651466
// This happens during testing. Onigasm not needed as we're not testing colorization.
14661467
traceWarning('File system not found. Colorization will not be available.');
1467-
this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, undefined).ignoreErrors();
1468+
this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse).ignoreErrors();
14681469
}
14691470
}
14701471

src/client/datascience/interactive-common/interactiveWindowTypes.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export enum InteractiveWindowMessages {
7676
LoadAllCells = 'load_all_cells',
7777
LoadAllCellsComplete = 'load_all_cells_complete',
7878
ScrollToCell = 'scroll_to_cell',
79-
ReExecuteCell = 'reexecute_cell',
79+
ReExecuteCells = 'reexecute_cells',
8080
NotebookIdentity = 'identity',
8181
NotebookDirty = 'dirty',
8282
NotebookClean = 'clean',
@@ -177,9 +177,8 @@ export interface ISubmitNewCell {
177177
id: string;
178178
}
179179

180-
export interface IReExecuteCell {
181-
newCode: string;
182-
cell: ICell;
180+
export interface IReExecuteCells {
181+
entries: { cell: ICell; code: string }[];
183182
}
184183

185184
export interface IProvideCompletionItemsRequest {
@@ -372,7 +371,7 @@ export class IInteractiveWindowMapping {
372371
public [InteractiveWindowMessages.LoadAllCells]: ILoadAllCells;
373372
public [InteractiveWindowMessages.LoadAllCellsComplete]: ILoadAllCells;
374373
public [InteractiveWindowMessages.ScrollToCell]: IScrollToCell;
375-
public [InteractiveWindowMessages.ReExecuteCell]: IReExecuteCell;
374+
public [InteractiveWindowMessages.ReExecuteCells]: IReExecuteCells;
376375
public [InteractiveWindowMessages.NotebookIdentity]: INotebookIdentity;
377376
public [InteractiveWindowMessages.NotebookDirty]: never | undefined;
378377
public [InteractiveWindowMessages.NotebookClean]: never | undefined;

src/client/datascience/interactive-ipynb/nativeEditor.ts

Lines changed: 119 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as detectIndent from 'detect-indent';
88
import { inject, injectable, multiInject, named } from 'inversify';
99
import * as path from 'path';
1010
import * as uuid from 'uuid/v4';
11-
import { Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode';
11+
import { CancellationToken, CancellationTokenSource, Event, EventEmitter, Memento, Uri, ViewColumn } from 'vscode';
1212

1313
import { concatMultilineStringInput, splitMultilineString } from '../../../datascience-ui/common';
1414
import { createCodeCell, createErrorOutput } from '../../../datascience-ui/common/cellFactory';
@@ -21,10 +21,11 @@ import {
2121
IWorkspaceService
2222
} from '../../common/application/types';
2323
import { ContextKey } from '../../common/contextKey';
24-
import { traceError } from '../../common/logger';
24+
import { traceError, traceInfo } from '../../common/logger';
2525
import { IFileSystem, TemporaryFile } from '../../common/platform/types';
2626
import {
2727
GLOBAL_MEMENTO,
28+
IAsyncDisposableRegistry,
2829
IConfigurationService,
2930
ICryptoUtils,
3031
IDisposableRegistry,
@@ -55,7 +56,7 @@ import {
5556
IInsertCell,
5657
INativeCommand,
5758
InteractiveWindowMessages,
58-
IReExecuteCell,
59+
IReExecuteCells,
5960
IRemoveCell,
6061
ISaveAll,
6162
ISubmitNewCell,
@@ -166,6 +167,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
166167
private indentAmount: string = ' ';
167168
private notebookJson: Partial<nbformat.INotebookContent> = {};
168169
private debouncedWriteToStorage = debounce(this.writeToStorage.bind(this), 250);
170+
private executeCancelTokens = new Set<CancellationTokenSource>();
171+
private _disposed = false;
169172

170173
constructor(
171174
@multiInject(IInteractiveWindowListener) listeners: IInteractiveWindowListener[],
@@ -195,7 +198,8 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
195198
@inject(ICryptoUtils) private crypto: ICryptoUtils,
196199
@inject(IExtensionContext) private context: IExtensionContext,
197200
@inject(ProgressReporter) progressReporter: ProgressReporter,
198-
@inject(IExperimentsManager) experimentsManager: IExperimentsManager
201+
@inject(IExperimentsManager) experimentsManager: IExperimentsManager,
202+
@inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry
199203
) {
200204
super(
201205
progressReporter,
@@ -231,8 +235,10 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
231235
ViewColumn.Active,
232236
experimentsManager
233237
);
238+
asyncRegistry.push(this);
234239
}
235240
public dispose(): Promise<void> {
241+
this._disposed = true;
236242
super.dispose();
237243
return this.close();
238244
}
@@ -279,7 +285,7 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
279285
public onMessage(message: string, payload: any) {
280286
super.onMessage(message, payload);
281287
switch (message) {
282-
case InteractiveWindowMessages.ReExecuteCell:
288+
case InteractiveWindowMessages.ReExecuteCells:
283289
this.executedEvent.fire(this);
284290
break;
285291

@@ -324,6 +330,14 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
324330
this.handleMessage(message, payload, this.clearAllOutputs);
325331
break;
326332

333+
case InteractiveWindowMessages.RestartKernel:
334+
this.interruptExecution();
335+
break;
336+
337+
case InteractiveWindowMessages.Interrupt:
338+
this.interruptExecution();
339+
break;
340+
327341
default:
328342
break;
329343
}
@@ -403,11 +417,12 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
403417
line: number,
404418
id?: string,
405419
data?: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell,
406-
debug?: boolean
420+
debug?: boolean,
421+
cancelToken?: CancellationToken
407422
): Promise<boolean> {
408423
const stopWatch = new StopWatch();
409424
const submitCodePromise = super
410-
.submitCode(code, file, line, id, data, debug)
425+
.submitCode(code, file, line, id, data, debug, cancelToken)
411426
.finally(() => this.sendPerceivedCellExecute(stopWatch));
412427
// When code is executed, update the version number in the metadata.
413428
return submitCodePromise.then(value => {
@@ -452,45 +467,35 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
452467

453468
@captureTelemetry(Telemetry.ExecuteNativeCell, undefined, true)
454469
// tslint:disable-next-line:no-any
455-
protected async reexecuteCell(info: IReExecuteCell): Promise<void> {
470+
protected async reexecuteCells(info: IReExecuteCells): Promise<void> {
471+
const tokenSource = new CancellationTokenSource();
472+
this.executeCancelTokens.add(tokenSource);
473+
let finishedPos = info && info.entries ? info.entries.length : -1;
456474
try {
457-
// If there's any payload, it has the code and the id
458-
if (info && info.newCode && info.cell.id && info.cell.data.cell_type !== 'messages') {
459-
// Clear the result if we've run before
460-
await this.clearResult(info.cell.id);
461-
462-
// Send to ourselves.
463-
await this.submitCode(info.newCode, Identifiers.EmptyFileName, 0, info.cell.id, info.cell.data);
464-
465-
// Activate the other side, and send as if came from a file
466-
await this.ipynbProvider.show(this.file);
467-
this.shareMessage(InteractiveWindowMessages.RemoteReexecuteCode, {
468-
code: info.newCode,
469-
file: Identifiers.EmptyFileName,
470-
line: 0,
471-
id: info.cell.id,
472-
originator: this.id,
473-
debug: false
474-
});
475+
if (info && info.entries) {
476+
for (let i = 0; i < info.entries.length && !tokenSource.token.isCancellationRequested; i += 1) {
477+
await this.reexecuteCell(info.entries[i], tokenSource.token);
478+
if (!tokenSource.token.isCancellationRequested) {
479+
finishedPos = i;
480+
}
481+
}
475482
}
476483
} catch (exc) {
477-
// Make this error our cell output
478-
this.sendCellsToWebView([
479-
{
480-
// tslint:disable-next-line: no-any
481-
data: { ...info.cell.data, outputs: [createErrorOutput(exc)] } as any, // nyc compiler issue
482-
id: info.cell.id,
483-
file: Identifiers.EmptyFileName,
484-
line: 0,
485-
state: CellState.error
486-
}
487-
]);
488-
489484
// Tell the other side we restarted the kernel. This will stop all executions
490485
this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors();
491486

492487
// Handle an error
493488
await this.errorHandler.handleError(exc);
489+
} finally {
490+
this.executeCancelTokens.delete(tokenSource);
491+
492+
// Make sure everything is marked as finished or error after the final finished
493+
// position
494+
if (info && info.entries) {
495+
for (let i = finishedPos + 1; i < info.entries.length; i += 1) {
496+
this.finishCell(info.entries[i]);
497+
}
498+
}
494499
}
495500
}
496501

@@ -563,6 +568,75 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
563568
// Actually don't close, just let the error bubble out
564569
}
565570

571+
private interruptExecution() {
572+
this.executeCancelTokens.forEach(t => t.cancel());
573+
}
574+
575+
private finishCell(entry: { cell: ICell; code: string }) {
576+
this.sendCellsToWebView([
577+
{
578+
...entry.cell,
579+
// tslint:disable-next-line: no-any
580+
data: { ...entry.cell.data, source: entry.code } as any, // nyc compiler issue
581+
state: CellState.finished
582+
}
583+
]);
584+
}
585+
586+
private async reexecuteCell(entry: { cell: ICell; code: string }, cancelToken: CancellationToken): Promise<void> {
587+
try {
588+
// If there's any payload, it has the code and the id
589+
if (entry.code && entry.cell.id && entry.cell.data.cell_type !== 'messages') {
590+
traceInfo(`Executing cell ${entry.cell.id}`);
591+
592+
// Clear the result if we've run before
593+
await this.clearResult(entry.cell.id);
594+
595+
// Send to ourselves.
596+
await this.submitCode(
597+
entry.code,
598+
Identifiers.EmptyFileName,
599+
0,
600+
entry.cell.id,
601+
entry.cell.data,
602+
false,
603+
cancelToken
604+
);
605+
606+
if (!cancelToken.isCancellationRequested) {
607+
// Activate the other side, and send as if came from a file
608+
await this.ipynbProvider.show(this.file);
609+
this.shareMessage(InteractiveWindowMessages.RemoteReexecuteCode, {
610+
code: entry.code,
611+
file: Identifiers.EmptyFileName,
612+
line: 0,
613+
id: entry.cell.id,
614+
originator: this.id,
615+
debug: false
616+
});
617+
}
618+
}
619+
} catch (exc) {
620+
// Make this error our cell output
621+
this.sendCellsToWebView([
622+
{
623+
// tslint:disable-next-line: no-any
624+
data: { ...entry.cell.data, outputs: [createErrorOutput(exc)] } as any, // nyc compiler issue
625+
id: entry.cell.id,
626+
file: Identifiers.EmptyFileName,
627+
line: 0,
628+
state: CellState.error
629+
}
630+
]);
631+
632+
throw exc;
633+
} finally {
634+
if (entry && entry.cell.id) {
635+
traceInfo(`Finished executing cell ${entry.cell.id}`);
636+
}
637+
}
638+
}
639+
566640
private sendPerceivedCellExecute(runningStopWatch?: StopWatch) {
567641
if (runningStopWatch) {
568642
const props = { notebook: true };
@@ -882,11 +956,13 @@ export class NativeEditor extends InteractiveBase implements INotebookEditor {
882956

883957
private async writeToStorage(filePath: string, contents?: string): Promise<void> {
884958
try {
885-
if (contents) {
886-
await this.fileSystem.createDirectory(path.dirname(filePath));
887-
return this.fileSystem.writeFile(filePath, contents);
888-
} else {
889-
return this.fileSystem.deleteFile(filePath);
959+
if (!this._disposed) {
960+
if (contents) {
961+
await this.fileSystem.createDirectory(path.dirname(filePath));
962+
return this.fileSystem.writeFile(filePath, contents);
963+
} else {
964+
return this.fileSystem.deleteFile(filePath);
965+
}
890966
}
891967
} catch (exc) {
892968
traceError(`Error writing storage for ${filePath}: `, exc);

src/datascience-ui/interactive-common/redux/postOffice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export enum IncomingMessageActions {
7878
LOADALLCELLS = 'action.load_all_cells',
7979
LOADALLCELLSCOMPLETE = 'action.load_all_cells_complete',
8080
SCROLLTOCELL = 'action.scroll_to_cell',
81-
REEXECUTECELL = 'action.reexecute_cell',
81+
REEXECUTECELLS = 'action.reexecute_cells',
8282
NOTEBOOKIDENTITY = 'action.identity',
8383
NOTEBOOKDIRTY = 'action.dirty',
8484
NOTEBOOKCLEAN = 'action.clean',

0 commit comments

Comments
 (0)