Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More progress indicator when starting a kernel #8584

Merged
merged 7 commits into from
Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/ci/performance/perf-results.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{ "run_number": 85, "head_ref": "envActivationService", "results": {} }
{ "run_number": 93, "head_ref": "addProgressIndicator", "results": {} }
2 changes: 2 additions & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"jupyter.command.jupyter.runByLineNext.title": "Run Next Line",
"jupyter.command.jupyter.runByLineStop.title": "Continue Execution",
"DataScience.checkingIfImportIsSupported": "Checking if import is supported",
"DataScience.validatingKernelDependencies": "Validating kernel dependencies",
"DataScience.installingMissingDependencies": "Installing missing dependencies",
"DataScience.exportNotebookToPython": "Exporting Notebook to Python",
"DataScience.performingExport": "Performing Export",
Expand All @@ -28,6 +29,7 @@
"DataScience.installKernel": "The Jupyter Kernel '{0}' could not be found and needs to be installed in order to execute cells in this notebook.",
"DataScience.customizeLayout": "Customize Layout",
"DataScience.warnWhenSelectingKernelWithUnSupportedPythonVersion": "The version of Python associated with the selected kernel is no longer supported. Please consider selecting a different kernel.",
"DataScience.activatingPythonEnvironment": "Activating Python Environment '{0}'",
"jupyter.kernel.percentPipCondaInstallInsteadOfBang": "Use '%{0} install' instead of '!{0} install'",
"jupyter.kernel.pipCondaInstallHoverWarning": "'!{0} install' could install packages into the wrong environment. [More info]({1})",
"jupyter.kernel.replacePipCondaInstallCodeAction": "Replace with '%{0} install'",
Expand Down
62 changes: 40 additions & 22 deletions src/client/common/process/condaService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const CACHEKEY_FOR_CONDA_INFO = 'CONDA_INFORMATION_CACHE';
export class CondaService {
private _file?: string;
private _version?: SemVer;
private _previousVersionCall?: Promise<SemVer | undefined>;
private _previousFileCall?: Promise<string | undefined>;
constructor(
@inject(IPythonApiProvider) private readonly pythonApi: IPythonApiProvider,
@inject(IMemento) @named(GLOBAL_MEMENTO) private readonly globalState: Memento,
Expand All @@ -24,35 +26,51 @@ export class CondaService {
if (this._version) {
return this._version;
}
const latestInfo = this.pythonApi
.getApi()
.then((api) => (api.getCondaVersion ? api.getCondaVersion() : undefined));
void latestInfo.then((version) => {
this._version = version;
void this.updateCache();
});
const cachedInfo = createDeferredFromPromise(this.getCachedInformation());
await Promise.race([cachedInfo, latestInfo]);
if (cachedInfo.completed && cachedInfo.value?.version) {
return (this._version = cachedInfo.value.version);
if (this._previousVersionCall) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ensure we make these calls only once

return this._previousVersionCall;
}
return latestInfo;
const promise = async () => {
const latestInfo = this.pythonApi
.getApi()
.then((api) => (api.getCondaVersion ? api.getCondaVersion() : undefined));
void latestInfo.then((version) => {
this._version = version;
void this.updateCache();
});
const cachedInfo = createDeferredFromPromise(this.getCachedInformation());
await Promise.race([cachedInfo, latestInfo]);
if (cachedInfo.completed && cachedInfo.value?.version) {
return (this._version = cachedInfo.value.version);
}
return latestInfo;
};
this._previousVersionCall = promise();
return this._previousVersionCall;
}
async getCondaFile() {
if (this._file) {
return this._file;
}
const latestInfo = this.pythonApi.getApi().then((api) => (api.getCondaFile ? api.getCondaFile() : undefined));
void latestInfo.then((file) => {
this._file = file;
void this.updateCache();
});
const cachedInfo = createDeferredFromPromise(this.getCachedInformation());
await Promise.race([cachedInfo, latestInfo]);
if (cachedInfo.completed && cachedInfo.value?.file) {
return (this._file = cachedInfo.value.file);
if (this._previousFileCall) {
return this._previousFileCall;
}
return latestInfo;
const promise = async () => {
const latestInfo = this.pythonApi
.getApi()
.then((api) => (api.getCondaFile ? api.getCondaFile() : undefined));
void latestInfo.then((file) => {
this._file = file;
void this.updateCache();
});
const cachedInfo = createDeferredFromPromise(this.getCachedInformation());
await Promise.race([cachedInfo, latestInfo]);
if (cachedInfo.completed && cachedInfo.value?.file) {
return (this._file = cachedInfo.value.file);
}
return latestInfo;
};
this._previousFileCall = promise();
return this._previousFileCall;
}
private async updateCache() {
if (!this._file || !this._version) {
Expand Down
125 changes: 73 additions & 52 deletions src/client/common/process/environmentActivationService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { printEnvVariablesToFile } from './internal/scripts';
import { ProcessService } from './proc';
import { BufferDecoder } from './decoder';
import { testOnlyMethod } from '../utils/decorators';
import { KernelProgressReporter } from '../../datascience/progress/kernelProgressReporter';
import { DataScience } from '../utils/localize';

const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6';
const ENVIRONMENT_TIMEOUT = 30000;
Expand Down Expand Up @@ -132,6 +134,18 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
public async getActivatedEnvironmentVariables(
resource: Resource,
@logValue<PythonEnvironment>('path') interpreter: PythonEnvironment
): Promise<NodeJS.ProcessEnv | undefined> {
const title = DataScience.activatingPythonEnvironment().format(
interpreter.displayName || getDisplayPath(interpreter.path)
);
return KernelProgressReporter.wrapAndReportProgress(resource, title, () =>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display progress message

this.getActivatedEnvironmentVariablesImpl(resource, interpreter)
);
}
@traceDecorators.verbose('Getting activated env variables', TraceOptions.BeforeCall | TraceOptions.Arguments)
public async getActivatedEnvironmentVariablesImpl(
resource: Resource,
@logValue<PythonEnvironment>('path') interpreter: PythonEnvironment
): Promise<NodeJS.ProcessEnv | undefined> {
const stopWatch = new StopWatch();
const envVariablesOurSelves = createDeferredFromPromise(
Expand Down Expand Up @@ -218,9 +232,19 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
): Promise<NodeJS.ProcessEnv | undefined> {
const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource);
const key = `${workspaceKey}_${interpreter && getInterpreterHash(interpreter)}`;

if (this.activatedEnvVariablesCache.has(key)) {
return this.activatedEnvVariablesCache.get(key);
}

const shellInfo = defaultShells[this.platform.osType];
const envType = interpreter?.envType;
if (!shellInfo) {
traceWarning(
`Cannot get activated env variables for ${getDisplayPath(
interpreter?.path
)}, shell cannot be determined.`
);
sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, 0, {
envType,
pythonEnvType: envType,
Expand All @@ -231,68 +255,65 @@ export class EnvironmentActivationService implements IEnvironmentActivationServi
return;
}

if (this.activatedEnvVariablesCache.has(key)) {
return this.activatedEnvVariablesCache.get(key);
}

const customEnvVarsPromise = this.envVarsService.getEnvironmentVariables(resource);
// If this is a conda environment that supports conda run, then we don't need conda activation commands.
let activationCommandsPromise = this.getActivationCommands(resource, interpreter);
if (interpreter.envType === EnvironmentType.Conda) {
const condaVersion = await this.condaService.getCondaVersion();
if (condaVersionSupportsLiveStreaming(condaVersion)) {
activationCommandsPromise = Promise.resolve([] as string[]);
const promise = (async () => {
const customEnvVarsPromise = this.envVarsService.getEnvironmentVariables(resource);
// If this is a conda environment that supports conda run, then we don't need conda activation commands.
let activationCommandsPromise = this.getActivationCommands(resource, interpreter);
if (interpreter.envType === EnvironmentType.Conda) {
const condaVersion = await this.condaService.getCondaVersion();
if (condaVersionSupportsLiveStreaming(condaVersion)) {
activationCommandsPromise = Promise.resolve([] as string[]);
}
}
}

const [activationCommands, customEnvVars] = await Promise.all([
activationCommandsPromise,
customEnvVarsPromise
]);

// Check cache.
const customEnvVariablesHash = getTelemetrySafeHashedString(JSON.stringify(customEnvVars));
const cachedVariables = this.getActivatedEnvVariablesFromCache(
resource,
interpreter,
customEnvVariablesHash,
activationCommands
);
if (cachedVariables) {
traceVerbose(`Got activation Env Vars from cache`);
return cachedVariables;
}
const [activationCommands, customEnvVars] = await Promise.all([
activationCommandsPromise,
customEnvVarsPromise
]);

const condaActivation = async () => {
const stopWatch = new StopWatch();
try {
const env = await this.getCondaEnvVariables(resource, interpreter);
sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, stopWatch.elapsedTime, {
envType,
pythonEnvType: envType,
source: 'jupyter',
failed: Object.keys(env || {}).length === 0,
reason: Object.keys(env || {}).length === 0 ? 'emptyFromCondaRun' : undefined
});
return env;
} catch (ex) {
sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, stopWatch.elapsedTime, {
envType,
pythonEnvType: envType,
source: 'jupyter',
failed: true,
reason: 'unhandledError'
});
traceError('Failed to get activated environment variables ourselves', ex);
// Check cache.
const customEnvVariablesHash = getTelemetrySafeHashedString(JSON.stringify(customEnvVars));
const cachedVariables = this.getActivatedEnvVariablesFromCache(
resource,
interpreter,
customEnvVariablesHash,
activationCommands
);
if (cachedVariables) {
traceVerbose(`Got activation Env Vars from cache`);
return cachedVariables;
}
};

const promise = (async () => {
const condaActivation = async () => {
const stopWatch = new StopWatch();
try {
const env = await this.getCondaEnvVariables(resource, interpreter);
sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, stopWatch.elapsedTime, {
envType,
pythonEnvType: envType,
source: 'jupyter',
failed: Object.keys(env || {}).length === 0,
reason: Object.keys(env || {}).length === 0 ? 'emptyFromCondaRun' : undefined
});
return env;
} catch (ex) {
sendTelemetryEvent(Telemetry.GetActivatedEnvironmentVariables, stopWatch.elapsedTime, {
envType,
pythonEnvType: envType,
source: 'jupyter',
failed: true,
reason: 'unhandledError'
});
traceError('Failed to get activated environment variables ourselves', ex);
}
};

if (interpreter.envType !== EnvironmentType.Conda) {
return this.getActivatedEnvVarsUsingActivationCommands(resource, interpreter);
}
return condaActivation();
})();

promise.catch(() => {
if (this.activatedEnvVariablesCache.get(key) === promise) {
this.activatedEnvVariablesCache.delete(key);
Expand Down
8 changes: 8 additions & 0 deletions src/client/common/utils/localize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ export namespace DataScience {
'DataScience.installingMissingDependencies',
'Installing missing dependencies'
);
export const validatingKernelDependencies = localize(
'DataScience.validatingKernelDependencies',
'Validating kernel dependencies'
);
export const performingExport = localize('DataScience.performingExport', 'Performing Export');
export const convertingToPDF = localize('DataScience.convertingToPDF', 'Converting to PDF');
export const exportNotebookToPython = localize(
Expand Down Expand Up @@ -1075,6 +1079,10 @@ export namespace DataScience {
'DataScience.thanksForUsingJupyterKernelApiPleaseRegisterWithUs',
'Thanks for trying the Jupyter API. Please file an issue on our repo to use this API in production. This would prevent us from breaking your extension when updating the API (as it is still a work in progress).'
);
export const activatingPythonEnvironment = localize(
'DataScience.activatingEnvironment',
"Activating Python Environment '{0}'"
);
}

// Skip using vscode-nls and instead just compute our strings based on key values. Key values
Expand Down
49 changes: 30 additions & 19 deletions src/client/datascience/baseJupyterSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { suppressShutdownErrors } from './raw-kernel/rawKernel';
import { IJupyterSession, ISessionWithSocket, KernelSocketInformation } from './types';
import { KernelInterruptTimeoutError } from './errors/kernelInterruptTimeoutError';
import { SessionDisposedError } from './errors/sessionDisposedError';
import { KernelProgressReporter } from './progress/kernelProgressReporter';

/**
* Exception raised when starting a Jupyter Session fails.
Expand Down Expand Up @@ -295,30 +296,40 @@ export abstract class BaseJupyterSession implements IJupyterSession {
isRestartSession?: boolean
): Promise<void> {
if (session && session.kernel) {
traceInfo(`Waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`);
const progress = isRestartSession
? undefined
: KernelProgressReporter.reportProgress(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Display progress messages

this.resource,
localize.DataScience.waitingForJupyterSessionToBeIdle()
);
try {
traceInfo(`Waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`);

// When our kernel connects and gets a status message it triggers the ready promise
const deferred = createDeferred<string>();
const handler = (_session: Kernel.IKernelConnection, status: KernelMessage.Status) => {
if (status == 'idle') {
deferred.resolve(status);
// When our kernel connects and gets a status message it triggers the ready promise
const deferred = createDeferred<string>();
const handler = (_session: Kernel.IKernelConnection, status: KernelMessage.Status) => {
if (status == 'idle') {
deferred.resolve(status);
}
};
session.kernel.statusChanged?.connect(handler);
if (session.kernel.status == 'idle') {
deferred.resolve(session.kernel.status);
}
};
session.kernel.statusChanged?.connect(handler);
if (session.kernel.status == 'idle') {
deferred.resolve(session.kernel.status);
}

const result = await Promise.race([deferred.promise, sleep(timeout)]);
session.kernel.statusChanged?.disconnect(handler);
traceInfo(`Finished waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`);
const result = await Promise.race([deferred.promise, sleep(timeout)]);
session.kernel.statusChanged?.disconnect(handler);
traceInfo(`Finished waiting for idle on (kernel): ${session.kernel.id} -> ${session.kernel.status}`);

if (result.toString() == 'idle') {
return;
if (result.toString() == 'idle') {
return;
}
// If we throw an exception, make sure to shutdown the session as it's not usable anymore
this.shutdownSession(session, this.statusHandler, isRestartSession).ignoreErrors();
throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut());
} finally {
progress?.dispose();
}
// If we throw an exception, make sure to shutdown the session as it's not usable anymore
this.shutdownSession(session, this.statusHandler, isRestartSession).ignoreErrors();
throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut());
} else {
throw new JupyterInvalidKernelError(undefined);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class JupyterKernelService {
kernel.interpreter.path
)} for ${kernel.id}`
);
await this.updateKernelEnvironment(kernel.interpreter, kernel.kernelSpec, specFile, token);
await this.updateKernelEnvironment(resource, kernel.interpreter, kernel.kernelSpec, specFile, token);
}
}

Expand Down Expand Up @@ -200,6 +200,7 @@ export class JupyterKernelService {
return kernelSpecFilePath;
}
private async updateKernelEnvironment(
resource: Resource,
interpreter: PythonEnvironment | undefined,
kernel: IJupyterKernelSpec,
specFile: string,
Expand Down Expand Up @@ -242,7 +243,7 @@ export class JupyterKernelService {
// Get the activated environment variables (as a work around for `conda run` and similar).
// This ensures the code runs within the context of an activated environment.
specModel.env = await this.activationHelper
.getActivatedEnvironmentVariables(undefined, interpreter, true)
.getActivatedEnvironmentVariables(resource, interpreter, true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass resource so we can display progress message

.catch(noop)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.then((env) => (env || {}) as any);
Expand Down
Loading