Conditional Lumino plugin load
fcollonval opened this issue · comments
This is a draft that requires refinement.
To reduce the start-up time of JupyterLab, we could be smarter about plugin loading logic. For now, a Lumino plugin can be started automatically or not using the autoStart flag. But there is no way to specify conditions for which a plugin should start.
Before laying down a plan for Lumino plugin, here is an analysis of VS Code extension API that allows to conditionally load plugins. There are two key points:
- Entry points must be defined in a no-code way
- Trigger events must be defined in a no-code way
In VS Code, those elements are defined in the extension package.json. The entry points are for example (see contributions documentation):
- Commands
- Menus
- Views
The trigger events are for example (see activation events documentation):
onCommand
: When a command of the extension is triggeredonView
: When a specific view is restoredonStartupFinished
: After the application has started
The side effect of defining the contributed element in a no-code format raises the need to have an evaluable string to determine if an element is activable or not. This is calledwhen
clause in VS Code.
Back to JupyterLab and Lumino, the equivalent of onStartupFinished
could be implemented quickly. But it may result in unwanted side effects; e.g. if the plugin is providing a widget and that widget is used in a notebook output that is loaded at startup, the widget may or may not be rendered. To ensure robustness, other events should be available; for example the widget plugin should load before opening a notebook.
Plugins adding new panels in the main area or in the side panel, like jupyterlab-git, are probably the best candidates for optimization. This requires defining side panels and main area widgets in a no-code way. In JupyterLab, we introduced the definition of keyboard shortcuts and menus in setting schema files. So it seems a more natural place than the extension package.json
.
To be able to add a panel, widget titles need to be defined in a no-code way. This requires in particular the definition of icons in a no-code way.
Some first actions could be:
- Extend
autoStart
to be a boolean or ‘"defer"’ (done in #588) - We need to figure out how we could load the settings defining the entry points and the triggered events without loading the JavaScript code. A possibility would be to define entry points as we do for keyboard shortcuts or menus; i.e. in the settings file definitions. The entry points / no-code definitions in order of implementation would be:
- Icon definition (needed for commands, widget title, file type,...)
- Command definition
- Panel definition (via the widget title to be displayed as placeholder in sidebar for example).
- File type definition (needed for document factory)
- Document factory
Thank you for opening your first issue in this project! Engagement like this is essential for open source projects! 🤗
If you haven't done so already, check out Jupyter's Code of Conduct. Also, please try to follow the issue template as it helps other other community members to contribute more effectively.
You can meet the other Jovyans by joining our Discourse forum. There is also an intro thread there where you can stop by and say Hi! 👋
Welcome to the Jupyter community! 🎉
Discussion notes: https://hackmd.io/DBlDBDszSVSjYK9ieg1mZA
More thoughts about this for the GSoC project that will be implemented in JupyterLab repo directly - on a specific branch.
Note
This is a proposal to help kicking off an implementation
Vocabulary
What is a plugin entrypoint?
An plugin entrypoint is no-code model defined as JSON that describes enough about a type of application feature to create a placeholder in the user interface.Then if the user interact with the placeholder, the JavaScript of the plugin will be loaded and its activate function will be called to replace all placeholders (for that plugin) with the real implementation.
What is a plugin?
It is object providing features in the application.What is an extension?
It is a NPM package providing one or more plugins.Generic logic
A type of application feature is defined by an object provided by a plugin through a Token
or an attribute of the Application
. Other plugins may request that token to add a variant for that feature. For example, a application feature is the command that can be added through the CommandRegistry
. Another example is a document viewer that can be added through the DocRegistry
in JupyterLab. In the following I will name registry an object allowing to add a type of application feature.
Therefore the generic logic to use entrypoints is the following:
- List entrypoints per plugins from all extension package.json
- Store those entrypoints in the
Application
- Registries supporting entrypoints will look at the entrypoints defined in the
Application
- For each supported entrypoint, they will add a placeholder
- Each placeholder should be stored in the
Application
. So that the application can disposed them before activating the associated plugin. - When an user triggered a placeholder:
- will load the plugin assets
- activate the plugin
A specific case
Let take the example of a new document viewer for CSV (interface IWidgetFactory) that is added to the DocRegistry using addWidgetFactory.
If the JSON schema of IWidgetFactory
looks like:
{
defaultFor?: readonly string[];
defaultRendered?: readonly string[];
fileTypes: readonly string[];
label?: string;
modelName?: string;
name: string;
}
The placeholder interface will be something like:
interface IPlaceholder extends IObservableDisposable {}
The idea of using
IObservableDisposable
is to automatically trigger loading and activating the plugin by theApplication
when it is notified that a placeholder is disposed.
But a dedicated method inIPlaceholder
that returns a Promise resolving when the plugin is activated may be more appropriate to know when the actual call to the feature can be made.
The example could be
{
"name": "CSVTable",
"label": "CSV Viewer",
"fileTypes": ["csv"],
"defaultFor": ["csv"]
}
class WidgetFactoryPlaceholder implements IWidgetFactory, IPlaceholder {
constructor(entrypoint: IWidgetFactoryPlaceholder) {
// ...
}
createNew() {
// This is actually this method that when triggered should load the plugin
}
}
The DocRegistry
would add the placeholder:
app.docRegistry.addWidgetFactory(new WidgetFactoryPlaceholder(entrypoint));
Note
One of the technical difficulty is to be able to redo the action triggered on the placeholder with the real code once the plugin is loaded. For the aforementioned example, this materializes in how to trigger the widget factory createNew
method from the real code.