- Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Context
The extension's activation hook (per package.json) is the activate() function in src/client/extension.ts. However, it's difficult to tell what is happening there and when. Basically, this function does quite a few things, in roughly the following order:
- track startup time
- set up the trivial state of the extension
- globals tracking extension lifecycle
- DI objects (e.g.
ServiceManager) - registration of objects with the DI framework
- simple initialization of the state of those objects (complexity limited to doing things in the right order such that the dependency tree is satisfied)
- "activate" some of the objects
- represents the actual work done during extension activation
- includes anything more than simple initialization of the state of the extension's objects
- kinds:
- synchronous (blocks everything else during ext activation but usually quick)
- async but must complete before activation is done
- started in the background (consumers can later wait for the resource to become ready before using it)
- set up to activate lazily later
- examples: register VS Code hooks, start background services
- send telemetry
- set up the extension's public API
The main problems with the status quo are:
activate()(and it's subordinates) isn't clearly structured along those lines- steps 2 and 3 are quite tangled together
- a lot of the work is spread out in many different files (making it hard to identify what is actually happening)
Fixing this will help the project in a variety of ways, including making it easier to reduce the extension's startup time.
Solution
We should restructure activate() and the rest of extension.ts along lines of the steps above. At a high-level the following would happen during activation:
- start displaying "progress" in the status bar
- start a timer
- initialize (simple) extension globals
- initialize (simple) all our objects
- initialize DI (e.g. ServiceManager, ServiceContainer)
- register the objects with DI
- this could be grouped by sub-graph (e.g. by component)
- initialize the objects
- this could be grouped by sub-graph (e.g. by component)
- this could be done either via registration with DI or separately after
- the benefit of "separately" is that it makes a clearer distinction between what is relevant to the object graph and what isn't (it would also make it easier to wean the extension off using a DI framework)
- activation
- stop the timer
- stop displaying "progress"
- send telemetry
- set up the extension's public API
The top-level code would be straightforward and look something like this:
function activate(...) { displayProgress(activationDeferred.promise); startTimer(); const ... = initializeExtension(...); const ... = await activateComponents(...); stopTimer(); activationDeferred.resolve(); await sendTelemetry(); return buildAPI(...); } function initializeExtension(...) { initializeGlobals(...); const ... = initializeDI(...); initializeComponents(...); return ...; } function initializeComponents(...) { ... } async function activateComponents(...) { // XXX Adjust to accomodate dependencies between components? return Promise.All( ... activateEnvs(...), ... activateLinters(...), ... ); }To get there the following must happen in this order:
A. refactor extension.ts as above, with initializeComponents() initially empty and activateComponents() basically the init/activation code currently in activateUnsafe()
B. identify the objects currently involved in activation and where:
- where are "primary" objects instantiated and injected into DI, relative to current extension activation
- which of those objects have non-trivial constructors?
- what other activation is happening, if any, and where?
C. factor DI registration & object init out of existing "activation" code, putting it into initializeComponents()
- this can be split up into many parallel tasks (by-component, by-file, etc.)
- requires that no constructors have side effects or do any work (a good idea regardless)
- will involve digging down through our whole object graph
- will be a lot easier if we consolidate all the DI registration and simple object init into one module
- it will probably make sense to organize this by component
- in some cases it will make sense to pull some of the "activation" code up into extension.ts at the same time
D. (maybe) separate DI registration from object init
E. pull remaining activation code up into extension.ts
- this can be split up into many parallel tasks (by-component, by-file, etc.)
Open Questions
- how much code do we pull up into extension.ts (or cohorts)? We want to be sure it's crystal clear what is going on during activation...
- further split things up?
- scope:
- necessary
- support
- on-demand
- components:
- low-level resources
- horizontal layers (cross-cutting concerns like Python env mgmt)
- vertical layers (e.g. linters, debugging)
- scope: