Path: blob/main/src/vs/workbench/contrib/notebook/browser/services/notebookExecutionStateServiceImpl.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter } from '../../../../../base/common/event.js';6import { combinedDisposable, Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';7import { ResourceMap } from '../../../../../base/common/map.js';8import { isEqual } from '../../../../../base/common/resources.js';9import { URI } from '../../../../../base/common/uri.js';10import { generateUuid } from '../../../../../base/common/uuid.js';11import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';12import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';13import { ILogService } from '../../../../../platform/log/common/log.js';14import { NotebookTextModel } from '../../common/model/notebookTextModel.js';15import { CellEditType, CellUri, ICellEditOperation, NotebookCellExecutionState, NotebookCellInternalMetadata, NotebookExecutionState, NotebookTextModelWillAddRemoveEvent } from '../../common/notebookCommon.js';16import { CellExecutionUpdateType, INotebookExecutionService } from '../../common/notebookExecutionService.js';17import { ICellExecuteUpdate, ICellExecutionComplete, ICellExecutionStateChangedEvent, ICellExecutionStateUpdate, IExecutionStateChangedEvent, IFailedCellInfo, INotebookCellExecution, INotebookExecution, INotebookExecutionStateService, INotebookFailStateChangedEvent, NotebookExecutionType } from '../../common/notebookExecutionStateService.js';18import { INotebookKernelService } from '../../common/notebookKernelService.js';19import { INotebookService } from '../../common/notebookService.js';2021export class NotebookExecutionStateService extends Disposable implements INotebookExecutionStateService {22declare _serviceBrand: undefined;2324private readonly _executions = new ResourceMap<Map<number, CellExecution>>();25private readonly _notebookExecutions = new ResourceMap<[NotebookExecution, IDisposable]>();26private readonly _notebookListeners = new ResourceMap<NotebookExecutionListeners>();27private readonly _cellListeners = new ResourceMap<IDisposable>();28private readonly _lastFailedCells = new ResourceMap<IFailedCellInfo>();29private readonly _lastCompletedCellHandles = new ResourceMap<number>();3031private readonly _onDidChangeExecution = this._register(new Emitter<ICellExecutionStateChangedEvent | IExecutionStateChangedEvent>());32onDidChangeExecution = this._onDidChangeExecution.event;3334private readonly _onDidChangeLastRunFailState = this._register(new Emitter<INotebookFailStateChangedEvent>());35onDidChangeLastRunFailState = this._onDidChangeLastRunFailState.event;3637constructor(38@IInstantiationService private readonly _instantiationService: IInstantiationService,39@ILogService private readonly _logService: ILogService,40@INotebookService private readonly _notebookService: INotebookService,41@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService42) {43super();44}4546getLastFailedCellForNotebook(notebook: URI): number | undefined {47const failedCell = this._lastFailedCells.get(notebook);48return failedCell?.visible ? failedCell.cellHandle : undefined;49}5051getLastCompletedCellForNotebook(notebook: URI): number | undefined {52return this._lastCompletedCellHandles.get(notebook);53}5455forceCancelNotebookExecutions(notebookUri: URI): void {56const notebookCellExecutions = this._executions.get(notebookUri);57if (notebookCellExecutions) {58for (const exe of notebookCellExecutions.values()) {59this._onCellExecutionDidComplete(notebookUri, exe.cellHandle, exe);60}61}62if (this._notebookExecutions.has(notebookUri)) {63this._onExecutionDidComplete(notebookUri);64}65}6667getCellExecution(cellUri: URI): INotebookCellExecution | undefined {68const parsed = CellUri.parse(cellUri);69if (!parsed) {70throw new Error(`Not a cell URI: ${cellUri}`);71}7273const exeMap = this._executions.get(parsed.notebook);74if (exeMap) {75return exeMap.get(parsed.handle);76}7778return undefined;79}80getExecution(notebook: URI): INotebookExecution | undefined {81return this._notebookExecutions.get(notebook)?.[0];82}8384getCellExecutionsForNotebook(notebook: URI): INotebookCellExecution[] {85const exeMap = this._executions.get(notebook);86return exeMap ? Array.from(exeMap.values()) : [];87}8889getCellExecutionsByHandleForNotebook(notebook: URI): Map<number, INotebookCellExecution> | undefined {90const exeMap = this._executions.get(notebook);91return exeMap ? new Map(exeMap.entries()) : undefined;92}9394private _onCellExecutionDidChange(notebookUri: URI, cellHandle: number, exe: CellExecution): void {95this._onDidChangeExecution.fire(new NotebookCellExecutionEvent(notebookUri, cellHandle, exe));96}9798private _onCellExecutionDidComplete(notebookUri: URI, cellHandle: number, exe: CellExecution, lastRunSuccess?: boolean): void {99const notebookExecutions = this._executions.get(notebookUri);100if (!notebookExecutions) {101this._logService.debug(`NotebookExecutionStateService#_onCellExecutionDidComplete - unknown notebook ${notebookUri.toString()}`);102return;103}104105exe.dispose();106const cellUri = CellUri.generate(notebookUri, cellHandle);107this._cellListeners.get(cellUri)?.dispose();108this._cellListeners.delete(cellUri);109notebookExecutions.delete(cellHandle);110if (notebookExecutions.size === 0) {111this._executions.delete(notebookUri);112this._notebookListeners.get(notebookUri)?.dispose();113this._notebookListeners.delete(notebookUri);114}115116if (lastRunSuccess !== undefined) {117if (lastRunSuccess) {118if (this._executions.size === 0) {119this._accessibilitySignalService.playSignal(AccessibilitySignal.notebookCellCompleted);120}121this._clearLastFailedCell(notebookUri);122} else {123this._accessibilitySignalService.playSignal(AccessibilitySignal.notebookCellFailed);124this._setLastFailedCell(notebookUri, cellHandle);125}126this._lastCompletedCellHandles.set(notebookUri, cellHandle);127}128129this._onDidChangeExecution.fire(new NotebookCellExecutionEvent(notebookUri, cellHandle));130}131132private _onExecutionDidChange(notebookUri: URI, exe: NotebookExecution): void {133this._onDidChangeExecution.fire(new NotebookExecutionEvent(notebookUri, exe));134}135136private _onExecutionDidComplete(notebookUri: URI): void {137const disposables = this._notebookExecutions.get(notebookUri);138if (!Array.isArray(disposables)) {139this._logService.debug(`NotebookExecutionStateService#_onCellExecutionDidComplete - unknown notebook ${notebookUri.toString()}`);140return;141}142143this._notebookExecutions.delete(notebookUri);144this._onDidChangeExecution.fire(new NotebookExecutionEvent(notebookUri));145disposables.forEach(d => d.dispose());146}147148createCellExecution(notebookUri: URI, cellHandle: number): INotebookCellExecution {149const notebook = this._notebookService.getNotebookTextModel(notebookUri);150if (!notebook) {151throw new Error(`Notebook not found: ${notebookUri.toString()}`);152}153154let notebookExecutionMap = this._executions.get(notebookUri);155if (!notebookExecutionMap) {156const listeners = this._instantiationService.createInstance(NotebookExecutionListeners, notebookUri);157this._notebookListeners.set(notebookUri, listeners);158159notebookExecutionMap = new Map<number, CellExecution>();160this._executions.set(notebookUri, notebookExecutionMap);161}162163let exe = notebookExecutionMap.get(cellHandle);164if (!exe) {165exe = this._createNotebookCellExecution(notebook, cellHandle);166notebookExecutionMap.set(cellHandle, exe);167exe.initialize();168this._onDidChangeExecution.fire(new NotebookCellExecutionEvent(notebookUri, cellHandle, exe));169}170171return exe;172}173createExecution(notebookUri: URI): INotebookExecution {174const notebook = this._notebookService.getNotebookTextModel(notebookUri);175if (!notebook) {176throw new Error(`Notebook not found: ${notebookUri.toString()}`);177}178179if (!this._notebookListeners.has(notebookUri)) {180const listeners = this._instantiationService.createInstance(NotebookExecutionListeners, notebookUri);181this._notebookListeners.set(notebookUri, listeners);182}183184let info = this._notebookExecutions.get(notebookUri);185if (!info) {186info = this._createNotebookExecution(notebook);187this._notebookExecutions.set(notebookUri, info);188this._onDidChangeExecution.fire(new NotebookExecutionEvent(notebookUri, info[0]));189}190191return info[0];192}193194private _createNotebookCellExecution(notebook: NotebookTextModel, cellHandle: number): CellExecution {195const notebookUri = notebook.uri;196const exe: CellExecution = this._instantiationService.createInstance(CellExecution, cellHandle, notebook);197const disposable = combinedDisposable(198exe.onDidUpdate(() => this._onCellExecutionDidChange(notebookUri, cellHandle, exe)),199exe.onDidComplete(lastRunSuccess => this._onCellExecutionDidComplete(notebookUri, cellHandle, exe, lastRunSuccess)));200this._cellListeners.set(CellUri.generate(notebookUri, cellHandle), disposable);201202return exe;203}204205private _createNotebookExecution(notebook: NotebookTextModel): [NotebookExecution, IDisposable] {206const notebookUri = notebook.uri;207const exe: NotebookExecution = this._instantiationService.createInstance(NotebookExecution, notebook);208const disposable = combinedDisposable(209exe.onDidUpdate(() => this._onExecutionDidChange(notebookUri, exe)),210exe.onDidComplete(() => this._onExecutionDidComplete(notebookUri)));211return [exe, disposable];212}213214private _setLastFailedCell(notebookURI: URI, cellHandle: number): void {215const prevLastFailedCellInfo = this._lastFailedCells.get(notebookURI);216const notebook = this._notebookService.getNotebookTextModel(notebookURI);217if (!notebook) {218return;219}220221const newLastFailedCellInfo: IFailedCellInfo = {222cellHandle: cellHandle,223disposable: prevLastFailedCellInfo ? prevLastFailedCellInfo.disposable : this._getFailedCellListener(notebook),224visible: true225};226227this._lastFailedCells.set(notebookURI, newLastFailedCellInfo);228229this._onDidChangeLastRunFailState.fire({ visible: true, notebook: notebookURI });230}231232private _setLastFailedCellVisibility(notebookURI: URI, visible: boolean): void {233const lastFailedCellInfo = this._lastFailedCells.get(notebookURI);234235if (lastFailedCellInfo) {236this._lastFailedCells.set(notebookURI, {237cellHandle: lastFailedCellInfo.cellHandle,238disposable: lastFailedCellInfo.disposable,239visible: visible,240});241}242243this._onDidChangeLastRunFailState.fire({ visible: visible, notebook: notebookURI });244}245246private _clearLastFailedCell(notebookURI: URI): void {247const lastFailedCellInfo = this._lastFailedCells.get(notebookURI);248249if (lastFailedCellInfo) {250lastFailedCellInfo.disposable?.dispose();251this._lastFailedCells.delete(notebookURI);252}253254this._onDidChangeLastRunFailState.fire({ visible: false, notebook: notebookURI });255}256257private _getFailedCellListener(notebook: NotebookTextModel): IDisposable {258return notebook.onWillAddRemoveCells((e: NotebookTextModelWillAddRemoveEvent) => {259const lastFailedCell = this._lastFailedCells.get(notebook.uri)?.cellHandle;260if (lastFailedCell !== undefined) {261const lastFailedCellPos = notebook.cells.findIndex(c => c.handle === lastFailedCell);262e.rawEvent.changes.forEach(([start, deleteCount, addedCells]) => {263if (deleteCount) {264if (lastFailedCellPos >= start && lastFailedCellPos < start + deleteCount) {265this._setLastFailedCellVisibility(notebook.uri, false);266}267}268269if (addedCells.some(cell => cell.handle === lastFailedCell)) {270this._setLastFailedCellVisibility(notebook.uri, true);271}272273});274}275});276}277278override dispose(): void {279super.dispose();280this._executions.forEach(executionMap => {281executionMap.forEach(execution => execution.dispose());282executionMap.clear();283});284this._executions.clear();285this._notebookExecutions.forEach(disposables => {286disposables.forEach(d => d.dispose());287});288this._notebookExecutions.clear();289290this._cellListeners.forEach(disposable => disposable.dispose());291this._notebookListeners.forEach(disposable => disposable.dispose());292this._lastFailedCells.forEach(elem => elem.disposable.dispose());293}294}295296class NotebookCellExecutionEvent implements ICellExecutionStateChangedEvent {297readonly type = NotebookExecutionType.cell;298constructor(299readonly notebook: URI,300readonly cellHandle: number,301readonly changed?: CellExecution302) { }303304affectsCell(cell: URI): boolean {305const parsedUri = CellUri.parse(cell);306return !!parsedUri && isEqual(this.notebook, parsedUri.notebook) && this.cellHandle === parsedUri.handle;307}308309affectsNotebook(notebook: URI): boolean {310return isEqual(this.notebook, notebook);311}312}313314class NotebookExecutionEvent implements IExecutionStateChangedEvent {315readonly type = NotebookExecutionType.notebook;316constructor(317readonly notebook: URI,318readonly changed?: NotebookExecution319) { }320321affectsNotebook(notebook: URI): boolean {322return isEqual(this.notebook, notebook);323}324}325326class NotebookExecutionListeners extends Disposable {327private readonly _notebookModel: NotebookTextModel;328329constructor(330notebook: URI,331@INotebookService private readonly _notebookService: INotebookService,332@INotebookKernelService private readonly _notebookKernelService: INotebookKernelService,333@INotebookExecutionService private readonly _notebookExecutionService: INotebookExecutionService,334@INotebookExecutionStateService private readonly _notebookExecutionStateService: INotebookExecutionStateService,335@ILogService private readonly _logService: ILogService,336) {337super();338this._logService.debug(`NotebookExecution#ctor ${notebook.toString()}`);339340const notebookModel = this._notebookService.getNotebookTextModel(notebook);341if (!notebookModel) {342throw new Error('Notebook not found: ' + notebook);343}344345this._notebookModel = notebookModel;346this._register(this._notebookModel.onWillAddRemoveCells(e => this.onWillAddRemoveCells(e)));347this._register(this._notebookModel.onWillDispose(() => this.onWillDisposeDocument()));348}349350private cancelAll(): void {351this._logService.debug(`NotebookExecutionListeners#cancelAll`);352const exes = this._notebookExecutionStateService.getCellExecutionsForNotebook(this._notebookModel.uri);353this._notebookExecutionService.cancelNotebookCellHandles(this._notebookModel, exes.map(exe => exe.cellHandle));354}355356private onWillDisposeDocument(): void {357this._logService.debug(`NotebookExecution#onWillDisposeDocument`);358this.cancelAll();359}360361private onWillAddRemoveCells(e: NotebookTextModelWillAddRemoveEvent): void {362const notebookExes = this._notebookExecutionStateService.getCellExecutionsByHandleForNotebook(this._notebookModel.uri);363364const executingDeletedHandles = new Set<number>();365const pendingDeletedHandles = new Set<number>();366if (notebookExes) {367e.rawEvent.changes.forEach(([start, deleteCount]) => {368if (deleteCount) {369const deletedHandles = this._notebookModel.cells.slice(start, start + deleteCount).map(c => c.handle);370deletedHandles.forEach(h => {371const exe = notebookExes.get(h);372if (exe?.state === NotebookCellExecutionState.Executing) {373executingDeletedHandles.add(h);374} else if (exe) {375pendingDeletedHandles.add(h);376}377});378}379});380}381382if (executingDeletedHandles.size || pendingDeletedHandles.size) {383const kernel = this._notebookKernelService.getSelectedOrSuggestedKernel(this._notebookModel);384if (kernel) {385const implementsInterrupt = kernel.implementsInterrupt;386const handlesToCancel = implementsInterrupt ? [...executingDeletedHandles] : [...executingDeletedHandles, ...pendingDeletedHandles];387this._logService.debug(`NotebookExecution#onWillAddRemoveCells, ${JSON.stringify([...handlesToCancel])}`);388if (handlesToCancel.length) {389kernel.cancelNotebookCellExecution(this._notebookModel.uri, handlesToCancel);390}391}392}393}394}395396function updateToEdit(update: ICellExecuteUpdate, cellHandle: number): ICellEditOperation {397if (update.editType === CellExecutionUpdateType.Output) {398return {399editType: CellEditType.Output,400handle: update.cellHandle,401append: update.append,402outputs: update.outputs,403};404} else if (update.editType === CellExecutionUpdateType.OutputItems) {405return {406editType: CellEditType.OutputItems,407items: update.items,408append: update.append,409outputId: update.outputId410};411} else if (update.editType === CellExecutionUpdateType.ExecutionState) {412const newInternalMetadata: Partial<NotebookCellInternalMetadata> = {};413if (typeof update.executionOrder !== 'undefined') {414newInternalMetadata.executionOrder = update.executionOrder;415}416if (typeof update.runStartTime !== 'undefined') {417newInternalMetadata.runStartTime = update.runStartTime;418}419return {420editType: CellEditType.PartialInternalMetadata,421handle: cellHandle,422internalMetadata: newInternalMetadata423};424}425426throw new Error('Unknown cell update type');427}428429class CellExecution extends Disposable implements INotebookCellExecution {430private readonly _onDidUpdate = this._register(new Emitter<void>());431readonly onDidUpdate = this._onDidUpdate.event;432433private readonly _onDidComplete = this._register(new Emitter<boolean | undefined>());434readonly onDidComplete = this._onDidComplete.event;435436private _state: NotebookCellExecutionState = NotebookCellExecutionState.Unconfirmed;437get state() {438return this._state;439}440441get notebook(): URI {442return this._notebookModel.uri;443}444445private _didPause = false;446get didPause() {447return this._didPause;448}449450private _isPaused = false;451get isPaused() {452return this._isPaused;453}454455constructor(456readonly cellHandle: number,457private readonly _notebookModel: NotebookTextModel,458@ILogService private readonly _logService: ILogService,459) {460super();461this._logService.debug(`CellExecution#ctor ${this.getCellLog()}`);462}463464initialize() {465const startExecuteEdit: ICellEditOperation = {466editType: CellEditType.PartialInternalMetadata,467handle: this.cellHandle,468internalMetadata: {469executionId: generateUuid(),470runStartTime: null,471runEndTime: null,472lastRunSuccess: null,473executionOrder: null,474renderDuration: null,475}476};477this._applyExecutionEdits([startExecuteEdit]);478}479480private getCellLog(): string {481return `${this._notebookModel.uri.toString()}, ${this.cellHandle}`;482}483484private logUpdates(updates: ICellExecuteUpdate[]): void {485const updateTypes = updates.map(u => CellExecutionUpdateType[u.editType]).join(', ');486this._logService.debug(`CellExecution#updateExecution ${this.getCellLog()}, [${updateTypes}]`);487}488489confirm() {490this._logService.debug(`CellExecution#confirm ${this.getCellLog()}`);491this._state = NotebookCellExecutionState.Pending;492this._onDidUpdate.fire();493}494495update(updates: ICellExecuteUpdate[]): void {496this.logUpdates(updates);497if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) {498this._state = NotebookCellExecutionState.Executing;499}500501if (!this._didPause && updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState && u.didPause)) {502this._didPause = true;503}504505const lastIsPausedUpdate = [...updates].reverse().find(u => u.editType === CellExecutionUpdateType.ExecutionState && typeof u.isPaused === 'boolean');506if (lastIsPausedUpdate) {507this._isPaused = (lastIsPausedUpdate as ICellExecutionStateUpdate).isPaused!;508}509510const cellModel = this._notebookModel.cells.find(c => c.handle === this.cellHandle);511if (!cellModel) {512this._logService.debug(`CellExecution#update, updating cell not in notebook: ${this._notebookModel.uri.toString()}, ${this.cellHandle}`);513} else {514const edits = updates.map(update => updateToEdit(update, this.cellHandle));515this._applyExecutionEdits(edits);516}517518if (updates.some(u => u.editType === CellExecutionUpdateType.ExecutionState)) {519this._onDidUpdate.fire();520}521}522523complete(completionData: ICellExecutionComplete): void {524const cellModel = this._notebookModel.cells.find(c => c.handle === this.cellHandle);525if (!cellModel) {526this._logService.debug(`CellExecution#complete, completing cell not in notebook: ${this._notebookModel.uri.toString()}, ${this.cellHandle}`);527} else {528const edit: ICellEditOperation = {529editType: CellEditType.PartialInternalMetadata,530handle: this.cellHandle,531internalMetadata: {532lastRunSuccess: completionData.lastRunSuccess,533runStartTime: this._didPause ? null : cellModel.internalMetadata.runStartTime,534runEndTime: this._didPause ? null : completionData.runEndTime,535error: completionData.error536}537};538this._applyExecutionEdits([edit]);539}540541this._onDidComplete.fire(completionData.lastRunSuccess);542}543544private _applyExecutionEdits(edits: ICellEditOperation[]): void {545this._notebookModel.applyEdits(edits, true, undefined, () => undefined, undefined, false);546}547}548549class NotebookExecution extends Disposable implements INotebookExecution {550private readonly _onDidUpdate = this._register(new Emitter<void>());551readonly onDidUpdate = this._onDidUpdate.event;552553private readonly _onDidComplete = this._register(new Emitter<void>());554readonly onDidComplete = this._onDidComplete.event;555556private _state: NotebookExecutionState = NotebookExecutionState.Unconfirmed;557get state() {558return this._state;559}560561get notebook(): URI {562return this._notebookModel.uri;563}564565constructor(566private readonly _notebookModel: NotebookTextModel,567@ILogService private readonly _logService: ILogService,568) {569super();570this._logService.debug(`NotebookExecution#ctor`);571}572private debug(message: string) {573this._logService.debug(`${message} ${this._notebookModel.uri.toString()}`);574}575576confirm() {577this.debug(`Execution#confirm`);578this._state = NotebookExecutionState.Pending;579this._onDidUpdate.fire();580}581582begin(): void {583this.debug(`Execution#begin`);584this._state = NotebookExecutionState.Executing;585this._onDidUpdate.fire();586}587588complete(): void {589this.debug(`Execution#begin`);590this._state = NotebookExecutionState.Unconfirmed;591this._onDidComplete.fire();592}593}594595596