|
| 1 | +// Copyright (c) Microsoft Corporation. All rights reserved. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +'use strict'; |
| 5 | + |
| 6 | +import { CancellationToken, NotebookCell, NotebookCellRunState } from 'vscode'; |
| 7 | +import { createDeferred } from '../../../common/utils/async'; |
| 8 | +import { StopWatch } from '../../../common/utils/stopWatch'; |
| 9 | +import { sendTelemetryEvent } from '../../../telemetry'; |
| 10 | +import { Telemetry } from '../../constants'; |
| 11 | +import { updateCellWithErrorStatus } from '../../notebook/helpers/executionHelpers'; |
| 12 | +import { |
| 13 | + clearCellForExecution, |
| 14 | + getCellStatusMessageBasedOnFirstCellErrorOutput, |
| 15 | + updateCellExecutionTimes |
| 16 | +} from '../../notebook/helpers/helpers'; |
| 17 | +import { MultiCancellationTokenSource } from '../../notebook/helpers/multiCancellationToken'; |
| 18 | +import { NotebookEditor } from '../../notebook/notebookEditor'; |
| 19 | +import { INotebookContentProvider } from '../../notebook/types'; |
| 20 | +import { IDataScienceErrorHandler, INotebookEditorProvider } from '../../types'; |
| 21 | +// tslint:disable-next-line: no-var-requires no-require-imports |
| 22 | +const vscodeNotebookEnums = require('vscode') as typeof import('vscode-proposed'); |
| 23 | + |
| 24 | +export class CellExecutionFactory { |
| 25 | + constructor( |
| 26 | + private readonly contentProvider: INotebookContentProvider, |
| 27 | + private readonly errorHandler: IDataScienceErrorHandler, |
| 28 | + private readonly editorProvider: INotebookEditorProvider |
| 29 | + ) {} |
| 30 | + |
| 31 | + public create(cell: NotebookCell) { |
| 32 | + // tslint:disable-next-line: no-use-before-declare |
| 33 | + return CellExecution.fromCell(cell, this.contentProvider, this.errorHandler, this.editorProvider); |
| 34 | + } |
| 35 | +} |
| 36 | +/** |
| 37 | + * Responsible for execution of an individual cell and manages the state of the cell as it progresses through the execution phases. |
| 38 | + * Execution phases include - enqueue for execution (done in ctor), start execution, completed execution with/without errors, cancel execution or dequeue. |
| 39 | + */ |
| 40 | +export class CellExecution { |
| 41 | + public get result(): Promise<NotebookCellRunState | undefined> { |
| 42 | + return this._result.promise; |
| 43 | + } |
| 44 | + |
| 45 | + public get token(): CancellationToken { |
| 46 | + return this.source.token; |
| 47 | + } |
| 48 | + |
| 49 | + public get completed() { |
| 50 | + return this._completed; |
| 51 | + } |
| 52 | + |
| 53 | + private static sentExecuteCellTelemetry?: boolean; |
| 54 | + |
| 55 | + private readonly oldCellRunState?: NotebookCellRunState; |
| 56 | + |
| 57 | + private stopWatch = new StopWatch(); |
| 58 | + |
| 59 | + private readonly source = new MultiCancellationTokenSource(); |
| 60 | + |
| 61 | + private readonly _result = createDeferred<NotebookCellRunState | undefined>(); |
| 62 | + |
| 63 | + private started?: boolean; |
| 64 | + |
| 65 | + private _completed?: boolean; |
| 66 | + |
| 67 | + private constructor( |
| 68 | + public readonly cell: NotebookCell, |
| 69 | + private readonly contentProvider: INotebookContentProvider, |
| 70 | + private readonly errorHandler: IDataScienceErrorHandler, |
| 71 | + private readonly editorProvider: INotebookEditorProvider |
| 72 | + ) { |
| 73 | + this.oldCellRunState = cell.metadata.runState; |
| 74 | + this.enqueue(); |
| 75 | + } |
| 76 | + |
| 77 | + public static fromCell( |
| 78 | + cell: NotebookCell, |
| 79 | + contentProvider: INotebookContentProvider, |
| 80 | + errorHandler: IDataScienceErrorHandler, |
| 81 | + editorProvider: INotebookEditorProvider |
| 82 | + ) { |
| 83 | + return new CellExecution(cell, contentProvider, errorHandler, editorProvider); |
| 84 | + } |
| 85 | + |
| 86 | + public start() { |
| 87 | + this.started = true; |
| 88 | + // Ensure we clear the cell state and trigger a change. |
| 89 | + clearCellForExecution(this.cell); |
| 90 | + this.cell.metadata.runStartTime = new Date().getTime(); |
| 91 | + this.stopWatch.reset(); |
| 92 | + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. |
| 93 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 94 | + this.notifyCellExecution(); |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * Cancel execution. |
| 99 | + * If execution has commenced, then interrupt (via cancellation token) else dequeue from execution. |
| 100 | + */ |
| 101 | + public cancel() { |
| 102 | + // We need to notify cancellation only if execution is in progress, |
| 103 | + // coz if not, we can safely reset the states. |
| 104 | + if (this.started && !this._completed) { |
| 105 | + this.source.cancel(); |
| 106 | + } |
| 107 | + |
| 108 | + if (!this.started) { |
| 109 | + this.dequeue(); |
| 110 | + } |
| 111 | + this._result.resolve(this.cell.metadata.runState); |
| 112 | + } |
| 113 | + |
| 114 | + public completedWithErrors(error: Partial<Error>) { |
| 115 | + this.sendPerceivedCellExecute(); |
| 116 | + this.cell.metadata.lastRunDuration = this.stopWatch.elapsedTime; |
| 117 | + updateCellWithErrorStatus(this.cell, error); |
| 118 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 119 | + this.errorHandler.handleError((error as unknown) as Error).ignoreErrors(); |
| 120 | + |
| 121 | + this._completed = true; |
| 122 | + this._result.resolve(this.cell.metadata.runState); |
| 123 | + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. |
| 124 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 125 | + } |
| 126 | + |
| 127 | + public completedSuccessfully() { |
| 128 | + this.sendPerceivedCellExecute(); |
| 129 | + // If we requested a cancellation, then assume it did not even run. |
| 130 | + // If it did, then we'd get an interrupt error in the output. |
| 131 | + this.cell.metadata.runState = this.token.isCancellationRequested |
| 132 | + ? vscodeNotebookEnums.NotebookCellRunState.Idle |
| 133 | + : vscodeNotebookEnums.NotebookCellRunState.Success; |
| 134 | + |
| 135 | + this.cell.metadata.statusMessage = ''; |
| 136 | + this.cell.metadata.lastRunDuration = this.stopWatch.elapsedTime; |
| 137 | + updateCellExecutionTimes(this.cell, { |
| 138 | + startTime: this.cell.metadata.runStartTime, |
| 139 | + duration: this.cell.metadata.lastRunDuration |
| 140 | + }); |
| 141 | + // If there are any errors in the cell, then change status to error. |
| 142 | + if (this.cell.outputs.some((output) => output.outputKind === vscodeNotebookEnums.CellOutputKind.Error)) { |
| 143 | + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Error; |
| 144 | + this.cell.metadata.statusMessage = getCellStatusMessageBasedOnFirstCellErrorOutput(this.cell.outputs); |
| 145 | + } |
| 146 | + |
| 147 | + this._completed = true; |
| 148 | + this._result.resolve(this.cell.metadata.runState); |
| 149 | + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. |
| 150 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 151 | + } |
| 152 | + |
| 153 | + /** |
| 154 | + * Notify other parts of extension about the cell execution. |
| 155 | + */ |
| 156 | + private notifyCellExecution() { |
| 157 | + const editor = this.editorProvider.editors.find((e) => e.file.toString() === this.cell.notebook.uri.toString()); |
| 158 | + if (!editor) { |
| 159 | + throw new Error('No editor for Model'); |
| 160 | + } |
| 161 | + if (editor && !(editor instanceof NotebookEditor)) { |
| 162 | + throw new Error('Executing Notebook with another Editor'); |
| 163 | + } |
| 164 | + editor.notifyExecution(this.cell.document.getText()); |
| 165 | + } |
| 166 | + |
| 167 | + /** |
| 168 | + * This cell will no longer be processed for execution (even though it was meant to be). |
| 169 | + * At this point we revert cell state & indicate that it has nto started & it is not busy. |
| 170 | + */ |
| 171 | + private dequeue() { |
| 172 | + if (this.oldCellRunState === vscodeNotebookEnums.NotebookCellRunState.Running) { |
| 173 | + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Idle; |
| 174 | + } else { |
| 175 | + this.cell.metadata.runState = this.oldCellRunState; |
| 176 | + } |
| 177 | + this.cell.metadata.runStartTime = undefined; |
| 178 | + this._completed = true; |
| 179 | + this._result.resolve(this.cell.metadata.runState); |
| 180 | + // Changes to metadata must be saved in ipynb, hence mark doc has dirty. |
| 181 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 182 | + } |
| 183 | + |
| 184 | + /** |
| 185 | + * Place in queue for execution with kernel. |
| 186 | + * (mark it as busy). |
| 187 | + */ |
| 188 | + private enqueue() { |
| 189 | + this.cell.metadata.runState = vscodeNotebookEnums.NotebookCellRunState.Running; |
| 190 | + this.contentProvider.notifyChangesToDocument(this.cell.notebook); |
| 191 | + } |
| 192 | + |
| 193 | + private sendPerceivedCellExecute() { |
| 194 | + const props = { notebook: true }; |
| 195 | + if (!CellExecution.sentExecuteCellTelemetry) { |
| 196 | + CellExecution.sentExecuteCellTelemetry = true; |
| 197 | + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, this.stopWatch.elapsedTime, props); |
| 198 | + } else { |
| 199 | + sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, this.stopWatch.elapsedTime, props); |
| 200 | + } |
| 201 | + } |
| 202 | +} |
0 commit comments