diff --git a/src/api.proposed.kernelStartHook.d.ts b/src/api.proposed.kernelStartHook.d.ts index 5377ee50f08..3b298851726 100644 --- a/src/api.proposed.kernelStartHook.d.ts +++ b/src/api.proposed.kernelStartHook.d.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { Event, Uri } from 'vscode'; +import type { CancellationToken, Event, Uri } from 'vscode'; declare module './api' { export interface Kernels { @@ -11,6 +11,16 @@ declare module './api' { onDidStart: Event<{ uri: Uri; kernel: Kernel; + token: CancellationToken; + /** + * Allows to pause the event loop until the provided thenable resolved. + * This can be useful to ensure startup code is executed before user code. + * + * *Note:* This function can only be called during event dispatch. + * + * @param thenable A thenable that delays kernel startup. + */ + waitUntil(thenable: Thenable): void; }>; } } diff --git a/src/gdpr.ts b/src/gdpr.ts index 36c6812749a..aeeb04708e9 100644 --- a/src/gdpr.ts +++ b/src/gdpr.ts @@ -592,7 +592,18 @@ ] } */ -// (47). Telemetry.NewJupyterKernelApiUsage (DATASCIENCE.JUPYTER_NEW_KERNEL_API_USAGE) +// (47). Telemetry.NewJupyterKernelApiKernelStartupWaitUntil (DATASCIENCE.JUPYTER_NEW_KERNEL_API_KERNEL_STARTUP_WAIT_UNTIL) +// Telemetry sent when an extension uses our 3rd party Kernel onDidStart API and calls waitUntil. +/* __GDPR__ + "DATASCIENCE.JUPYTER_NEW_KERNEL_API_KERNEL_STARTUP_WAIT_UNTIL" : { + "extensionId": {"classification":"PublicNonPersonalData","purpose":"FeatureInsight","comment":"Extension Id that's attempting to use the API.","owner":"donjayamanne"}, + "${include}": [ + "${F1}" + + ] + } + */ +// (48). Telemetry.NewJupyterKernelApiUsage (DATASCIENCE.JUPYTER_NEW_KERNEL_API_USAGE) // Telemetry sent when an extension uses our 3rd party Kernel Execution API. /* __GDPR__ "DATASCIENCE.JUPYTER_NEW_KERNEL_API_USAGE" : { @@ -606,7 +617,7 @@ ] } */ -// (48). Telemetry.NewJupyterKernelsApiUsage (DATASCIENCE.JUPYTER_NEW_KERNELS_API_USAGE) +// (49). Telemetry.NewJupyterKernelsApiUsage (DATASCIENCE.JUPYTER_NEW_KERNELS_API_USAGE) // Telemetry sent when an extension uses our 3rd party Kernels API. /* __GDPR__ "DATASCIENCE.JUPYTER_NEW_KERNELS_API_USAGE" : { @@ -619,7 +630,7 @@ ] } */ -// (49). Telemetry.NativeNotebookEditPerformance (DATASCIENCE.JUPYTER_NOTEBOOK_EDIT_PERFORMANCE) +// (50). Telemetry.NativeNotebookEditPerformance (DATASCIENCE.JUPYTER_NOTEBOOK_EDIT_PERFORMANCE) // Telemetry sent during test on CI to measure performance of execution of large notebooks. /* __GDPR__ "DATASCIENCE.JUPYTER_NOTEBOOK_EDIT_PERFORMANCE" : { @@ -631,7 +642,7 @@ ] } */ -// (50). Telemetry.NativeNotebookExecutionPerformance (DATASCIENCE.JUPYTER_NOTEBOOK_EXEC_PERFORMANCE) +// (51). Telemetry.NativeNotebookExecutionPerformance (DATASCIENCE.JUPYTER_NOTEBOOK_EXEC_PERFORMANCE) // Telemetry sent during test on CI to measure performance of execution of large notebooks. /* __GDPR__ "DATASCIENCE.JUPYTER_NOTEBOOK_EXEC_PERFORMANCE" : { @@ -647,7 +658,7 @@ ] } */ -// (51). Telemetry.KernelCrash (DATASCIENCE.KERNEL_CRASH) +// (52). Telemetry.KernelCrash (DATASCIENCE.KERNEL_CRASH) // Sent when Kernel crashes. /* __GDPR__ "DATASCIENCE.KERNEL_CRASH" : { @@ -674,7 +685,7 @@ ] } */ -// (52). Telemetry.KernelSpecLanguage (DATASCIENCE.KERNEL_SPEC_LANGUAGE) +// (53). Telemetry.KernelSpecLanguage (DATASCIENCE.KERNEL_SPEC_LANGUAGE) // Sent to detect the different languages of kernel specs used. /* __GDPR__ "DATASCIENCE.KERNEL_SPEC_LANGUAGE" : { @@ -687,7 +698,7 @@ ] } */ -// (53). Telemetry.OpenNotebookAll (DATASCIENCE.NATIVE.OPEN_NOTEBOOK_ALL) +// (54). Telemetry.OpenNotebookAll (DATASCIENCE.NATIVE.OPEN_NOTEBOOK_ALL) // Sent when we have opened any Jupyter notebook in a VS Code session. // Not tagging as a user action as this could be something like auto opening a file // from a previous session and not a direct user action. @@ -701,7 +712,7 @@ ] } */ -// (54). Telemetry.NoActiveKernelSession (DATASCIENCE.NO_ACTIVE_KERNEL_SESSION) +// (55). Telemetry.NoActiveKernelSession (DATASCIENCE.NO_ACTIVE_KERNEL_SESSION) // Send when we want to install data viewer dependendies, but don't have an active kernel session. // Used by the dataViewerDependencyService. /* __GDPR__ @@ -712,7 +723,7 @@ ] } */ -// (55). Telemetry.NotebookFirstKernelAutoSelectionBreakDown (DATASCIENCE.NOTEBOOK_FIRST_KERNEL_AUTO_SELECTION_BREAKDOWN) +// (56). Telemetry.NotebookFirstKernelAutoSelectionBreakDown (DATASCIENCE.NOTEBOOK_FIRST_KERNEL_AUTO_SELECTION_BREAKDOWN) // This event is sent to measure the times involved in automatically selecting the first kernel of a notebook. /* __GDPR__ "DATASCIENCE.NOTEBOOK_FIRST_KERNEL_AUTO_SELECTION_BREAKDOWN" : { @@ -745,7 +756,7 @@ ] } */ -// (56). Telemetry.NotebookFirstStartBreakDown (DATASCIENCE.NOTEBOOK_FIRST_START_BREAKDOWN) +// (57). Telemetry.NotebookFirstStartBreakDown (DATASCIENCE.NOTEBOOK_FIRST_START_BREAKDOWN) // This event is sent to measure the times involved in various parts of extension when running a cell. // The reference time is `openedAfter` (time when the notebook was opened). // All other times are relative to this time, except in the case where a notebook was already opened before the extension was activated. @@ -804,7 +815,7 @@ ] } */ -// (57). Telemetry.NotebookInterrupt (DATASCIENCE.NOTEBOOK_INTERRUPT) +// (58). Telemetry.NotebookInterrupt (DATASCIENCE.NOTEBOOK_INTERRUPT) // Telemetry sent when user interrupts the kernel. // Check the `resourceType` to determine whether its a Jupyter Notebook or IW. /* __GDPR__ @@ -833,7 +844,7 @@ ] } */ -// (58). Telemetry.NotebookRestart (DATASCIENCE.NOTEBOOK_RESTART) +// (59). Telemetry.NotebookRestart (DATASCIENCE.NOTEBOOK_RESTART) // Telemetry sent when user Restarts the Kernel. // Check the `resourceType` to determine whether its a Jupyter Notebook or IW. /* __GDPR__ @@ -861,7 +872,7 @@ ] } */ -// (59). Telemetry.NotebookStart (DATASCIENCE.NOTEBOOK_START) +// (60). Telemetry.NotebookStart (DATASCIENCE.NOTEBOOK_START) // Send when a kernel starts. /* __GDPR__ "DATASCIENCE.NOTEBOOK_START" : { @@ -888,7 +899,7 @@ ] } */ -// (60). Telemetry.OpenPlotViewer (DATASCIENCE.OPEN_PLOT_VIEWER) +// (61). Telemetry.OpenPlotViewer (DATASCIENCE.OPEN_PLOT_VIEWER) // A new instance of the plot viewer was opened. /* __GDPR__ "DATASCIENCE.OPEN_PLOT_VIEWER" : { @@ -898,7 +909,7 @@ ] } */ -// (61). Telemetry.PythonVariableFetchingCodeFailure (DATASCIENCE.PYTHON_VARIABLE_FETCHING_CODE_FAILURE) +// (62). Telemetry.PythonVariableFetchingCodeFailure (DATASCIENCE.PYTHON_VARIABLE_FETCHING_CODE_FAILURE) // The Python code that we ran to fetch variables had a failure. /* __GDPR__ "DATASCIENCE.PYTHON_VARIABLE_FETCHING_CODE_FAILURE" : { @@ -908,7 +919,7 @@ ] } */ -// (62). Telemetry.RecommendExtension (DATASCIENCE.RECOMMENT_EXTENSION) +// (63). Telemetry.RecommendExtension (DATASCIENCE.RECOMMENT_EXTENSION) // Telemetry sent when we recommend installing an extension. /* __GDPR__ "DATASCIENCE.RECOMMENT_EXTENSION" : { @@ -920,7 +931,7 @@ ] } */ -// (63). Telemetry.RefreshDataViewer (DATASCIENCE.REFRESH_DATA_VIEWER) +// (64). Telemetry.RefreshDataViewer (DATASCIENCE.REFRESH_DATA_VIEWER) // Sent when the jupyter.refreshDataViewer command is invoked /* __GDPR__ "DATASCIENCE.REFRESH_DATA_VIEWER" : { @@ -930,7 +941,7 @@ ] } */ -// (64). Telemetry.ResumeCellExecution (DATASCIENCE.RESUME_EXECUTE_CELL) +// (65). Telemetry.ResumeCellExecution (DATASCIENCE.RESUME_EXECUTE_CELL) // Sent when we resume execution of a cell (e.g. when reloading VS Code while a cell was executing). /* __GDPR__ "DATASCIENCE.RESUME_EXECUTE_CELL" : { @@ -957,7 +968,7 @@ ] } */ -// (65). Telemetry.RunAllCells (DATASCIENCE.RUN_ALL_CELLS) +// (66). Telemetry.RunAllCells (DATASCIENCE.RUN_ALL_CELLS) // Command to Run all cells from the active python file in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_ALL_CELLS" : { @@ -967,7 +978,7 @@ ] } */ -// (66). Telemetry.RunAllCellsAbove (DATASCIENCE.RUN_ALL_CELLS_ABOVE) +// (67). Telemetry.RunAllCellsAbove (DATASCIENCE.RUN_ALL_CELLS_ABOVE) // Command to Run all the above cells in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_ALL_CELLS_ABOVE" : { @@ -977,7 +988,7 @@ ] } */ -// (67). Telemetry.RunByLineVariableHover (DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER) +// (68). Telemetry.RunByLineVariableHover (DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER) // Fired when a user hovers a variable while debugging the IW. /* __GDPR__ "DATASCIENCE.RUN_BY_LINE_VARIABLE_HOVER" : { @@ -987,7 +998,7 @@ ] } */ -// (68). Telemetry.RunCellAndAllBelow (DATASCIENCE.RUN_CELL_AND_ALL_BELOW) +// (69). Telemetry.RunCellAndAllBelow (DATASCIENCE.RUN_CELL_AND_ALL_BELOW) // Command to Run current cell and all below in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_CELL_AND_ALL_BELOW" : { @@ -997,7 +1008,7 @@ ] } */ -// (69). Telemetry.ChangeCellToCode (DATASCIENCE.RUN_CHANGE_CELL_TO_CODE) +// (70). Telemetry.ChangeCellToCode (DATASCIENCE.RUN_CHANGE_CELL_TO_CODE) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_CHANGE_CELL_TO_CODE" : { @@ -1007,7 +1018,7 @@ ] } */ -// (70). Telemetry.ChangeCellToMarkdown (DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN) +// (71). Telemetry.ChangeCellToMarkdown (DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_CHANGE_CELL_TO_MARKDOWN" : { @@ -1017,7 +1028,7 @@ ] } */ -// (71). Telemetry.RunCurrentCell (DATASCIENCE.RUN_CURRENT_CELL) +// (72). Telemetry.RunCurrentCell (DATASCIENCE.RUN_CURRENT_CELL) // Command to Run the current Cell in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_CURRENT_CELL" : { @@ -1027,7 +1038,7 @@ ] } */ -// (72). Telemetry.RunCurrentCellAndAddBelow (DATASCIENCE.RUN_CURRENT_CELL_AND_ADD_BELOW) +// (73). Telemetry.RunCurrentCellAndAddBelow (DATASCIENCE.RUN_CURRENT_CELL_AND_ADD_BELOW) // Run the cell and everything below it in the Interactive Window. /* __GDPR__ "DATASCIENCE.RUN_CURRENT_CELL_AND_ADD_BELOW" : { @@ -1037,7 +1048,7 @@ ] } */ -// (73). Telemetry.RunCurrentCellAndAdvance (DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE) +// (74). Telemetry.RunCurrentCellAndAdvance (DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE) // Command to Run current cell in the Interactive Window and advance cursor to the next cell /* __GDPR__ "DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE" : { @@ -1047,7 +1058,7 @@ ] } */ -// (74). Telemetry.DeleteCells (DATASCIENCE.RUN_DELETE_CELLS) +// (75). Telemetry.DeleteCells (DATASCIENCE.RUN_DELETE_CELLS) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_DELETE_CELLS" : { @@ -1057,7 +1068,7 @@ ] } */ -// (75). Telemetry.ExtendSelectionByCellAbove (DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_ABOVE) +// (76). Telemetry.ExtendSelectionByCellAbove (DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_ABOVE) // Cell Selection Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_ABOVE" : { @@ -1067,7 +1078,7 @@ ] } */ -// (76). Telemetry.ExtendSelectionByCellBelow (DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_BELOW) +// (77). Telemetry.ExtendSelectionByCellBelow (DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_BELOW) // Cell Selection Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_EXTEND_SELECTION_BY_CELL_BELOW" : { @@ -1077,7 +1088,7 @@ ] } */ -// (77). Telemetry.RunFileInteractive (DATASCIENCE.RUN_FILE_INTERACTIVE) +// (78). Telemetry.RunFileInteractive (DATASCIENCE.RUN_FILE_INTERACTIVE) // Command to Run the active file in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_FILE_INTERACTIVE" : { @@ -1087,7 +1098,7 @@ ] } */ -// (78). Telemetry.RunFromLine (DATASCIENCE.RUN_FROM_LINE) +// (79). Telemetry.RunFromLine (DATASCIENCE.RUN_FROM_LINE) // Command to Run the active file contents from the cursor location in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_FROM_LINE" : { @@ -1097,7 +1108,7 @@ ] } */ -// (79). Telemetry.InsertCellAbove (DATASCIENCE.RUN_INSERT_CELL_ABOVE) +// (80). Telemetry.InsertCellAbove (DATASCIENCE.RUN_INSERT_CELL_ABOVE) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_INSERT_CELL_ABOVE" : { @@ -1107,7 +1118,7 @@ ] } */ -// (80). Telemetry.InsertCellBelow (DATASCIENCE.RUN_INSERT_CELL_BELOW) +// (81). Telemetry.InsertCellBelow (DATASCIENCE.RUN_INSERT_CELL_BELOW) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_INSERT_CELL_BELOW" : { @@ -1117,7 +1128,7 @@ ] } */ -// (81). Telemetry.InsertCellBelowPosition (DATASCIENCE.RUN_INSERT_CELL_BELOW_POSITION) +// (82). Telemetry.InsertCellBelowPosition (DATASCIENCE.RUN_INSERT_CELL_BELOW_POSITION) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_INSERT_CELL_BELOW_POSITION" : { @@ -1127,7 +1138,7 @@ ] } */ -// (82). Telemetry.MoveCellsDown (DATASCIENCE.RUN_MOVE_CELLS_DOWN) +// (83). Telemetry.MoveCellsDown (DATASCIENCE.RUN_MOVE_CELLS_DOWN) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_MOVE_CELLS_DOWN" : { @@ -1137,7 +1148,7 @@ ] } */ -// (83). Telemetry.MoveCellsUp (DATASCIENCE.RUN_MOVE_CELLS_UP) +// (84). Telemetry.MoveCellsUp (DATASCIENCE.RUN_MOVE_CELLS_UP) // Cell Edit Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_MOVE_CELLS_UP" : { @@ -1147,7 +1158,7 @@ ] } */ -// (84). Telemetry.SelectCell (DATASCIENCE.RUN_SELECT_CELL) +// (85). Telemetry.SelectCell (DATASCIENCE.RUN_SELECT_CELL) // Cell Selection Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_SELECT_CELL" : { @@ -1157,7 +1168,7 @@ ] } */ -// (85). Telemetry.SelectCellContents (DATASCIENCE.RUN_SELECT_CELL_CONTENTS) +// (86). Telemetry.SelectCellContents (DATASCIENCE.RUN_SELECT_CELL_CONTENTS) // Cell Selection Command in Interactive Window /* __GDPR__ "DATASCIENCE.RUN_SELECT_CELL_CONTENTS" : { @@ -1167,7 +1178,7 @@ ] } */ -// (86). Telemetry.RunSelectionOrLine (DATASCIENCE.RUN_SELECTION_OR_LINE) +// (87). Telemetry.RunSelectionOrLine (DATASCIENCE.RUN_SELECTION_OR_LINE) // Command to Run a Selection or Line in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_SELECTION_OR_LINE" : { @@ -1177,7 +1188,7 @@ ] } */ -// (87). Telemetry.RunToLine (DATASCIENCE.RUN_TO_LINE) +// (88). Telemetry.RunToLine (DATASCIENCE.RUN_TO_LINE) // Command to Run the active file contents up to the cursor location in the Interactive Window /* __GDPR__ "DATASCIENCE.RUN_TO_LINE" : { @@ -1187,7 +1198,7 @@ ] } */ -// (88). Telemetry.SelfCertsMessageClose (DATASCIENCE.SELFCERTSMESSAGECLOSE) +// (89). Telemetry.SelfCertsMessageClose (DATASCIENCE.SELFCERTSMESSAGECLOSE) // Sent when users chose not to allow connecting to Jupyter over HTTPS when certificate isn't trusted by a trusted CA. /* __GDPR__ "DATASCIENCE.SELFCERTSMESSAGECLOSE" : { @@ -1197,7 +1208,7 @@ ] } */ -// (89). Telemetry.SelfCertsMessageEnabled (DATASCIENCE.SELFCERTSMESSAGEENABLED) +// (90). Telemetry.SelfCertsMessageEnabled (DATASCIENCE.SELFCERTSMESSAGEENABLED) // Sent when users chose to use self-signed certificates when connecting to Jupyter over https. // Basically this means users has chosen to connect to Jupyter over HTTPS when certificate isn't trusted by a trusted CA. /* __GDPR__ @@ -1208,7 +1219,7 @@ ] } */ -// (90). Telemetry.ShowDataViewer (DATASCIENCE.SHOW_DATA_EXPLORER) +// (91). Telemetry.ShowDataViewer (DATASCIENCE.SHOW_DATA_EXPLORER) // Request was made to show the data viewer with specific data frame info. /* __GDPR__ "DATASCIENCE.SHOW_DATA_EXPLORER" : { @@ -1220,7 +1231,7 @@ ] } */ -// (91). Telemetry.ShowDataViewerRowsLoaded (DATASCIENCE.SHOW_DATA_EXPLORER_ROWS_LOADED) +// (92). Telemetry.ShowDataViewerRowsLoaded (DATASCIENCE.SHOW_DATA_EXPLORER_ROWS_LOADED) // Data viewer loads rows in chunks, this event is sent when the rows have all been loaded /* __GDPR__ "DATASCIENCE.SHOW_DATA_EXPLORER_ROWS_LOADED" : { @@ -1231,7 +1242,7 @@ ] } */ -// (92). Telemetry.StartShowDataViewer (DATASCIENCE.START_SHOW_DATA_EXPLORER) +// (93). Telemetry.StartShowDataViewer (DATASCIENCE.START_SHOW_DATA_EXPLORER) // User requested to open the data frame viewer. /* __GDPR__ "DATASCIENCE.START_SHOW_DATA_EXPLORER" : { @@ -1241,7 +1252,7 @@ ] } */ -// (93). Telemetry.UserDidNotInstallJupyter (DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER) +// (94). Telemetry.UserDidNotInstallJupyter (DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER) // Sent when user click `cancel` button when prompted to install Jupyter. /* __GDPR__ "DATASCIENCE.USER_DID_NOT_INSTALL_JUPYTER" : { @@ -1251,7 +1262,7 @@ ] } */ -// (94). Telemetry.UserDidNotInstallPandas (DATASCIENCE.USER_DID_NOT_INSTALL_PANDAS) +// (95). Telemetry.UserDidNotInstallPandas (DATASCIENCE.USER_DID_NOT_INSTALL_PANDAS) // Prompted to install Pandas and chose not to install // Note: This could be just ignoring the UI so not a user action. /* __GDPR__ @@ -1262,7 +1273,7 @@ ] } */ -// (95). Telemetry.UserInstalledJupyter (DATASCIENCE.USER_INSTALLED_JUPYTER) +// (96). Telemetry.UserInstalledJupyter (DATASCIENCE.USER_INSTALLED_JUPYTER) // Sent when user installs Jupyter. /* __GDPR__ "DATASCIENCE.USER_INSTALLED_JUPYTER" : { @@ -1272,7 +1283,7 @@ ] } */ -// (96). Telemetry.UserInstalledPandas (DATASCIENCE.USER_INSTALLED_PANDAS) +// (97). Telemetry.UserInstalledPandas (DATASCIENCE.USER_INSTALLED_PANDAS) // Installed the python Pandas package. /* __GDPR__ "DATASCIENCE.USER_INSTALLED_PANDAS" : { @@ -1282,7 +1293,7 @@ ] } */ -// (97). Telemetry.DataViewerUsingInterpreter (DATAVIEWER.USING_INTERPRETER) +// (98). Telemetry.DataViewerUsingInterpreter (DATAVIEWER.USING_INTERPRETER) // When the Data Viewer installer is using a Python interpreter to do the install. /* __GDPR__ "DATAVIEWER.USING_INTERPRETER" : { @@ -1292,7 +1303,7 @@ ] } */ -// (98). Telemetry.DataViewerUsingKernel (DATAVIEWER.USING_KERNEL) +// (99). Telemetry.DataViewerUsingKernel (DATAVIEWER.USING_KERNEL) // When the Data Viewer installer is using the Kernel to do the install. /* __GDPR__ "DATAVIEWER.USING_KERNEL" : { @@ -1302,7 +1313,7 @@ ] } */ -// (99). Telemetry.DataViewerWebviewLoaded (DATAVIEWER.WEBVIEW_LOADED) +// (100). Telemetry.DataViewerWebviewLoaded (DATAVIEWER.WEBVIEW_LOADED) // The Data Viewer webview was loaded. /* __GDPR__ "DATAVIEWER.WEBVIEW_LOADED" : { @@ -1312,7 +1323,7 @@ ] } */ -// (100). Telemetry.ActiveInterpreterListingPerf (DS_INTERNAL.ACTIVE_INTERPRETER_LISTING_PERF) +// (101). Telemetry.ActiveInterpreterListingPerf (DS_INTERNAL.ACTIVE_INTERPRETER_LISTING_PERF) // Total time taken by Python extension to return the active Python environment. /* __GDPR__ "DS_INTERNAL.ACTIVE_INTERPRETER_LISTING_PERF" : { @@ -1323,7 +1334,7 @@ ] } */ -// (101). Telemetry.CellOutputMimeType (DS_INTERNAL.CELL_OUTPUT_MIME_TYPE) +// (102). Telemetry.CellOutputMimeType (DS_INTERNAL.CELL_OUTPUT_MIME_TYPE) // Mime type of a cell output. // Used to detect the popularity of a mime type, that would help determine which mime types are most common. // E.g. if we see widget mimetype, then we know how many use ipywidgets and the like and helps us prioritize widget issues, @@ -1339,7 +1350,7 @@ ] } */ -// (102). Telemetry.CodeLensAverageAcquisitionTime (DS_INTERNAL.CODE_LENS_ACQ_TIME) +// (103). Telemetry.CodeLensAverageAcquisitionTime (DS_INTERNAL.CODE_LENS_ACQ_TIME) // How long on average we spent parsing code lens. Sent on shutdown. // We should be able to deprecate in favor of DocumentWithCodeCells, but we should compare the numbers first. /* __GDPR__ @@ -1350,7 +1361,7 @@ ] } */ -// (103). Telemetry.CommandExecuted (DS_INTERNAL.COMMAND_EXECUTED) +// (104). Telemetry.CommandExecuted (DS_INTERNAL.COMMAND_EXECUTED) // A command that the extension contributes is executed. /* __GDPR__ "DS_INTERNAL.COMMAND_EXECUTED" : { @@ -1361,7 +1372,7 @@ ] } */ -// (104). Telemetry.CreateInteractiveWindow (DS_INTERNAL.CREATED_INTERACTIVE_WINDOW) +// (105). Telemetry.CreateInteractiveWindow (DS_INTERNAL.CREATED_INTERACTIVE_WINDOW) /* __GDPR__ "DS_INTERNAL.CREATED_INTERACTIVE_WINDOW" : { "hasKernel": {"classification":"SystemMetaData","purpose":"FeatureInsight","comment":"If the kernel was known at the time of creation","owner":"amunger"}, @@ -1375,7 +1386,7 @@ ] } */ -// (105). Telemetry.DocumentWithCodeCells (DS_INTERNAL.DOCUMENT_WITH_CODE_CELLS) +// (106). Telemetry.DocumentWithCodeCells (DS_INTERNAL.DOCUMENT_WITH_CODE_CELLS) // Info about code lenses, count and average time to parse the document. /* __GDPR__ "DS_INTERNAL.DOCUMENT_WITH_CODE_CELLS" : { @@ -1387,7 +1398,7 @@ ] } */ -// (106). Telemetry.ExperimentLoad (DS_INTERNAL.EXPERIMENT_LOAD) +// (107). Telemetry.ExperimentLoad (DS_INTERNAL.EXPERIMENT_LOAD) // Telemetry event sent with perf measures related to loading experiments. /* __GDPR__ "DS_INTERNAL.EXPERIMENT_LOAD" : { @@ -1397,7 +1408,7 @@ ] } */ -// (107). Telemetry.GetActivatedEnvironmentVariables (DS_INTERNAL.GET_ACTIVATED_ENV_VARIABLES) +// (108). Telemetry.GetActivatedEnvironmentVariables (DS_INTERNAL.GET_ACTIVATED_ENV_VARIABLES) // Used to capture time taken to get environment variables for a python environment. // Also lets us know whether it worked or not. /* __GDPR__ @@ -1411,7 +1422,7 @@ ] } */ -// (108). Telemetry.GetPasswordFailure (DS_INTERNAL.GET_PASSWORD_FAILURE) +// (109). Telemetry.GetPasswordFailure (DS_INTERNAL.GET_PASSWORD_FAILURE) // Sent to indicate we've failed to connect to a Remote Jupyter Server successfully after requesting a password. /* __GDPR__ "DS_INTERNAL.GET_PASSWORD_FAILURE" : { @@ -1421,7 +1432,7 @@ ] } */ -// (109). Telemetry.GetPasswordSuccess (DS_INTERNAL.GET_PASSWORD_SUCCESS) +// (110). Telemetry.GetPasswordSuccess (DS_INTERNAL.GET_PASSWORD_SUCCESS) // Sent to indicate we've connected to a Remote Jupyter Server successfully after requesting a password. /* __GDPR__ "DS_INTERNAL.GET_PASSWORD_SUCCESS" : { @@ -1431,7 +1442,7 @@ ] } */ -// (110). Telemetry.InteractiveFileTooltipsPerf (DS_INTERNAL.INTERACTIVE_FILE_TOOLTIPS_PERF) +// (111). Telemetry.InteractiveFileTooltipsPerf (DS_INTERNAL.INTERACTIVE_FILE_TOOLTIPS_PERF) // How long it took to return our hover tooltips for a .py file. /* __GDPR__ "DS_INTERNAL.INTERACTIVE_FILE_TOOLTIPS_PERF" : { @@ -1442,7 +1453,7 @@ ] } */ -// (111). Telemetry.DiscoverIPyWidgetNamesPerf (DS_INTERNAL.IPYWIDGET_DISCOVER_WIDGETS_NB_EXTENSIONS) +// (112). Telemetry.DiscoverIPyWidgetNamesPerf (DS_INTERNAL.IPYWIDGET_DISCOVER_WIDGETS_NB_EXTENSIONS) // Total time taken to discover all IPyWidgets. // This is how long it takes to discover all widgets on disc (from python environment). /* __GDPR__ @@ -1454,7 +1465,7 @@ ] } */ -// (112). Telemetry.HashedIPyWidgetScriptDiscoveryError (DS_INTERNAL.IPYWIDGET_DISCOVERY_ERRORED) +// (113). Telemetry.HashedIPyWidgetScriptDiscoveryError (DS_INTERNAL.IPYWIDGET_DISCOVERY_ERRORED) // Something went wrong in looking for a widget. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_DISCOVERY_ERRORED" : { @@ -1464,7 +1475,7 @@ ] } */ -// (113). Telemetry.IPyWidgetExtensionJsInfo (DS_INTERNAL.IPYWIDGET_EXTENSIONJS_INFO) +// (114). Telemetry.IPyWidgetExtensionJsInfo (DS_INTERNAL.IPYWIDGET_EXTENSIONJS_INFO) // Telemetry event sent once we've successfully or unsuccessfully parsed the extension.js file in the widget folder. // E.g. if we have a widget named ipyvolume, we attempt to parse the nbextensions/ipyvolume/extension.js file to get some info out of it. /* __GDPR__ @@ -1479,7 +1490,7 @@ ] } */ -// (114). Telemetry.IPyWidgetLoadFailure (DS_INTERNAL.IPYWIDGET_LOAD_FAILURE) +// (115). Telemetry.IPyWidgetLoadFailure (DS_INTERNAL.IPYWIDGET_LOAD_FAILURE) // Telemetry event sent when an ipywidget module fails to load. Module name is hashed. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_LOAD_FAILURE" : { @@ -1493,7 +1504,7 @@ ] } */ -// (115). Telemetry.IPyWidgetLoadSuccess (DS_INTERNAL.IPYWIDGET_LOAD_SUCCESS) +// (116). Telemetry.IPyWidgetLoadSuccess (DS_INTERNAL.IPYWIDGET_LOAD_SUCCESS) // Telemetry event sent when an ipywidget module loads. Module name is hashed. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_LOAD_SUCCESS" : { @@ -1505,7 +1516,7 @@ ] } */ -// (116). Telemetry.IPyWidgetPromptToUseCDN (DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN) +// (117). Telemetry.IPyWidgetPromptToUseCDN (DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN) // Telemetry sent when we prompt user to use a CDN for IPyWidget scripts. // This is always sent when we display a prompt. /* __GDPR__ @@ -1516,7 +1527,7 @@ ] } */ -// (117). Telemetry.IPyWidgetPromptToUseCDNSelection (DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION) +// (118). Telemetry.IPyWidgetPromptToUseCDNSelection (DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION) // Telemetry sent when user does something with the prompt displayed to user about using CDN for IPyWidget scripts. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_PROMPT_TO_USE_CDN_SELECTION" : { @@ -1527,7 +1538,7 @@ ] } */ -// (118). Telemetry.IPyWidgetRenderFailure (DS_INTERNAL.IPYWIDGET_RENDER_FAILURE) +// (119). Telemetry.IPyWidgetRenderFailure (DS_INTERNAL.IPYWIDGET_RENDER_FAILURE) // Telemetry event sent when the widget render function fails (note, this may not be sufficient to capture all failures). /* __GDPR__ "DS_INTERNAL.IPYWIDGET_RENDER_FAILURE" : { @@ -1537,7 +1548,7 @@ ] } */ -// (119). Telemetry.IPyWidgetNbExtensionCopyTime (DS_INTERNAL.IPYWIDGET_TIME_TO_COPY_NBEXTENSIONS_DIR) +// (120). Telemetry.IPyWidgetNbExtensionCopyTime (DS_INTERNAL.IPYWIDGET_TIME_TO_COPY_NBEXTENSIONS_DIR) // Total time take to copy the nb extensions folder. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_TIME_TO_COPY_NBEXTENSIONS_DIR" : { @@ -1547,7 +1558,7 @@ ] } */ -// (120). Telemetry.IPyWidgetUnhandledMessage (DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE) +// (121). Telemetry.IPyWidgetUnhandledMessage (DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE) // Telemetry event sent when the widget tries to send a kernel message but nothing was listening /* __GDPR__ "DS_INTERNAL.IPYWIDGET_UNHANDLED_MESSAGE" : { @@ -1558,7 +1569,7 @@ ] } */ -// (121). Telemetry.HashedIPyWidgetNameUsed (DS_INTERNAL.IPYWIDGET_USED_BY_USER) +// (122). Telemetry.HashedIPyWidgetNameUsed (DS_INTERNAL.IPYWIDGET_USED_BY_USER) // Telemetry event sent with name of a Widget that is used. // Helps determine which widgets are used the most, and which are not. // Useful in prioritizing which widgets to work on if things fail to work. @@ -1576,7 +1587,7 @@ ] } */ -// (122). Telemetry.IPyWidgetWidgetVersionNotSupportedLoadFailure (DS_INTERNAL.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED_LOAD_FAILURE) +// (123). Telemetry.IPyWidgetWidgetVersionNotSupportedLoadFailure (DS_INTERNAL.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED_LOAD_FAILURE) // Telemetry event sent when an ipywidget version that is not supported is used & we have trapped this and warned the user abou it. /* __GDPR__ "DS_INTERNAL.IPYWIDGET_WIDGET_VERSION_NOT_SUPPORTED_LOAD_FAILURE" : { @@ -1588,7 +1599,7 @@ ] } */ -// (123). Telemetry.CheckPasswordJupyterHub (DS_INTERNAL.JUPYTER_HUB_PASSWORD) +// (124). Telemetry.CheckPasswordJupyterHub (DS_INTERNAL.JUPYTER_HUB_PASSWORD) // Sent when checking for passwords for Jupyter Hub /* __GDPR__ "DS_INTERNAL.JUPYTER_HUB_PASSWORD" : { @@ -1599,7 +1610,7 @@ ] } */ -// (124). Telemetry.KernelSpec (DS_INTERNAL.JUPYTER_KERNEL_SPEC) +// (125). Telemetry.KernelSpec (DS_INTERNAL.JUPYTER_KERNEL_SPEC) // Information about KernelSpecs /* __GDPR__ "DS_INTERNAL.JUPYTER_KERNEL_SPEC" : { @@ -1620,7 +1631,7 @@ ] } */ -// (125). Telemetry.ZMQSupport (DS_INTERNAL.JUPYTER_ZMQ_SUPPORT) +// (126). Telemetry.ZMQSupport (DS_INTERNAL.JUPYTER_ZMQ_SUPPORT) // Information used to determine the zmq binary support. // the alpine, libc, armv version is used by the node module /* __GDPR__ @@ -1638,7 +1649,7 @@ ] } */ -// (126). Telemetry.ZMQSupportFailure (DS_INTERNAL.JUPYTER_ZMQ_SUPPORT_FAILURE) +// (127). Telemetry.ZMQSupportFailure (DS_INTERNAL.JUPYTER_ZMQ_SUPPORT_FAILURE) // Information used to determine the zmq binary support. // the alpine, libc, armv version is used by the node module /* __GDPR__ @@ -1659,7 +1670,7 @@ ] } */ -// (127). Telemetry.KernelCount (DS_INTERNAL.KERNEL_COUNT) +// (128). Telemetry.KernelCount (DS_INTERNAL.KERNEL_COUNT) // Telemetry sent with the total number of different types of kernels in the kernel picker. /* __GDPR__ "DS_INTERNAL.KERNEL_COUNT" : { @@ -1674,7 +1685,7 @@ ] } */ -// (128). Telemetry.KernelLauncherPerf (DS_INTERNAL.KERNEL_LAUNCHER_PERF) +// (129). Telemetry.KernelLauncherPerf (DS_INTERNAL.KERNEL_LAUNCHER_PERF) // Total time taken to Launch a raw kernel. /* __GDPR__ "DS_INTERNAL.KERNEL_LAUNCHER_PERF" : { @@ -1685,7 +1696,7 @@ ] } */ -// (129). Telemetry.NativeVariableViewLoaded (DS_INTERNAL.NATIVE_VARIABLE_VIEW_LOADED) +// (130). Telemetry.NativeVariableViewLoaded (DS_INTERNAL.NATIVE_VARIABLE_VIEW_LOADED) // The Variable View webview was loaded. /* __GDPR__ "DS_INTERNAL.NATIVE_VARIABLE_VIEW_LOADED" : { @@ -1695,7 +1706,7 @@ ] } */ -// (130). Telemetry.NativeVariableViewMadeVisible (DS_INTERNAL.NATIVE_VARIABLE_VIEW_MADE_VISIBLE) +// (131). Telemetry.NativeVariableViewMadeVisible (DS_INTERNAL.NATIVE_VARIABLE_VIEW_MADE_VISIBLE) // The Variable View webview was made visible. /* __GDPR__ "DS_INTERNAL.NATIVE_VARIABLE_VIEW_MADE_VISIBLE" : { @@ -1705,7 +1716,7 @@ ] } */ -// (131). Telemetry.NewFileForInteractiveWindow (DS_INTERNAL.NEW_FILE_USED_IN_INTERACTIVE) +// (132). Telemetry.NewFileForInteractiveWindow (DS_INTERNAL.NEW_FILE_USED_IN_INTERACTIVE) // Telemetry event sent when a user runs the interactive window with a new file /* __GDPR__ "DS_INTERNAL.NEW_FILE_USED_IN_INTERACTIVE" : { @@ -1715,7 +1726,7 @@ ] } */ -// (132). Telemetry.PerceivedJupyterStartupNotebook (DS_INTERNAL.PERCEIVED_JUPYTER_STARTUP_NOTEBOOK) +// (133). Telemetry.PerceivedJupyterStartupNotebook (DS_INTERNAL.PERCEIVED_JUPYTER_STARTUP_NOTEBOOK) // Time take for jupyter server to start and be ready to run first user cell. // (Note: The property `notebook` only gets sent correctly in Jupyter version 2022.8.0 or later) /* __GDPR__ @@ -1743,7 +1754,7 @@ ] } */ -// (133). Telemetry.PreferredKernelExactMatch (DS_INTERNAL.PREFERRED_KERNEL_EXACT_MATCH) +// (134). Telemetry.PreferredKernelExactMatch (DS_INTERNAL.PREFERRED_KERNEL_EXACT_MATCH) // Send we we complete our preferred kernel match. Matched reason might be 'no match'. /* __GDPR__ "DS_INTERNAL.PREFERRED_KERNEL_EXACT_MATCH" : { @@ -1754,7 +1765,7 @@ ] } */ -// (134). Telemetry.PythonExtensionInstalledViaKernelPicker (DS_INTERNAL.PYTHON_EXTENSION_INSTALLED_VIA_KERNEL_PICKER) +// (135). Telemetry.PythonExtensionInstalledViaKernelPicker (DS_INTERNAL.PYTHON_EXTENSION_INSTALLED_VIA_KERNEL_PICKER) // Python extension was attempted to be installed via the kernel picker command. /* __GDPR__ "DS_INTERNAL.PYTHON_EXTENSION_INSTALLED_VIA_KERNEL_PICKER" : { @@ -1765,7 +1776,7 @@ ] } */ -// (135). Telemetry.PythonExtensionNotInstalled (DS_INTERNAL.PYTHON_EXTENSION_NOT_INSTALLED) +// (136). Telemetry.PythonExtensionNotInstalled (DS_INTERNAL.PYTHON_EXTENSION_NOT_INSTALLED) // The kernel picker command to install python extension was shown. /* __GDPR__ "DS_INTERNAL.PYTHON_EXTENSION_NOT_INSTALLED" : { @@ -1776,7 +1787,7 @@ ] } */ -// (136). Telemetry.PythonModuleInstall (DS_INTERNAL.PYTHON_MODULE_INSTALL) +// (137). Telemetry.PythonModuleInstall (DS_INTERNAL.PYTHON_MODULE_INSTALL) // Telemetry sent when user is presented with a dialog to install a python package. // Also sent with the user's response to the dialog. /* __GDPR__ @@ -1793,7 +1804,7 @@ ] } */ -// (137). Telemetry.PythonNotInstalled (DS_INTERNAL.PYTHON_NOT_INSTALLED) +// (138). Telemetry.PythonNotInstalled (DS_INTERNAL.PYTHON_NOT_INSTALLED) // The kernel picker command to install python was shown. /* __GDPR__ "DS_INTERNAL.PYTHON_NOT_INSTALLED" : { @@ -1804,7 +1815,7 @@ ] } */ -// (138). Telemetry.RawKernelInfoResponse (DS_INTERNAL.RAWKERNEL_INFO_RESPONSE) +// (139). Telemetry.RawKernelInfoResponse (DS_INTERNAL.RAWKERNEL_INFO_RESPONSE) // After starting a kernel we send a request to get the kernel info. // This tracks the total time taken to get the response back (or wether we timedout). // If we timeout and later we find successful comms for this session, then timeout is too low @@ -1836,7 +1847,7 @@ ] } */ -// (139). Telemetry.RawKernelProcessLaunch (DS_INTERNAL.RAWKERNEL_PROCESS_LAUNCH) +// (140). Telemetry.RawKernelProcessLaunch (DS_INTERNAL.RAWKERNEL_PROCESS_LAUNCH) // Sent to measure time taken to spawn the raw kernel process. /* __GDPR__ "DS_INTERNAL.RAWKERNEL_PROCESS_LAUNCH" : { @@ -1846,7 +1857,7 @@ ] } */ -// (140). Telemetry.RawKernelSessionDisposed (DS_INTERNAL.RAWKERNEL_SESSION_DISPOSED) +// (141). Telemetry.RawKernelSessionDisposed (DS_INTERNAL.RAWKERNEL_SESSION_DISPOSED) // This event is sent when a RawSession's `dispose` method is called. // Used to determine what part of the code that shut down the session, so as to determine when and how the kernel session crashed. /* __GDPR__ @@ -1875,7 +1886,7 @@ ] } */ -// (141). Telemetry.RawKernelSessionKernelProcessExited (DS_INTERNAL.RAWKERNEL_SESSION_KERNEL_PROCESS_EXITED) +// (142). Telemetry.RawKernelSessionKernelProcessExited (DS_INTERNAL.RAWKERNEL_SESSION_KERNEL_PROCESS_EXITED) // This event is sent when the underlying kernelProcess for a // RawJupyterSession exits. /* __GDPR__ @@ -1905,7 +1916,7 @@ ] } */ -// (142). Telemetry.RawKernelSessionStartNoIpykernel (DS_INTERNAL.RAWKERNEL_SESSION_NO_IPYKERNEL) +// (143). Telemetry.RawKernelSessionStartNoIpykernel (DS_INTERNAL.RAWKERNEL_SESSION_NO_IPYKERNEL) // Telemetry event sent when raw kernel startup fails due to missing ipykernel dependency. // This is useful to see what the user does with this error message. /* __GDPR__ @@ -1934,7 +1945,7 @@ ] } */ -// (143). Telemetry.RawKernelSessionShutdown (DS_INTERNAL.RAWKERNEL_SESSION_SHUTDOWN) +// (144). Telemetry.RawKernelSessionShutdown (DS_INTERNAL.RAWKERNEL_SESSION_SHUTDOWN) // This event is sent when a RawJupyterSession's `shutdownSession` method is called. // Used to determine what part of the code that shut down the session, so as to determine when and how the kernel session crashed. /* __GDPR__ @@ -1964,7 +1975,7 @@ ] } */ -// (144). Telemetry.RunTest (DS_INTERNAL.RUNTEST) +// (145). Telemetry.RunTest (DS_INTERNAL.RUNTEST) // A automated test has been run /* __GDPR__ "DS_INTERNAL.RUNTEST" : { @@ -1979,7 +1990,7 @@ ] } */ -// (145). Telemetry.ShiftEnterBannerShown (DS_INTERNAL.SHIFTENTER_BANNER_SHOWN) +// (146). Telemetry.ShiftEnterBannerShown (DS_INTERNAL.SHIFTENTER_BANNER_SHOWN) // Information banner displayed to give the user the option to configure shift+enter for the Interactive Window. /* __GDPR__ "DS_INTERNAL.SHIFTENTER_BANNER_SHOWN" : { @@ -1989,7 +2000,7 @@ ] } */ -// (146). Telemetry.PandasNotInstalled (DS_INTERNAL.SHOW_DATA_NO_PANDAS) +// (147). Telemetry.PandasNotInstalled (DS_INTERNAL.SHOW_DATA_NO_PANDAS) // User tried to open the data viewer and Pandas package was not installed. // Note: Not a failure state, as we prompt for install after this. /* __GDPR__ @@ -2000,7 +2011,7 @@ ] } */ -// (147). Telemetry.PandasInstallCanceled (DS_INTERNAL.SHOW_DATA_PANDAS_INSTALL_CANCELED) +// (148). Telemetry.PandasInstallCanceled (DS_INTERNAL.SHOW_DATA_PANDAS_INSTALL_CANCELED) // When opening the data viewer the user was prompted to install / upgrade // pandas and choose to cancel the operation. /* __GDPR__ @@ -2011,7 +2022,7 @@ ] } */ -// (148). Telemetry.PandasOK (DS_INTERNAL.SHOW_DATA_PANDAS_OK) +// (149). Telemetry.PandasOK (DS_INTERNAL.SHOW_DATA_PANDAS_OK) // When opening the data viewer the version of Pandas installed was ok. /* __GDPR__ "DS_INTERNAL.SHOW_DATA_PANDAS_OK" : { @@ -2021,7 +2032,7 @@ ] } */ -// (149). Telemetry.PandasTooOld (DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD) +// (150). Telemetry.PandasTooOld (DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD) // When opening the data viewer the version of Pandas installed was too old. /* __GDPR__ "DS_INTERNAL.SHOW_DATA_PANDAS_TOO_OLD" : { @@ -2031,7 +2042,7 @@ ] } */ -// (150). Telemetry.SwitchKernel (DS_INTERNAL.SWITCH_KERNEL) +// (151). Telemetry.SwitchKernel (DS_INTERNAL.SWITCH_KERNEL) // Triggered when the kernel selection changes (note: This can also happen automatically when a notebook is opened). // WARNING: Due to changes in VS Code, this isn't necessarily a user action, hence difficult to tell if the user changed it or it changed automatically. /* __GDPR__ @@ -2059,7 +2070,7 @@ ] } */ -// (151). Telemetry.VariableExplorerFetchTime (DS_INTERNAL.VARIABLE_EXPLORER_FETCH_TIME) +// (152). Telemetry.VariableExplorerFetchTime (DS_INTERNAL.VARIABLE_EXPLORER_FETCH_TIME) // How long did it take for a single variable request to be resolved. /* __GDPR__ "DS_INTERNAL.VARIABLE_EXPLORER_FETCH_TIME" : { @@ -2069,7 +2080,7 @@ ] } */ -// (152). Telemetry.VariableExplorerVariableCount (DS_INTERNAL.VARIABLE_EXPLORER_VARIABLE_COUNT) +// (153). Telemetry.VariableExplorerVariableCount (DS_INTERNAL.VARIABLE_EXPLORER_VARIABLE_COUNT) // Count how many variables were in a variable request. /* __GDPR__ "DS_INTERNAL.VARIABLE_EXPLORER_VARIABLE_COUNT" : { @@ -2080,7 +2091,7 @@ ] } */ -// (153). Telemetry.VSCNotebookCellTranslationFailed (DS_INTERNAL.VSCNOTEBOOK_CELL_TRANSLATION_FAILED) +// (154). Telemetry.VSCNotebookCellTranslationFailed (DS_INTERNAL.VSCNOTEBOOK_CELL_TRANSLATION_FAILED) // We've failed to translate a Jupyter cell output for serialization into a Notebook cell. /* __GDPR__ "DS_INTERNAL.VSCNOTEBOOK_CELL_TRANSLATION_FAILED" : { @@ -2091,7 +2102,7 @@ ] } */ -// (154). EventName.ENVFILE_VARIABLE_SUBSTITUTION (ENVFILE_VARIABLE_SUBSTITUTION) +// (155). EventName.ENVFILE_VARIABLE_SUBSTITUTION (ENVFILE_VARIABLE_SUBSTITUTION) // Telemetry event sent when substituting Environment variables to calculate value of variables. // E.g. user has a a .env file with tokens that need to be replaced with env variables. // such as an env file having the variable `${HOME}`. @@ -2104,7 +2115,7 @@ ] } */ -// (155). EventName.ENVFILE_WORKSPACE (ENVFILE_WORKSPACE) +// (156). EventName.ENVFILE_WORKSPACE (ENVFILE_WORKSPACE) // Telemetry event sent when an environment file is detected in the workspace. /* __GDPR__ "ENVFILE_WORKSPACE" : { @@ -2114,7 +2125,7 @@ ] } */ -// (156). EventName.EXTENSION_LOAD (EXTENSION.LOAD) +// (157). EventName.EXTENSION_LOAD (EXTENSION.LOAD) // Telemetry event sent with perf measures related to activation and loading of extension. /* __GDPR__ "EXTENSION.LOAD" : { @@ -2127,7 +2138,7 @@ ] } */ -// (157). Telemetry.AmbiguousGlobalKernelSpec (GLOBAL_PYTHON_KERNELSPEC) +// (158). Telemetry.AmbiguousGlobalKernelSpec (GLOBAL_PYTHON_KERNELSPEC) // We have a Python Kernel spec without fully qualified path of Python env. // We have no idea how to start these kernels if the user has more than one Python env. // @@ -2151,7 +2162,7 @@ ] } */ -// (158). EventName.HASHED_PACKAGE_NAME (HASHED_PACKAGE_NAME) +// (159). EventName.HASHED_PACKAGE_NAME (HASHED_PACKAGE_NAME) // Telemetry event sent with hash of an imported python package. // Used to detect the popularity of a package, that would help determine which packages // need to be prioritized when resolving issues with intellisense or supporting similar issues related to a (known) specific package. @@ -2166,7 +2177,7 @@ ] } */ -// (159). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_ERROR (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_ERROR_EX) +// (160). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_ERROR (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_ERROR_EX) // Telemetry event sent when user opens the data viewer via the variable view and there is an error in doing so. /* __GDPR__ "OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_ERROR_EX" : { @@ -2176,7 +2187,7 @@ ] } */ -// (160). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_REQUEST (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_REQUEST_EX) +// (161). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_REQUEST (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_REQUEST_EX) // Telemetry event sent when user opens the data viewer via the variable view. /* __GDPR__ "OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_REQUEST_EX" : { @@ -2186,7 +2197,7 @@ ] } */ -// (161). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_SUCCESS (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_SUCCESS_EX) +// (162). EventName.OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_SUCCESS (OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_SUCCESS_EX) // Telemetry event sent when user opens the data viewer via the variable view and we successfully open the view. /* __GDPR__ "OPEN_DATAVIEWER_FROM_VARIABLE_WINDOW_SUCCESS_EX" : { @@ -2196,7 +2207,7 @@ ] } */ -// (162). Telemetry.PlotViewerWebviewLoaded (PLOTVIEWER.WEBVIEW_LOADED) +// (163). Telemetry.PlotViewerWebviewLoaded (PLOTVIEWER.WEBVIEW_LOADED) // The Plot Viewer webview was loaded. /* __GDPR__ "PLOTVIEWER.WEBVIEW_LOADED" : { diff --git a/src/kernels/kernel.ts b/src/kernels/kernel.ts index a63893ad1fb..7afc6f00ddd 100644 --- a/src/kernels/kernel.ts +++ b/src/kernels/kernel.ts @@ -15,7 +15,8 @@ import { Memento, CancellationError, window, - workspace + workspace, + CancellationToken } from 'vscode'; import { CodeSnippets, @@ -66,6 +67,7 @@ import { KernelInterruptTimeoutError } from './errors/kernelInterruptTimeoutErro import { dispose } from '../platform/common/utils/lifecycle'; import { getCachedVersion, getEnvironmentType } from '../platform/interpreter/helpers'; import { getNotebookTelemetryTracker } from './telemetry/notebookTelemetry'; +import { AsyncEmitter } from '../platform/common/utils/events'; const widgetVersionOutPrefix = 'e976ee50-99ed-4aba-9b6b-9dcd5634d07d:IPyWidgets:'; /** @@ -129,7 +131,7 @@ abstract class BaseKernel implements IBaseKernel { get onStarted(): Event { return this._onStarted.event; } - get onPostInitialized(): Event { + get onPostInitialized(): Event<{ token: CancellationToken; waitUntil(thenable: Thenable): void }> { return this._onPostInitialized.event; } get onDisposed(): Event { @@ -178,12 +180,15 @@ abstract class BaseKernel implements IBaseKernel { private _disposed?: boolean; private _disposing?: boolean; private _ignoreJupyterSessionDisposedErrors?: boolean; - private _postInitializedOnStart?: boolean; + private _postInitializedOnStartPromise?: Promise; private readonly _onDidKernelSocketChange = new EventEmitter(); private readonly _onStatusChanged = new EventEmitter(); private readonly _onRestarted = new EventEmitter(); private readonly _onStarted = new EventEmitter(); - private readonly _onPostInitialized = new EventEmitter(); + private readonly _onPostInitialized = new AsyncEmitter<{ + waitUntil: (thenable: Thenable) => void; + token: CancellationToken; + }>(); private readonly _onDisposed = new EventEmitter(); private _jupyterSessionPromise?: Promise; private readonly hookedSessionForEvents = new WeakSet(); @@ -254,16 +259,9 @@ abstract class BaseKernel implements IBaseKernel { this.startCancellation.dispose(); this.startCancellation = new CancellationTokenSource(); } - return this.startJupyterSession(options).then((result) => { - // If we started and the UI is no longer disabled (ie., a user executed a cell) - // then we can signal that the kernel was created and can be used by third-party extensions. - // We also only want to fire off a single event here. - if (!options?.disableUI && !this._postInitializedOnStart) { - this._onPostInitialized.fire(); - this._postInitializedOnStart = true; - } - return result; - }); + const result = await this.startJupyterSession(options); + await this.triggerOnDidStartKernel(options?.disableUI === true, false); + return result; } /** * Interrupts the execution of cells. @@ -335,6 +333,7 @@ abstract class BaseKernel implements IBaseKernel { ? await this._jupyterSessionPromise.catch(() => undefined) : undefined; this._jupyterSessionPromise = undefined; + this._postInitializedOnStartPromise = undefined; if (this._session) { promises.push(disposeAsync(this._session, this.disposables)); this._session = undefined; @@ -404,6 +403,7 @@ abstract class BaseKernel implements IBaseKernel { const session = this._session; this._session = undefined; this._jupyterSessionPromise = undefined; + this._postInitializedOnStartPromise = undefined; // If we get a kernel promise failure, then restarting timed out. Just shutdown and restart the entire server. // Note, this code might not be necessary, as such an error is thrown only when interrupting a kernel times out. sendKernelTelemetryEvent( @@ -428,7 +428,7 @@ abstract class BaseKernel implements IBaseKernel { this._onRestarted.fire(); // Also signal that the kernel post initialization completed. - this._onPostInitialized.fire(); + await this.triggerOnDidStartKernel(false, true); } catch (ex) { logger.error(`Failed to restart kernel ${getDisplayPath(this.uri)}`, ex); throw ex; @@ -487,6 +487,26 @@ abstract class BaseKernel implements IBaseKernel { return this._jupyterSessionPromise; } + private async triggerOnDidStartKernel(disableUI: boolean, isRestarting = false) { + if (disableUI) { + return; + } + + // If we started and the UI is no longer disabled (ie., a user executed a cell) + // then we can signal that the kernel was created and can be used by third-party extensions. + // We also only want to fire off a single event here. + if (!this._postInitializedOnStartPromise || isRestarting) { + this._postInitializedOnStartPromise = this._onPostInitialized.fireAsync({}, this.startCancellation.token); + await this._postInitializedOnStartPromise; + } + + // Do not enable this, end up in a dead lock. + // E.g. extension `A` waits for `onDidStart` event, and then calls `executeCode` API. + // However the `executeCode` API waits for `onDidStart` event to be fired and completed, but thats still waiting for ext `A` to complete. + // Hence a deadlock. + // await this._postInitializedOnStartPromise; + } + private async interruptExecution( session: IKernelSession, pendingExecutions: Promise @@ -737,6 +757,7 @@ abstract class BaseKernel implements IBaseKernel { ); const isActiveSessionDead = this._session === session; this._jupyterSessionPromise = undefined; + this._postInitializedOnStartPromise = undefined; this._session = undefined; // If the active session died, then kernel is dead. diff --git a/src/kernels/kernelExecution.ts b/src/kernels/kernelExecution.ts index 2c27fee043c..7e32b15dcc1 100644 --- a/src/kernels/kernelExecution.ts +++ b/src/kernels/kernelExecution.ts @@ -166,6 +166,11 @@ export class NotebookKernelExecution implements INotebookKernelExecution { const sessionPromise = this.kernel.restarting.then(() => this.kernel.start(new DisplayOptions(false))); traceCellMessage(cell, `NotebookKernelExecution.executeCell (3), ${getDisplayPath(cell.notebook.uri)}`); + + // Wait for the kernel to complete post initialization before queueing the cell in case + // we need to allow extensions to run code before the initial user-triggered execution + // (because of this, we intentionally do not need the same await in `executeCode`). + await sessionPromise; const executionQueue = this.getOrCreateCellExecutionQueue(cell.notebook, sessionPromise); executionQueue.queueCell(cell, codeOverride); let success = true; @@ -196,18 +201,28 @@ export class NotebookKernelExecution implements INotebookKernelExecution { token: CancellationToken ): AsyncGenerator { const stopWatch = new StopWatch(); - // If we're restarting, wait for it to finish - const sessionPromise = this.kernel.restarting.then(() => this.kernel.start(new DisplayOptions(false))); - - const executionQueue = this.getOrCreateCellExecutionQueue(this.notebook, sessionPromise); let result: ICodeExecution; if (extensionId === JVSC_EXTENSION_ID || extensionId === POWER_TOYS_EXTENSION_ID) { + // If we're restarting, wait for it to finish + // For internal extension we do not care about the UI being displayed. + // This is also a way to bypass the queue. + // If disable UI is true, then we end up triggering post execution code that results in 3rd party extensions + // Getting the event for kernel starting, when in fact users didn't run any code against the kernel at all. + // Also in those cases its possible 3rd party extensions would handle that event and then run some initialization code + // which in turn comes back into this and ther's a new item in the queue, but that is blocked on the previous execution. + // As a result we end up in a deadlock. + const sessionPromise = this.kernel.restarting.then(() => this.kernel.start(new DisplayOptions(true))); + // No need to queue code execution for JVSC, as it will be executed immediately. // Only 3rd party code needs to be queued (as we need to give user code preference over 3rd party ext code) result = CodeExecution.fromCode(code, extensionId); void sessionPromise.then((session) => result.start(session)); } else { + // If we're restarting, wait for it to finish + const sessionPromise = this.kernel.restarting.then(() => this.kernel.start(new DisplayOptions(false))); + + const executionQueue = this.getOrCreateCellExecutionQueue(this.notebook, sessionPromise); result = executionQueue.queueCode(code, extensionId, token); } if (extensionId !== JVSC_EXTENSION_ID) { diff --git a/src/kernels/kernelProvider.base.ts b/src/kernels/kernelProvider.base.ts index f3dd4609bc3..38a6415752c 100644 --- a/src/kernels/kernelProvider.base.ts +++ b/src/kernels/kernelProvider.base.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import type { KernelMessage } from '@jupyterlab/services'; -import { Event, EventEmitter, NotebookDocument, Uri, workspace } from 'vscode'; +import { CancellationToken, Event, EventEmitter, NotebookDocument, Uri, workspace } from 'vscode'; import { logger } from '../platform/logging'; import { getDisplayPath } from '../platform/common/platform/fs-paths'; import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposableRegistry } from '../platform/common/types'; @@ -17,6 +17,7 @@ import { INotebookKernelExecution } from './types'; import { JupyterServerProviderHandle } from './jupyter/types'; +import { AsyncEmitter } from '../platform/common/utils/events'; /** * Provides kernels to the system. Generally backed by a URI or a notebook object. @@ -36,7 +37,11 @@ export abstract class BaseCoreKernelProvider implements IKernelProvider { protected readonly _onDidCreateKernel = new EventEmitter(); protected readonly _onDidDisposeKernel = new EventEmitter(); protected readonly _onKernelStatusChanged = new EventEmitter<{ status: KernelMessage.Status; kernel: IKernel }>(); - protected readonly _onDidPostInitializeKernel = new EventEmitter(); + protected readonly _onDidPostInitializeKernel = new AsyncEmitter<{ + kernel: IKernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>(); public readonly onKernelStatusChanged = this._onKernelStatusChanged.event; public get kernels() { const kernels = new Set(); @@ -75,7 +80,11 @@ export abstract class BaseCoreKernelProvider implements IKernelProvider { public get onDidCreateKernel(): Event { return this._onDidCreateKernel.event; } - public get onDidPostInitializeKernel(): Event { + public get onDidPostInitializeKernel(): Event<{ + kernel: IKernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }> { return this._onDidPostInitializeKernel.event; } public get(uriOrNotebook: Uri | NotebookDocument | string): IKernel | undefined { @@ -192,7 +201,11 @@ export abstract class BaseThirdPartyKernelProvider implements IThirdPartyKernelP protected readonly _onDidStartKernel = new EventEmitter(); protected readonly _onDidCreateKernel = new EventEmitter(); protected readonly _onDidDisposeKernel = new EventEmitter(); - protected readonly _onDidPostInitializeKernel = new EventEmitter(); + protected readonly _onDidPostInitializeKernel = new AsyncEmitter<{ + kernel: IThirdPartyKernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>(); protected readonly _onKernelStatusChanged = new EventEmitter<{ status: KernelMessage.Status; kernel: IThirdPartyKernel; @@ -234,7 +247,11 @@ export abstract class BaseThirdPartyKernelProvider implements IThirdPartyKernelP public get onDidCreateKernel(): Event { return this._onDidCreateKernel.event; } - public get onDidPostInitializeKernel(): Event { + public get onDidPostInitializeKernel(): Event<{ + kernel: IThirdPartyKernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }> { return this._onDidPostInitializeKernel.event; } public get(uri: Uri | string): IThirdPartyKernel | undefined { diff --git a/src/kernels/kernelProvider.node.ts b/src/kernels/kernelProvider.node.ts index 158b4b59593..b8442971e25 100644 --- a/src/kernels/kernelProvider.node.ts +++ b/src/kernels/kernelProvider.node.ts @@ -98,9 +98,7 @@ export class KernelProvider extends BaseCoreKernelProvider { this.disposables ); kernel.onPostInitialized( - () => { - this._onDidPostInitializeKernel.fire(kernel); - }, + (e) => e.waitUntil(this._onDidPostInitializeKernel.fireAsync({ kernel }, e.token)), this, this.disposables ); @@ -160,9 +158,7 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { this.disposables ); kernel.onPostInitialized( - () => { - this._onDidPostInitializeKernel.fire(kernel); - }, + (e) => e.waitUntil(this._onDidPostInitializeKernel.fireAsync({ kernel }, e.token)), this, this.disposables ); diff --git a/src/kernels/kernelProvider.node.unit.test.ts b/src/kernels/kernelProvider.node.unit.test.ts index f5ba56cb83d..541c1aac7f3 100644 --- a/src/kernels/kernelProvider.node.unit.test.ts +++ b/src/kernels/kernelProvider.node.unit.test.ts @@ -5,7 +5,15 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { EventEmitter, Memento, NotebookController, NotebookDocument, Uri } from 'vscode'; +import { + CancellationToken, + CancellationTokenSource, + EventEmitter, + Memento, + NotebookController, + NotebookDocument, + Uri +} from 'vscode'; import { IConfigurationService, IDisposable, @@ -33,6 +41,7 @@ import { JupyterNotebookView } from '../platform/common/constants'; import { mockedVSCodeNamespaces } from '../test/vscode-mock'; import { CellOutputDisplayIdTracker } from './execution/cellDisplayIdTracker'; import { IReplNotebookTrackerService } from '../platform/notebooks/replNotebookTrackerService'; +import { AsyncEmitter } from '../platform/common/utils/events'; suite('Jupyter Session', () => { suite('Node Kernel Provider', function () { @@ -95,7 +104,7 @@ suite('Jupyter Session', () => { await Promise.all(asyncDisposables.map((item) => item.dispose().catch(noop))); asyncDisposables.length = 0; }); - function testKernelProviderEvents(thirdPartyKernelProvider = false) { + async function testKernelProviderEvents(thirdPartyKernelProvider = false) { const kernelProvider = thirdPartyKernelProvider ? create3rdPartyKernelProvider() : createKernelProvider(); const kernelCreated = createEventHandler(kernelProvider, 'onDidCreateKernel', disposables); const kernelStarted = createEventHandler(kernelProvider, 'onDidStartKernel', disposables); @@ -107,7 +116,10 @@ suite('Jupyter Session', () => { const onStarted = new EventEmitter(); const onStatusChanged = new EventEmitter(); const onRestartedEvent = new EventEmitter(); - const onPostInitializedEvent = new EventEmitter(); + const onPostInitializedEvent = new AsyncEmitter<{ + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>(); const onDisposedEvent = new EventEmitter(); disposables.push(onStatusChanged); disposables.push(onRestartedEvent); @@ -155,13 +167,13 @@ suite('Jupyter Session', () => { assert.isTrue(kernelStatusChanged.fired, 'IKernelProvider.onKernelStatusChanged not fired'); onRestartedEvent.fire(); assert.isTrue(kernelRestarted.fired, 'IKernelProvider.onKernelRestarted not fired'); - onPostInitializedEvent.fire(); + await onPostInitializedEvent.fireAsync({}, new CancellationTokenSource().token); assert.isTrue(kernelPostInitialized.fired, 'IKernelProvider.onDidPostInitializeKernel not fired'); onDisposedEvent.fire(); assert.isTrue(kernelDisposed.fired, 'IKernelProvider.onDisposedEvent not fired'); } - test('Kernel Events', () => testKernelProviderEvents(false)); - test('3rd Party Kernel Events', () => testKernelProviderEvents(true)); + test('Kernel Events', async () => await testKernelProviderEvents(false)); + test('3rd Party Kernel Events', async () => await testKernelProviderEvents(true)); }); suite('KernelProvider Node', () => { diff --git a/src/kernels/kernelProvider.web.ts b/src/kernels/kernelProvider.web.ts index 6a65b3f8c00..d6fdbce0433 100644 --- a/src/kernels/kernelProvider.web.ts +++ b/src/kernels/kernelProvider.web.ts @@ -80,7 +80,11 @@ export class KernelProvider extends BaseCoreKernelProvider { this.workspaceStorage ) as IKernel; kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); - kernel.onPostInitialized(() => this._onDidPostInitializeKernel.fire(kernel), this, this.disposables); + kernel.onPostInitialized( + (e) => e.waitUntil(this._onDidPostInitializeKernel.fireAsync({ kernel }, e.token)), + this, + this.disposables + ); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); kernel.onStarted(() => this._onDidStartKernel.fire(kernel), this, this.disposables); kernel.onStatusChanged( @@ -133,7 +137,11 @@ export class ThirdPartyKernelProvider extends BaseThirdPartyKernelProvider { this.workspaceStorage ); kernel.onRestarted(() => this._onDidRestartKernel.fire(kernel), this, this.disposables); - kernel.onPostInitialized(() => this._onDidPostInitializeKernel.fire(kernel), this, this.disposables); + kernel.onPostInitialized( + (e) => e.waitUntil(this._onDidPostInitializeKernel.fireAsync({ kernel }, e.token)), + this, + this.disposables + ); kernel.onDisposed(() => this._onDidDisposeKernel.fire(kernel), this, this.disposables); kernel.onStarted(() => this._onDidStartKernel.fire(kernel), this, this.disposables); kernel.onStatusChanged( diff --git a/src/kernels/kernelProvider.web.unit.test.ts b/src/kernels/kernelProvider.web.unit.test.ts index 6c8cc09cda2..f0a3ee42085 100644 --- a/src/kernels/kernelProvider.web.unit.test.ts +++ b/src/kernels/kernelProvider.web.unit.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import * as sinon from 'sinon'; import { anything, instance, mock, when } from 'ts-mockito'; -import { EventEmitter, Memento } from 'vscode'; +import { CancellationToken, CancellationTokenSource, EventEmitter, Memento } from 'vscode'; import { IConfigurationService, IDisposable, IExtensionContext } from '../platform/common/types'; import { createEventHandler } from '../test/common'; import { createKernelController, TestNotebookDocument } from '../test/datascience/notebook/executionHelper'; @@ -16,6 +16,7 @@ import { IKernelSessionFactory, IKernelController, IStartupCodeProviders, Kernel import { ThirdPartyKernelProvider } from './kernelProvider.node'; import { dispose } from '../platform/common/utils/lifecycle'; import { noop } from '../test/core'; +import { AsyncEmitter } from '../platform/common/utils/events'; suite('Jupyter Session', () => { suite('Web Kernel Provider', function () { @@ -76,7 +77,7 @@ suite('Jupyter Session', () => { await Promise.all(asyncDisposables.map((item) => item.dispose().catch(noop))); asyncDisposables.length = 0; }); - function testKernelProviderEvents(thirdPartyKernelProvider = false) { + async function testKernelProviderEvents(thirdPartyKernelProvider = false) { const kernelProvider = thirdPartyKernelProvider ? create3rdPartyKernelProvider() : createKernelProvider(); const kernelCreated = createEventHandler(kernelProvider, 'onDidCreateKernel', disposables); const kernelStarted = createEventHandler(kernelProvider, 'onDidStartKernel', disposables); @@ -88,7 +89,10 @@ suite('Jupyter Session', () => { const onStarted = new EventEmitter(); const onStatusChanged = new EventEmitter(); const onRestartedEvent = new EventEmitter(); - const onPostInitializedEvent = new EventEmitter(); + const onPostInitializedEvent = new AsyncEmitter<{ + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>(); const onDisposedEvent = new EventEmitter(); disposables.push(onStatusChanged); disposables.push(onRestartedEvent); @@ -136,12 +140,12 @@ suite('Jupyter Session', () => { assert.isTrue(kernelStatusChanged.fired, 'IKernelProvider.onKernelStatusChanged not fired'); onRestartedEvent.fire(); assert.isTrue(kernelRestarted.fired, 'IKernelProvider.onKernelRestarted not fired'); - onPostInitializedEvent.fire(); + await onPostInitializedEvent.fireAsync({}, new CancellationTokenSource().token); assert.isTrue(kernelPostInitialized.fired, 'IKernelProvider.onDidPostInitializeKernel not fired'); onDisposedEvent.fire(); assert.isTrue(kernelDisposed.fired, 'IKernelProvider.onDisposedEvent not fired'); } - test('Kernel Events', () => testKernelProviderEvents(false)); - test('3rd Party Kernel Events', () => testKernelProviderEvents(true)); + test('Kernel Events', async () => await testKernelProviderEvents(false)); + test('3rd Party Kernel Events', async () => await testKernelProviderEvents(true)); }); }); diff --git a/src/kernels/types.ts b/src/kernels/types.ts index b9bc32cf8a4..7dfd9f78a80 100644 --- a/src/kernels/types.ts +++ b/src/kernels/types.ts @@ -355,7 +355,7 @@ export interface IBaseKernel extends IAsyncDisposable { readonly onDisposed: Event; readonly onStarted: Event; readonly onRestarted: Event; - readonly onPostInitialized: Event; + readonly onPostInitialized: Event<{ token: CancellationToken; waitUntil(thenable: Thenable): void }>; readonly restarting: Promise; readonly status: KernelMessage.Status; readonly disposed: boolean; @@ -513,7 +513,11 @@ export interface IBaseKernelProvider extends IAsyncDispos onDidRestartKernel: Event; onDidDisposeKernel: Event; onKernelStatusChanged: Event<{ status: KernelMessage.Status; kernel: T }>; - onDidPostInitializeKernel: Event; + onDidPostInitializeKernel: Event<{ + kernel: T; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>; } /** diff --git a/src/platform/common/constants.ts b/src/platform/common/constants.ts index 79b04f69ad9..ccceaa13805 100644 --- a/src/platform/common/constants.ts +++ b/src/platform/common/constants.ts @@ -381,6 +381,7 @@ export enum Telemetry { NewJupyterKernelApiUsage = 'DATASCIENCE.JUPYTER_NEW_KERNEL_API_USAGE', NewJupyterKernelsApiUsage = 'DATASCIENCE.JUPYTER_NEW_KERNELS_API_USAGE', NewJupyterKernelApiExecution = 'DATASCIENCE.JUPYTER_NEW_KERNEL_API_EXEC', + NewJupyterKernelApiKernelStartupWaitUntil = 'DATASCIENCE.JUPYTER_NEW_KERNEL_API_KERNEL_STARTUP_WAIT_UNTIL', JupyterKernelApiAccess = 'DATASCIENCE.JUPYTER_KERNEL_API_ACCESS', JupyterKernelStartupHook = 'DATASCIENCE.JUPYTER_KERNEL_STARTUP_HOOK', JupyterKernelSpecEnumeration = 'DATASCIENCE.JUPYTER_KERNEL_SPEC_FETCH_FAILURE', diff --git a/src/platform/common/utils/events.ts b/src/platform/common/utils/events.ts index a84cd13eed6..2021ca74120 100644 --- a/src/platform/common/utils/events.ts +++ b/src/platform/common/utils/events.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import type { Event } from 'vscode'; +import { CancellationToken, Disposable, Event } from 'vscode'; import { IDisposable } from '../types'; import { EmptyDisposable } from './lifecycle'; @@ -47,3 +47,81 @@ export function toPromise(event: Event, thisArgs: any = null, disposables? // eslint-disable-next-line @typescript-eslint/no-explicit-any export const EmptyEvent: Event = () => EmptyDisposable; + +export interface IWaitUntil { + token: CancellationToken; + waitUntil(thenable: Promise): void; +} +export type IWaitUntilData = Omit, 'token'>; + +// Based on AsyncEmitter in https://github.com/microsoft/vscode/blob/88e6face01795b161523824ba075444fad55466a/src/vs/base/common/event.ts#L1303 +export class AsyncEmitter { + private _listeners: Set<(e: T) => unknown> = new Set(); + private _asyncDeliveryQueue?: Array<[(ev: T) => void, IWaitUntilData]>; + private _event: Event | undefined; + + public get event(): Event { + if (!this._event) { + this._event = (listener: (e: T) => unknown, thisArg?: unknown, disposables?: Disposable[]): Disposable => { + this._listeners.add(thisArg ? listener.bind(thisArg) : listener); + const disposable = new Disposable(() => this._listeners.delete(listener)); + disposables?.push(disposable); + return disposable; + }; + } + return this._event; + } + + async fireAsync(data: IWaitUntilData, token: CancellationToken): Promise { + if (!this._listeners) { + return; + } + + if (!this._asyncDeliveryQueue) { + this._asyncDeliveryQueue = []; + } + + for (const listener of this._listeners) { + this._asyncDeliveryQueue!.push([listener, data]); + } + + while (this._asyncDeliveryQueue.length > 0 && !token.isCancellationRequested) { + const [listener, data] = this._asyncDeliveryQueue.shift()!; + const thenables: Promise[] = []; + + const event = { + ...data, + token, + waitUntil: (p: Promise): void => { + if (Object.isFrozen(thenables)) { + throw new Error('waitUntil can NOT be called asynchronous'); + } + thenables.push(p); + } + }; + + try { + listener(event); + } catch (e) { + console.error(e); + continue; + } + + // freeze thenables-collection to enforce sync-calls to + // wait until and then wait for all thenables to resolve + Object.freeze(thenables); + + await Promise.allSettled(thenables).then((values) => { + for (const value of values) { + if (value.status === 'rejected') { + console.error(value.reason); + } + } + }); + } + } + + dispose(): void { + this._listeners.clear(); + } +} diff --git a/src/platform/common/utils/events.unit.test.ts b/src/platform/common/utils/events.unit.test.ts new file mode 100644 index 00000000000..1d7e7814230 --- /dev/null +++ b/src/platform/common/utils/events.unit.test.ts @@ -0,0 +1,237 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { AsyncEmitter, IWaitUntil } from './events'; +import * as fakeTimers from '@sinonjs/fake-timers'; +import { Disposable, CancellationTokenSource } from 'vscode'; +import { IDisposable } from '../types'; +import { dispose } from './lifecycle'; +import { createDeferredFromPromise } from './async'; + +suite('AsyncEmitter', async () => { + let clock: fakeTimers.InstalledClock; + let disposables: IDisposable[] = []; + + const timeout = (timeout: number) => new Promise((resolve) => setTimeout(resolve, timeout)); + + setup(() => { + clock = fakeTimers.install(); + console.error = sinon.stub(); // Errors in AsyncEmitter are logged, so we can suppress them here. + disposables.push(new Disposable(() => clock.uninstall())); + }); + + teardown(() => { + sinon.restore(); + disposables = dispose(disposables); + }); + + test('event has waitUntil-function', async function () { + let called = false; + + interface E extends IWaitUntil { + foo: boolean; + bar: number; + } + + const emitter = new AsyncEmitter(); + disposables.push(emitter); + disposables.push( + emitter.event((e) => { + called = true; + assert.strictEqual(e.foo, true); + assert.strictEqual(e.bar, 1); + assert.strictEqual(typeof e.waitUntil, 'function'); + }) + ); + + await emitter.fireAsync({ foo: true, bar: 1 }, new CancellationTokenSource().token); + assert.strictEqual(called, true); + }); + + test('sequential delivery', async () => { + interface E extends IWaitUntil { + foo: boolean; + } + + let globalState = 0; + const emitter = new AsyncEmitter(); + disposables.push(emitter); + disposables.push( + emitter.event((e) => { + e.waitUntil( + timeout(10).then((_) => { + assert.strictEqual(globalState, 0); + globalState += 1; + }) + ); + }) + ); + disposables.push( + emitter.event((e) => { + e.waitUntil( + timeout(1).then((_) => { + assert.strictEqual(globalState, 1); + globalState += 1; + }) + ); + }) + ); + + void clock.tickAsync(100); + await emitter.fireAsync({ foo: true }, new CancellationTokenSource().token); + assert.strictEqual(globalState, 2); + }); + + test('sequential, in-order delivery', async function () { + interface E extends IWaitUntil { + foo: number; + } + const events: number[] = []; + let done = false; + const emitter = new AsyncEmitter(); + disposables.push(emitter); + + // e1 + disposables.push( + emitter.event((e) => { + e.waitUntil( + timeout(10).then(async (_) => { + if (e.foo === 1) { + await emitter.fireAsync({ foo: 2 }, new CancellationTokenSource().token); + assert.deepStrictEqual(events, [1, 2]); + done = true; + } + }) + ); + }) + ); + + // e2 + disposables.push( + emitter.event((e) => { + events.push(e.foo); + e.waitUntil(timeout(7)); + }) + ); + + void clock.tickAsync(100); + await emitter.fireAsync({ foo: 1 }, new CancellationTokenSource().token); + assert.ok(done); + }); + + test('sequential, in-order delivery with proxied event emitter', async function () { + interface E extends IWaitUntil { + foo: number; + } + const events: Array<{ foo: number; bar: number }> = []; + let done = false; + const emitter = new AsyncEmitter(); + disposables.push(emitter); + + let bar = 0; + // set up emitter 2 as a proxied event emitter with an additional param + const emitter2 = new AsyncEmitter<{ bar: number } & E>(); + disposables.push(emitter2); + disposables.push(emitter.event((e) => e.waitUntil(emitter2.fireAsync({ foo: e.foo, bar: bar++ }, e.token)))); + + // e1 + disposables.push( + emitter2.event((e) => { + e.waitUntil( + timeout(10).then(async (_) => { + if (e.foo === 1) { + await emitter.fireAsync({ foo: 2 }, new CancellationTokenSource().token); + assert.deepStrictEqual(events, [ + { foo: 1, bar: 0 }, + { foo: 2, bar: 1 } + ]); + done = true; + } + }) + ); + }) + ); + + // e2 + disposables.push( + emitter2.event((e) => { + events.push({ foo: e.foo, bar: e.bar }); + e.waitUntil(timeout(7)); + }) + ); + + const deferrable = createDeferredFromPromise( + emitter.fireAsync({ foo: 1 }, new CancellationTokenSource().token) + ); + + // time: 9 + await clock.tickAsync(9); + assert.deepStrictEqual(events, []); + assert.strictEqual(deferrable.resolved, false); + + // time: 10 (e1 is unblocked, so e2 runs) + await clock.tickAsync(1); + assert.deepStrictEqual(events, [{ foo: 1, bar: 0 }]); + assert.strictEqual(deferrable.resolved, false); + + // time: 17 (e2 is unblocked so second fireAsync can run) + await clock.tickAsync(7); + assert.deepStrictEqual(events, [{ foo: 1, bar: 0 }]); + assert.strictEqual(deferrable.resolved, false); + + // time: 27 (e2 is unblocked again) + await clock.tickAsync(10); + assert.deepStrictEqual(events, [ + { foo: 1, bar: 0 }, + { foo: 2, bar: 1 } + ]); + assert.strictEqual(deferrable.resolved, false); + + // time: 34 (second fireAsync resolves) + await clock.tickAsync(7); + assert.deepStrictEqual(events, [ + { foo: 1, bar: 0 }, + { foo: 2, bar: 1 } + ]); + + // make sure that the fireAsync from the parent resolves at this point + assert.strictEqual(deferrable.resolved, true); + assert.ok(done); + }); + + test('catch errors', async function () { + interface E extends IWaitUntil { + foo: boolean; + } + + let globalState = 0; + const emitter = new AsyncEmitter(); + disposables.push(emitter); + disposables.push( + emitter.event((e) => { + globalState += 1; + e.waitUntil(new Promise((_r, reject) => reject(new Error()))); + }) + ); + disposables.push( + emitter.event((e) => { + globalState += 1; + e.waitUntil(timeout(10)); + e.waitUntil(timeout(20).then(() => globalState++)); // multiple `waitUntil` are supported and awaited on + }) + ); + + void clock.tickAsync(100); + await emitter + .fireAsync({ foo: true }, new CancellationTokenSource().token) + .then(() => { + assert.strictEqual(globalState, 3); + }) + .catch((e) => { + console.log(e); + assert.ok(false); + }); + }); +}); diff --git a/src/standalone/api/kernels/api.vscode.common.test.ts b/src/standalone/api/kernels/api.vscode.common.test.ts index 9236e2e68ff..439899b0129 100644 --- a/src/standalone/api/kernels/api.vscode.common.test.ts +++ b/src/standalone/api/kernels/api.vscode.common.test.ts @@ -268,35 +268,41 @@ suiteMandatory('Kernel API Tests @typescript', function () { // Register event listener to track invocations const source = new CancellationTokenSource(); let startEventCounter = 0; + + const executionOrderSet = createDeferred(); disposables.push( - kernels.onDidStart(async ({ kernel }) => { - const codeToRun = - startEventCounter === 0 ? `let foo = ${startEventCounter}` : `foo = ${startEventCounter}`; - startEventCounter++; - - // This is needed for the async generator to get executed. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for await (const _out of kernel.executeCode(codeToRun, source.token)) { - } + kernels.onDidStart(async ({ kernel, waitUntil }) => { + waitUntil( + (async () => { + const codeToRun = + startEventCounter === 0 + ? `let foo = ${startEventCounter}` + : `foo = ${startEventCounter}`; + startEventCounter++; + + // This is needed for the async generator to get executed. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for await (const _out of kernel.executeCode(codeToRun, source.token)) { + } + + // Cell should not have executed at this point. + assert.strictEqual(executionOrderSet.resolved, false); + })() + ); }) ); await insertCodeCell('console.log(foo)', { index: 0, language: 'typescript' }); - await realKernel.start({ - disableUI: true, - onDidChangeDisableUI: () => ({ - dispose: noop - }) - }); assert.equal(startEventCounter, 0, 'Kernel start event was triggered for a non-user kernel start'); const cell = notebook.cellAt(0)!; - const executionOrderSet = createDeferred(); const eventHandler = notebookCellExecutions.onDidChangeNotebookCellExecutionState((e) => { if (e.cell === cell && e.cell.executionSummary?.executionOrder) { executionOrderSet.resolve(); } }); disposables.push(eventHandler); + + // Do not explicitly start the kernel here, let it be triggered by the cell execution. await Promise.all([runCell(cell), waitForExecutionCompletedSuccessfully(cell), executionOrderSet.promise]); // Validate the cell execution output is equal to the expected value of "foo = 0" diff --git a/src/standalone/api/kernels/backgroundExecution.ts b/src/standalone/api/kernels/backgroundExecution.ts index 04e8659e5bb..5732de0b955 100644 --- a/src/standalone/api/kernels/backgroundExecution.ts +++ b/src/standalone/api/kernels/backgroundExecution.ts @@ -19,7 +19,7 @@ export async function execCodeInBackgroundThread( ) { const counter = executionCounters.get(kernel) || 0; executionCounters.set(kernel, counter + 1); - const api = createKernelApiForExtension(JVSC_EXTENSION_ID, kernel); + const { api } = createKernelApiForExtension(JVSC_EXTENSION_ID, kernel); const mime = `application/vnd.vscode.bg.execution.${counter}`; const mimeFinalResult = `application/vnd.vscode.bg.execution.${counter}.result`; const mimeErrorResult = `application/vnd.vscode.bg.execution.${counter}.error`; diff --git a/src/standalone/api/kernels/index.ts b/src/standalone/api/kernels/index.ts index b84065cb414..47bc4f8bcf1 100644 --- a/src/standalone/api/kernels/index.ts +++ b/src/standalone/api/kernels/index.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { Uri, workspace, EventEmitter } from 'vscode'; +import { Uri, workspace, EventEmitter, CancellationToken } from 'vscode'; import { Kernel, Kernels } from '../../../api'; import { ServiceContainer } from '../../../platform/ioc/container'; import { IKernel, IKernelProvider } from '../../../kernels/types'; @@ -10,13 +10,43 @@ import { Telemetry, sendTelemetryEvent } from '../../../telemetry'; import { DATA_WRANGLER_EXTENSION_ID, JVSC_EXTENSION_ID } from '../../../platform/common/constants'; import { initializeInteractiveOrNotebookTelemetryBasedOnUserAction } from '../../../kernels/telemetry/helper'; import { IDisposableRegistry } from '../../../platform/common/types'; +import { createDeferredFromPromise } from '../../../platform/common/utils/async'; +import { logger } from '../../../platform/logging'; +import { StopWatch } from '../../../platform/common/utils/stopWatch'; +import { sendKernelTelemetryEvent } from '../../../kernels/telemetry/sendKernelTelemetryEvent'; +import { KernelExecutionProgressIndicator } from './kernelProgressIndicator'; -const kernelCache = new WeakMap(); -let _onDidStart: EventEmitter<{ uri: Uri; kernel: Kernel }> | undefined = undefined; +const extensionAPICache = new Map< + string, + { + onDidStart: + | EventEmitter<{ + uri: Uri; + kernel: Kernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }> + | undefined; + // Kernel cache needs to be scoped per extension to make sure that the progress messages + // show accurately which extension is actually using it. + kernels: WeakMap; + } +>(); + +function getOrCreateExtensionAPI(extensionId: string) { + if (!extensionAPICache.has(extensionId)) { + extensionAPICache.set(extensionId, { + onDidStart: undefined, + kernels: new WeakMap() + }); + } + return extensionAPICache.get(extensionId)!; +} function getWrappedKernel(kernel: IKernel, extensionId: string) { - let wrappedKernel = kernelCache.get(kernel) || createKernelApiForExtension(extensionId, kernel); - kernelCache.set(kernel, wrappedKernel); + const extensionAPI = getOrCreateExtensionAPI(extensionId); + let wrappedKernel = extensionAPI.kernels.get(kernel) || createKernelApiForExtension(extensionId, kernel); + extensionAPI.kernels.set(kernel, wrappedKernel); return wrappedKernel; } @@ -44,7 +74,8 @@ export function getKernelsApi(extensionId: string): Kernels { kernel.kernelConnectionMetadata ); } - return getWrappedKernel(kernel, extensionId); + const { api } = getWrappedKernel(kernel, extensionId); + return api; }, get onDidStart() { if (![JVSC_EXTENSION_ID, DATA_WRANGLER_EXTENSION_ID].includes(extensionId)) { @@ -52,21 +83,54 @@ export function getKernelsApi(extensionId: string): Kernels { } // We can cache the event emitter for subsequent calls. - if (!_onDidStart) { + const extensionAPI = getOrCreateExtensionAPI(extensionId); + if (!extensionAPI.onDidStart) { const kernelProvider = ServiceContainer.instance.get(IKernelProvider); const disposableRegistry = ServiceContainer.instance.get(IDisposableRegistry); - _onDidStart = new EventEmitter<{ uri: Uri; kernel: Kernel }>(); + extensionAPI.onDidStart = new EventEmitter<{ + uri: Uri; + kernel: Kernel; + token: CancellationToken; + waitUntil(thenable: Thenable): void; + }>(); disposableRegistry.push( - kernelProvider.onDidPostInitializeKernel((kernel) => { - _onDidStart?.fire({ uri: kernel.uri, kernel: getWrappedKernel(kernel, extensionId) }); + extensionAPI.onDidStart, + kernelProvider.onDidPostInitializeKernel(({ kernel, token, waitUntil }) => { + const { api, progress } = getWrappedKernel(kernel, extensionId); + extensionAPI.onDidStart?.fire({ + uri: kernel.uri, + kernel: api, + token, + waitUntil: (thenable) => { + // Wrap around the `waitUntil` method to inject telemetry and notifications. + // For notifications, we reuse the kernel execution progress indicator message + // regardless of whether something is actually running on the kernel, since + // it is effectively preventing access to it. + const deferrable = createDeferredFromPromise(Promise.resolve(thenable)); + waitUntil(thenable); + const disposable = progress.show(); + const stopWatch = new StopWatch(); + void deferrable.promise.finally(() => { + logger.trace( + `${extensionId} took ${stopWatch.elapsedTime}ms during kernel startup` + ); + sendKernelTelemetryEvent( + kernel.resourceUri, + Telemetry.NewJupyterKernelApiKernelStartupWaitUntil, + { duration: stopWatch.elapsedTime }, + { extensionId } + ); + disposable.dispose(); + }); + } + }); }), - _onDidStart, - { dispose: () => (_onDidStart = undefined) } + extensionAPI.onDidStart ); } - return _onDidStart.event; + return extensionAPI.onDidStart.event; } }; } diff --git a/src/standalone/api/kernels/kernel.ts b/src/standalone/api/kernels/kernel.ts index efb1cb962d7..2e74d0dbf02 100644 --- a/src/standalone/api/kernels/kernel.ts +++ b/src/standalone/api/kernels/kernel.ts @@ -3,32 +3,17 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { - l10n, - CancellationToken, - ProgressLocation, - extensions, - window, - Disposable, - workspace, - NotebookDocument, - Event, - EventEmitter, - NotebookCellOutput, - type NotebookExecution, - type NotebookController -} from 'vscode'; +import { l10n, CancellationToken, Event, EventEmitter, NotebookCellOutput, type NotebookController } from 'vscode'; import { Kernel, KernelStatus, Output } from '../../../api'; import { ServiceContainer } from '../../../platform/ioc/container'; import { IKernel, IKernelProvider, INotebookKernelExecution } from '../../../kernels/types'; -import { getDisplayNameOrNameOfKernelConnection, isPythonKernelConnection } from '../../../kernels/helpers'; +import { isPythonKernelConnection } from '../../../kernels/helpers'; import { IDisposable, IDisposableRegistry } from '../../../platform/common/types'; -import { DisposableBase, ReferenceCollection, dispose } from '../../../platform/common/utils/lifecycle'; +import { DisposableBase, dispose } from '../../../platform/common/utils/lifecycle'; import { noop } from '../../../platform/common/utils/misc'; import { getTelemetrySafeHashedString } from '../../../platform/telemetry/helpers'; import { Telemetry, sendTelemetryEvent } from '../../../telemetry'; import { StopWatch } from '../../../platform/common/utils/stopWatch'; -import { Deferred, createDeferred, sleep } from '../../../platform/common/utils/async'; import { once } from '../../../platform/common/utils/events'; import { logger } from '../../../platform/logging'; import { @@ -45,133 +30,7 @@ import { import { getNotebookCellOutputMetadata } from '../../../kernels/execution/helpers'; import { registerChangeHandler, requestApiAccess } from './apiAccess'; import { IControllerRegistration } from '../../../notebooks/controllers/types'; - -class NotebookExecutionReferenceCollection extends ReferenceCollection { - private existingExecutions?: NotebookExecution; - constructor( - private readonly controller: NotebookController, - private readonly notebook: NotebookDocument - ) { - super(); - } - public dispose() { - this.disposeExistingExecution(); - } - - protected override createReferencedObject(_key: string, ..._args: any[]): NotebookExecution { - if (!this.existingExecutions) { - this.existingExecutions = this.controller.createNotebookExecution(this.notebook); - this.existingExecutions.start(); - } - return this.existingExecutions; - } - protected override destroyReferencedObject(_key: string, _object: NotebookExecution): void { - this.disposeExistingExecution(); - } - private disposeExistingExecution() { - try { - this.existingExecutions?.end(); - } catch { - // - } - this.existingExecutions = undefined; - } -} -/** - * Displays a progress indicator when 3rd party extensions execute code against a kernel. - * The progress indicator is displayed only when the notebook is visible. - * - * We need this to notify users when execution takes place for: - * 1. Transparency (they might not know that some code is being executed in a kernel) - * 2. If users experience delays in kernel execution within notebooks, then they have an idea why this might be the case. - */ -class KernelExecutionProgressIndicator { - private readonly controllerDisplayName: string; - private readonly notebook?: NotebookDocument; - private deferred?: Deferred; - private disposable?: IDisposable; - private eventHandler: IDisposable; - private readonly title: string; - private displayInProgress?: boolean; - private shouldDisplayProgress?: boolean; - private static notificationsPerExtension = new WeakMap>(); - private executionRefCountedDisposableFactory?: NotebookExecutionReferenceCollection; - constructor( - private readonly extensionId: string, - private readonly kernel: IKernel, - controller?: NotebookController - ) { - this.executionRefCountedDisposableFactory = controller - ? new NotebookExecutionReferenceCollection(controller, kernel.notebook) - : undefined; - const extensionDisplayName = extensions.getExtension(extensionId)?.packageJSON?.displayName || extensionId; - this.notebook = workspace.notebookDocuments.find((n) => n.uri.toString() === kernel.resourceUri?.toString()); - this.controllerDisplayName = getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata); - this.title = l10n.t(`Executing code in {0} from {1}`, this.controllerDisplayName, extensionDisplayName); - this.eventHandler = window.onDidChangeVisibleNotebookEditors(this.showProgressImpl, this); - } - dispose() { - this.eventHandler.dispose(); - this.disposable?.dispose(); - this.executionRefCountedDisposableFactory?.dispose(); - } - - show() { - const execution = this.executionRefCountedDisposableFactory?.acquire(''); - if (this.deferred && !this.deferred.completed) { - const oldDeferred = this.deferred; - this.deferred = createDeferred(); - oldDeferred.resolve(); - } else { - this.deferred = createDeferred(); - this.showProgress().catch(noop); - } - return (this.disposable = new Disposable(() => { - execution?.dispose(); - this.deferred?.resolve(); - })); - } - private async showProgress() { - // Give a grace period of 1000ms to avoid displaying progress indicators too aggressively. - // Clearly some extensions can take a while, see here https://github.com/microsoft/vscode-jupyter/issues/15613 - // More than 1s is too long, - await sleep(1_000); - if (!this.deferred || this.deferred.completed || this.displayInProgress) { - return; - } - this.shouldDisplayProgress = true; - await Promise.all([this.showProgressImpl(), this.waitUntilCompleted()]); - this.shouldDisplayProgress = false; - } - private async showProgressImpl() { - const notifiedExtensions = - KernelExecutionProgressIndicator.notificationsPerExtension.get(this.kernel) || new Set(); - KernelExecutionProgressIndicator.notificationsPerExtension.set(this.kernel, notifiedExtensions); - if (notifiedExtensions.has(this.extensionId)) { - return; - } - notifiedExtensions.add(this.extensionId); - if (!this.notebook || !this.shouldDisplayProgress) { - return; - } - if (!window.visibleNotebookEditors.some((e) => e.notebook === this.notebook)) { - return; - } - this.displayInProgress = true; - await window.withProgress({ location: ProgressLocation.Notification, title: this.title }, async () => - this.waitUntilCompleted() - ); - this.displayInProgress = false; - } - private async waitUntilCompleted() { - let deferred = this.deferred; - while (deferred && !deferred.completed) { - await deferred.promise; - // Possible the deferred was replaced. - deferred = this.deferred; - } - } -} +import { KernelExecutionProgressIndicator } from './kernelProgressIndicator'; /** * Error to be throw to to notify calls of the API that access to the API has been revoked by the user. @@ -285,7 +144,7 @@ class WrappedKernelPerExtension extends DisposableBase implements Kernel { ) { const wrapper = new WrappedKernelPerExtension(extensionId, kernel, execution, controller); ServiceContainer.instance.get(IDisposableRegistry).push(wrapper); - return wrapper._api; + return { api: wrapper._api, progress: wrapper.progress }; } private async checkAccess() { diff --git a/src/standalone/api/kernels/kernel.unit.test.ts b/src/standalone/api/kernels/kernel.unit.test.ts index f44eb100d72..95be3ea2487 100644 --- a/src/standalone/api/kernels/kernel.unit.test.ts +++ b/src/standalone/api/kernels/kernel.unit.test.ts @@ -101,7 +101,7 @@ suite('Kernel Api', () => { test('Verify Access Denied error message has expected value for the property `name`', async () => { try { - const api = createKernelApiForExtension('xyz', instance(kernel)); + const { api } = createKernelApiForExtension('xyz', instance(kernel)); for await (const x of api.executeCode('bogus', token)) { assert.fail(`Should have failed without producing any value such as ${x}`); } diff --git a/src/standalone/api/kernels/kernelProgressIndicator.ts b/src/standalone/api/kernels/kernelProgressIndicator.ts new file mode 100644 index 00000000000..aa6b5bbd2e0 --- /dev/null +++ b/src/standalone/api/kernels/kernelProgressIndicator.ts @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { + Disposable, + extensions, + l10n, + NotebookController, + NotebookDocument, + NotebookExecution, + ProgressLocation, + window, + workspace +} from 'vscode'; +import { createDeferred, Deferred, sleep } from '../../../platform/common/utils/async'; +import { IDisposable } from '../../../platform/common/types'; +import { IKernel } from '../../../kernels/types'; +import { getDisplayNameOrNameOfKernelConnection } from '../../../kernels/helpers'; +import { IReference, ReferenceCollection } from '../../../platform/common/utils/lifecycle'; +import { noop } from '../../../platform/common/utils/misc'; + +class NotebookExecutionReferenceCollection extends ReferenceCollection { + private existingExecutions?: NotebookExecution; + constructor( + private readonly controller: NotebookController, + private readonly notebook: NotebookDocument + ) { + super(); + } + public dispose() { + this.disposeExistingExecution(); + } + + protected override createReferencedObject(_key: string, ..._args: any[]): NotebookExecution { + if (!this.existingExecutions) { + this.existingExecutions = this.controller.createNotebookExecution(this.notebook); + this.existingExecutions.start(); + } + return this.existingExecutions; + } + protected override destroyReferencedObject(_key: string, _object: NotebookExecution): void { + this.disposeExistingExecution(); + } + private disposeExistingExecution() { + try { + this.existingExecutions?.end(); + } catch { + // + } + this.existingExecutions = undefined; + } +} + +export class KernelExecutionProgressIndicator { + private readonly controllerDisplayName: string; + private readonly notebook?: NotebookDocument; + private deferred?: Deferred; + private disposable?: IDisposable; + private eventHandler: IDisposable; + private readonly title: string; + private displayInProgress?: boolean; + private shouldDisplayProgress?: boolean; + private static notificationsPerExtension = new WeakMap>(); + private executionRefCountedDisposableFactory?: NotebookExecutionReferenceCollection; + constructor( + private readonly extensionId: string, + private readonly kernel: IKernel, + controller?: NotebookController + ) { + this.executionRefCountedDisposableFactory = controller + ? new NotebookExecutionReferenceCollection(controller, kernel.notebook) + : undefined; + const extensionDisplayName = extensions.getExtension(extensionId)?.packageJSON?.displayName || extensionId; + this.notebook = workspace.notebookDocuments.find((n) => n.uri.toString() === kernel.resourceUri?.toString()); + this.controllerDisplayName = getDisplayNameOrNameOfKernelConnection(kernel.kernelConnectionMetadata); + this.title = l10n.t(`Executing code in {0} from {1}`, this.controllerDisplayName, extensionDisplayName); + this.eventHandler = window.onDidChangeVisibleNotebookEditors(this.showProgressImpl, this); + } + dispose() { + this.eventHandler.dispose(); + this.disposable?.dispose(); + this.executionRefCountedDisposableFactory?.dispose(); + } + + show() { + let execution: IReference | undefined; + try { + execution = this.executionRefCountedDisposableFactory?.acquire(''); + } catch { + // It's okay to not acquire an execution ref here as there may already be one. + // E.g. when user is executing a cell, then an execution is already in progress & a progress will be displayed. + // Hence no need to acquire another execution ref. + } + + if (this.deferred && !this.deferred.completed) { + const oldDeferred = this.deferred; + this.deferred = createDeferred(); + oldDeferred.resolve(); + } else { + this.deferred = createDeferred(); + void this.showProgress().catch(noop); + } + return (this.disposable = new Disposable(() => { + execution?.dispose(); + this.deferred?.resolve(); + })); + } + private async showProgress() { + // Give a grace period of 1000ms to avoid displaying progress indicators too aggressively. + // Clearly some extensions can take a while, see here https://github.com/microsoft/vscode-jupyter/issues/15613 + // More than 1s is too long, + await sleep(1_000); + if (!this.deferred || this.deferred.completed || this.displayInProgress) { + return; + } + this.shouldDisplayProgress = true; + await Promise.all([this.showProgressImpl(), this.waitUntilCompleted()]); + this.shouldDisplayProgress = false; + } + private async showProgressImpl() { + const notifiedExtensions = + KernelExecutionProgressIndicator.notificationsPerExtension.get(this.kernel) || new Set(); + KernelExecutionProgressIndicator.notificationsPerExtension.set(this.kernel, notifiedExtensions); + if (notifiedExtensions.has(this.extensionId)) { + return; + } + notifiedExtensions.add(this.extensionId); + if (!this.notebook || !this.shouldDisplayProgress) { + return; + } + if (!window.visibleNotebookEditors.some((e) => e.notebook === this.notebook)) { + return; + } + this.displayInProgress = true; + await window.withProgress({ location: ProgressLocation.Notification, title: this.title }, async () => + this.waitUntilCompleted() + ); + this.displayInProgress = false; + } + private async waitUntilCompleted() { + let deferred = this.deferred; + while (deferred && !deferred.completed) { + await deferred.promise; + // Possible the deferred was replaced. + deferred = this.deferred; + } + } +} diff --git a/src/telemetry.ts b/src/telemetry.ts index 3ac87b3dd76..3b068db0c17 100644 --- a/src/telemetry.ts +++ b/src/telemetry.ts @@ -3915,6 +3915,28 @@ export class IEventNamePropertyMapping { } } }; + /** + * Telemetry sent when an extension uses our 3rd party Kernel onDidStart API and calls waitUntil. + */ + [Telemetry.NewJupyterKernelApiKernelStartupWaitUntil]: TelemetryEventInfo< + { + /** + * Extension Id that's attempting to use the API. + */ + extensionId: string; + } & DurationMeasurement + > = { + owner: 'donjayamanne', + feature: 'N/A', + source: 'N/A', + measures: commonClassificationForDurationProperties(), + properties: { + extensionId: { + classification: 'PublicNonPersonalData', + purpose: 'FeatureInsight' + } + } + }; /** * Telemetry sent when an extension uses our 3rd party API. */