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

execute hidden code with the executionQueue #7763

Closed
wants to merge 9 commits into from

Conversation

DavidKutu
Copy link

For #7740

  • Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR).
  • Title summarizes what is changing.
  • Has a news entry file (remember to thank yourself!).
  • Appropriate comments and documentation strings in the code.
  • Has sufficient logging.
  • Has telemetry for enhancements.
  • Unit tests & system/integration tests are added/updated.
  • Test plan is updated as appropriate.
  • package-lock.json has been regenerated by running npm install (if dependencies have changed).

@DavidKutu DavidKutu requested a review from a team as a code owner September 30, 2021 01:49
const stopWatch = new StopWatch();
const notebookPromise = this.startNotebook();
const promise = notebookPromise.then((nb) => executeSilently(nb.session, code));

const promise = this.kernelExecution.executeHidden(notebookPromise, code, doc);
Copy link
Author

Choose a reason for hiding this comment

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

should we do this for all the uses of executeSilently?

Copy link
Contributor

Choose a reason for hiding this comment

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

No we should not, this has the potential to break variable viewer and others.
I think we'd should have two methods.
And we might want to rename the executeCell to queueAndExecuteCell,
Similarly executeHidden and queueAndExeceuteHidden

Copy link
Contributor

Choose a reason for hiding this comment

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

We'd should not because if we have 10 cells and we're thinking all, then variables will never refresh because those hidden requests will get queued and run only after all 10 cells finish. Similarly plenty of other pitfalls.
Never this new method should be banned to make it obvious how the code will be executed, ie the fact that it's queued.

@@ -147,7 +147,7 @@ export interface IKernel extends IAsyncDisposable {
interrupt(): Promise<InterruptResult>;
restart(): Promise<void>;
executeCell(cell: NotebookCell): Promise<NotebookCellRunState>;
executeHidden(code: string): Promise<nbformat.IOutput[]>;
executeHidden(code: string, doc: NotebookDocument): Promise<nbformat.IOutput[]>;
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need to pass the document

Copy link
Contributor

Choose a reason for hiding this comment

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

It's already available in the class

Copy link
Author

Choose a reason for hiding this comment

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

We need it here to get the executionQueue

Copy link
Author

Choose a reason for hiding this comment

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

I don't see it in the class, there's documentExecutions which uses documents as keys, but there's no document in the class

Copy link
Contributor

Choose a reason for hiding this comment

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

Check line 113 of kernel.ts file

}
const cellExecution = this.executionFactory.create(cell, this.metadata);
this.queueOfCellsToExecute.push(cellExecution);
public queueCell(cell?: NotebookCell, code?: string): void {
Copy link
Contributor

@DonJayamanne DonJayamanne Sep 30, 2021

Choose a reason for hiding this comment

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

This api allows me to pass two empty arguments.
It should instead be code : NotebookCell | string
This way it's impossible to pass undefined and it's obvious what's expected without having to look at the code

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it's better to return a promise from this method, then we won't need waitForHidden.
Else you have two methods to deal with.

interface Execution {
cellExecution?: CellExecution;
codeExecution?: CodeExecution;
}
Copy link
Contributor

@DonJayamanne DonJayamanne Sep 30, 2021

Choose a reason for hiding this comment

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

This means we can have a cake with both properties as empty, which is not correct.
We should change this to type Execution = CellExecution | CodeExecution

this.queueToExecute.push({ codeExecution });

traceInfo('Hidden cell queued for execution', codeExecution.code.substring(0, 50));
}

Copy link
Contributor

Choose a reason for hiding this comment

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

We have missed an else clause and that results in bugs, the suggestion I made will ensure typescript types will prevent such bugs

traceCellMessage(toExecute.cellExecution.cell, 'Before Execute individual cell');
} else if (toExecute.codeExecution) {
traceInfo('Before Execute hidden code', toExecute.codeExecution.code.substring(0, 50));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We have missed an else clause and that results in bugs, the suggestion I made will ensure typescript types will prevent such bugs

@codecov-commenter
Copy link

codecov-commenter commented Sep 30, 2021

Codecov Report

Merging #7763 (0d76176) into main (f56ba6e) will decrease coverage by 0%.
The diff coverage is 69%.

❗ Current head 0d76176 differs from pull request most recent head 3876790. Consider uploading reports for the commit 3876790 to get more accurate results

@@          Coverage Diff           @@
##            main   #7763    +/-   ##
======================================
- Coverage     68%     68%    -1%     
======================================
  Files        363     364     +1     
  Lines      22593   22714   +121     
  Branches    3437    3456    +19     
======================================
+ Hits       15530   15607    +77     
- Misses      5725    5756    +31     
- Partials    1338    1351    +13     
Impacted Files Coverage Δ
...lient/datascience/jupyter/kernels/cellExecution.ts 76% <0%> (ø)
src/client/datascience/jupyter/kernels/types.ts 100% <ø> (ø)
...lient/datascience/jupyter/kernels/codeExecution.ts 51% <51%> (ø)
...ent/datascience/jupyter/kernels/kernelExecution.ts 69% <80%> (+<1%) ⬆️
src/client/datascience/jupyter/kernels/kernel.ts 75% <87%> (+<1%) ⬆️
.../datascience/jupyter/kernels/cellExecutionQueue.ts 90% <91%> (+2%) ⬆️
...atascience/interactive-window/interactiveWindow.ts 53% <100%> (ø)
...t/datascience/notebook/vscodeNotebookController.ts 77% <100%> (ø)
src/client/debugger/jupyter/helper.ts 57% <100%> (ø)
...client/datascience/kernel-launcher/kernelDaemon.ts 56% <0%> (-2%) ⬇️
... and 5 more

if (item.cellExecution) {
void item.cellExecution.cancel(forced);
} else {
void item.codeExecution?.cancel(forced);
Copy link
Contributor

Choose a reason for hiding this comment

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

We must await, that's why we have promise.all


return Promise.all(cellsToCheck.map((cell) => cell.result));
}
public async waitForHiddenOutput(code: string): Promise<nbformat.IOutput[]> {
Copy link
Contributor

Choose a reason for hiding this comment

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

This won't work if we have multiple expositions with the same code, e.g. pass or the like..
It will return the result from the first matching Execution with the same code

const execution = queue.find((exec) => exec.code === code);

if (execution) {
return Promise.resolve(execution.output);
Copy link
Contributor

Choose a reason for hiding this comment

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

Promise.resolve is unnecessary

}
const cellExecution = this.executionFactory.create(cell, this.metadata);
this.queueOfCellsToExecute.push(cellExecution);
public queueCell(code: NotebookCell | string): Promise<nbformat.IOutput[]> {
Copy link
Author

Choose a reason for hiding this comment

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

I couldn't figure out how to return both types (Promise<NotebookCellRunState[] | nbformat.IOutput[]>) here and then use instanceof nbformat.IOutput[] on kernelExecution.ts.

If you do, let me know @DonJayamanne

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd created two methods

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, and as is the naming is quite awkward here at least in my view. It's called queueCell, but it takes a parameter called 'code' now not cell. And then the paths below are harder to read and easy to miss whens on. @DonJayamanne 's suggestion is good. If it was one method it would be better with a more explicit name like queueExecution, then casting early to either CodeExecution or CellExecution so that the code after that is easier to read, but two methods sounds like a legit idea.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 on a separate method

@@ -7,7 +7,7 @@ import { IKernelDebugAdapterConfig, KernelDebugMode } from '../types';

export async function isUsingIpykernel6OrLater(kernel: IKernel): Promise<boolean> {
const code = 'import ipykernel\nprint(ipykernel.__version__)';
const output = await kernel.executeHidden(code);
const output = await kernel.queueAndExeceuteHidden(code);
Copy link
Contributor

Choose a reason for hiding this comment

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

This should not be queued, only the debugger start method should be queued, all other (older) methods that used hidden Execution b should not have to be queued

Copy link
Author

Choose a reason for hiding this comment

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

Not queuing that is the root of the issue

@@ -132,4 +171,28 @@ export class CellExecutionQueue {
}
}
}

private getCellExecutions(executionList: Execution[]): CellExecution[] {
Copy link
Member

Choose a reason for hiding this comment

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

Not a perf difference I believe, but this could look cleaner just using .filter

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree, personally we don't need these two methods, i'd just move the filtering into the place where we call this method, else we just iterate throught the same set of list twice.

}

/**
* Responsible for execution of an individual cell and manages the state of the cell as it progresses through the execution phases.
Copy link
Member

Choose a reason for hiding this comment

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

This is picky, but that's kinda how I am 😆 . The comments here look copied over from the other class, they should be updated for the new class (i.e. this is not responsible for execution of an individual cell).

* Further details here https://github.com/microsoft/vscode-jupyter/issues/232 & https://github.com/jupyter/jupyter_client/issues/297
*
*/
export class CodeExecution implements IDisposable {
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems weird that we'd need an entirely new class just to execute a string instead of a notebook cell. Could CellExecution not use this instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

Meaning derive from it or use this object internally to do execution?

Copy link
Contributor

@DonJayamanne DonJayamanne Sep 30, 2021

Choose a reason for hiding this comment

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

Valid request, however I think it gets way too messy (lots of if/else), i personally think this is cleaner than trying to get a single class to do both.
Also i don't think execute hidden will require execution of code that has widgets & other complexities.

Copy link
Contributor

Choose a reason for hiding this comment

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

No not a single class. The other class would use this one. This one knows only about code.

The other one uses this class internally to actually do an execute. It just listens to the promise and updates the cell outputs on the cell.

Otherwise we end up with the same code doing iopub listening and the potential for having to fix things in two spots if something is broken.

Copy link
Contributor

Choose a reason for hiding this comment

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

The other class would use this one. This one knows only about code.

I'm still not convinced, this is simple and small, it just gets messy. We don't need widgets, etc, hence this code is sufficient as is.

code doing iopub listening and the potential for having to fix things in two

I'd perfer to refactor when that happens, right now this feels much simpler to me, but thats just me.

@roblourens
Copy link
Member

Please add me as a reviewer on PRs like this so I get a notification. I don't quite follow this though, how does it actually queue RBL?

@DonJayamanne
Copy link
Contributor

how does it actually queue RBL?

Assume we have 10 cell and we're hit run all, then we hit RBL for the last cell, ideally what should happen (i plan on bringing this up in standup to discuss expected behavior) is we should start RBL after the 10 cells.
In jupyter all executions are queued (if you're already running a cell & attempt to run another cell, all that happens is, it gets put into a queue & it will run after all previous executions).
Thus, RBL will happen after all previous pending execution requests.

The down side of this is, RBL could start 10 minutes after you clicked RBL, (i'll be bringing up this issue & a few others related to RBL in standup)
Its possible this solution of queueing is the right solution, we could even just as well disable RBL when we have pending executions... lets discuss in standup

@DavidKutu
Copy link
Author

As we discussed on standup, we'll wait for after release for this. And there's a chance we'll drop this and just not allow run by line/debugging when the kernel is running.

@roblourens
Copy link
Member

@DonJayamanne I get it, you're describing how queueing should work, I don't see how this PR implements that. It seems like individual executions will be queued but not the operation of starting RBL itself. But anyway, like we discussed we can revisit it later

@DonJayamanne
Copy link
Contributor

I think we should re-vist this, I"m actually keen on taking some of this work, as it ensures hidden cells & others all go through the same queu, there are times when they must (& that's what this PR was trying to address)

Will leave open for now, will create an issue so we track this as an engineering/debt task

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants