Explore extension isolation#318146
Draft
alexdima wants to merge 6 commits into
Draft
Conversation
…itives Add the foundational communication layer for the worker-per-extension isolation runtime: - WorkerProtocol: wire format (Request/Response/Notification/Cancel) and IWorkerLike abstraction for testability - WorkerConnection: supervisor-side RPC with request/response, notifications, cancellation token forwarding, and timeout support - WorkerConnectionClient: worker-side mirror using parentPort - WorkerRegistry: manages worker lifecycle per ExtensionIdentifier with injectable WorkerFactory Tests use FakeWorker (in-memory async message passing) since Electron's V8 does not support worker_threads.Worker construction. 24 tests, 0 failures, 0 disposable leaks.
Introduce `extensions.experimental.workerIsolated` setting that routes configured extensions to a dedicated LocalProcess affinity backed by a worker-isolated extension host (not yet implemented — returns null). Design: reuse the existing affinity mechanism rather than adding a new ExtensionHostKind enum value. The supervisor process is a local process from the main thread's perspective; worker-per-extension isolation is an internal implementation detail. This keeps the change to 3 production files with zero impact on existing switch/if-else chains. - ExtensionRunningLocationTracker._computeAffinity() reads the setting and assigns matching extensions to a shared isolated affinity - isWorkerIsolatedLocalProcessAffinity() exposed for the factory - NativeExtensionHostFactory checks this in createExtensionHost() - Configuration schema registered with type validation - Setting key exported as EXTENSIONS_WORKER_ISOLATED_CONFIGURATION_KEY
…hase 2) Extensions configured in `extensions.experimental.workerIsolated` are routed to a dedicated LocalProcess affinity (Phase 1). This phase adds the plumbing so the extension host process knows it's in worker-isolated mode: - Add `workerIsolated?: boolean` to `IExtensionHostInitData` - Add `_isWorkerIsolated` constructor parameter to `NativeLocalProcessExtensionHost` that sets the init data field - Factory passes the flag from the affinity check The extension host starts normally via `ExtensionHostMain`. The flag is the hook for Phase 3, which will override activation in `AbstractExtHostExtensionService` to spawn worker threads instead of loading extensions in-process.
Phase 0-3 of extension isolation. Extensions listed in
`extensions.experimental.workerIsolated` are activated in dedicated
`worker_threads` instead of in-process.
Architecture:
- RPCProtocol reused over MessagePortProtocol (zero-copy ArrayBuffer transfer)
- Two parallel identifier hierarchies (WorkerHostIdentifier, WorkerClientIdentifier)
with own static counters, independent of ProxyIdentifier
- RPCProtocol constructor now requires explicit RPCProtocolOptions
{ identifierCount, getStringIdentifier } — no implicit defaults
- Per-worker DI-injectable host object (WorkerExtHostSupervisorHost)
- WorkerExtensionHost owns full worker lifecycle (spawn, protocol, exit)
- extensionWorkerBootstrap.ts in api/node/ creates proxied vscode API
- ExtensionContext fully typed with TODO@isolation markers
Settings:
- `extensions.experimental.workerIsolated`: string[] of extension IDs
- `extensions.experimental.workerIsolatedSeparateProcess`: when true,
isolated extensions get a separate ext host process (default: false,
workers spawn in the main ext host process)
Init data carries `workerIsolatedExtensions?: string[]` — every
LocalProcess ext host receives the full list, per-extension check
via ExtensionIdentifier.equals() at activation time.
Contributor
There was a problem hiding this comment.
Pull request overview
This PR explores “extension isolation” by introducing a worker-per-extension execution path inside the Node extension host process, plus the supporting inner RPC plumbing, configuration routing, and unit tests.
Changes:
- Added a worker ↔ supervisor RPC protocol and
WorkerExtensionHostto spawn aworker_threadsworker and activate an extension inside it. - Plumbed
extensions.experimental.workerIsolated(andworkerIsolatedSeparateProcess) through running-location tracking, extension host creation, and extension host init data. - Updated
RPCProtocolto require explicit identifier metadata (identifierCount,getStringIdentifier) and added new tests for the worker-isolated pieces.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/services/extensions/test/node/workerIsolated/workerExtensionHost.test.ts | New unit tests for worker-based activation and command/message plumbing. |
| src/vs/workbench/services/extensions/test/node/workerIsolated/workerConnection.test.ts | New unit tests for the supervisor-side WorkerConnection request/notify behavior. |
| src/vs/workbench/services/extensions/test/common/rpcProtocol.test.ts | Updated tests to construct RPCProtocol with explicit identifier metadata. |
| src/vs/workbench/services/extensions/test/common/extensionRunningLocationTracker.test.ts | Added tests for worker-isolated affinity routing behavior. |
| src/vs/workbench/services/extensions/node/workerIsolated/workerExtensionHost.ts | New worker lifecycle/activation manager built on RPCProtocol over MessagePortProtocol. |
| src/vs/workbench/services/extensions/node/workerIsolated/workerConnectionClient.ts | New worker-side request/notify client over parentPort. |
| src/vs/workbench/services/extensions/node/workerIsolated/workerConnection.ts | New supervisor-side request/notify connection abstraction over IWorkerLike. |
| src/vs/workbench/services/extensions/ext-isolation-containment-plan.md | Design/plan document describing phased implementation and goals. |
| src/vs/workbench/services/extensions/electron-browser/nativeExtensionService.ts | Plumbs configured worker-isolated extension IDs into local process extension host creation. |
| src/vs/workbench/services/extensions/electron-browser/localProcessExtensionHost.ts | Includes workerIsolatedExtensions list in extension host init data. |
| src/vs/workbench/services/extensions/common/workerIsolated/workerProtocol.ts | New common wire-protocol types and IWorkerLike abstraction. |
| src/vs/workbench/services/extensions/common/workerIsolated/workerExtHostProtocol.ts | New worker-side proxy identifier hierarchy and activation/message/command shapes. |
| src/vs/workbench/services/extensions/common/workerIsolated/messagePortProtocol.ts | Bridge from message ports/workers to IMessagePassingProtocol for RPCProtocol. |
| src/vs/workbench/services/extensions/common/rpcProtocol.ts | RPCProtocol now requires RPCProtocolOptions to define identifier space and debug naming. |
| src/vs/workbench/services/extensions/common/extensionRunningLocationTracker.ts | Adds worker-isolated configuration keys and affinity routing support. |
| src/vs/workbench/services/extensions/common/extensionHostProtocol.ts | Adds workerIsolatedExtensions?: string[] to init data. |
| src/vs/workbench/services/extensions/common/extensionHostManager.ts | Updates RPCProtocol construction to pass identifier metadata. |
| src/vs/workbench/contrib/extensions/browser/extensions.contribution.ts | Registers extensions.experimental.workerIsolated and workerIsolatedSeparateProcess settings. |
| src/vs/workbench/api/node/workerExtHostSupervisorHost.ts | New DI-hosted supervisor-side implementation for worker commands and messages. |
| src/vs/workbench/api/node/extHostExtensionService.ts | Activates selected extensions via WorkerExtensionHost when configured. |
| src/vs/workbench/api/node/extensionWorkerBootstrap.ts | Worker entrypoint to load an extension and proxy minimal vscode APIs back to supervisor. |
| src/vs/workbench/api/common/extHostExtensionService.ts | Makes _doActivateExtension overridable for node-specific worker activation path. |
| src/vs/workbench/api/common/extensionHostMain.ts | Updates RPCProtocol construction to pass identifier metadata and URI transformer. |
Copilot's findings
- Files reviewed: 23/23 changed files
- Comments generated: 11
Comment on lines
+108
to
+112
| const worker = this._workerFactory(bootstrapPath); | ||
|
|
||
| const extensionDisposables = this._register(new DisposableStore()); | ||
|
|
||
| // Ensure worker is terminated on disposal |
Comment on lines
+200
to
+203
| const configurationService = new TestConfigurationService(); | ||
| configurationService.setUserConfiguration('extensions.experimental.affinity', configuredAffinities); | ||
| configurationService.setUserConfiguration(EXTENSIONS_WORKER_ISOLATED_CONFIGURATION_KEY, workerIsolatedIds); | ||
|
|
Comment on lines
+284
to
+286
| // Give async delivery time | ||
| await new Promise(resolve => setTimeout(resolve, 10)); | ||
|
|
Comment on lines
+51
to
+54
| async $unregisterCommand(_commandId: string): Promise<void> { | ||
| // Disposal is handled by the DisposableStore via _register above. | ||
| // When this host is disposed, all registered commands are cleaned up. | ||
| } |
Comment on lines
+62
to
+64
| async $showInformationMessage(message: string): Promise<unknown> { | ||
| return this._messageProxy.$showMessage(1 /* Severity.Info */, message, {}, []); | ||
| } |
Comment on lines
+27
to
+30
| private readonly _requestHandlers = new Map<string, (...args: unknown[]) => Promise<unknown>>(); | ||
| private readonly _notificationHandlers = new Map<string, (...args: unknown[]) => void>(); | ||
| private readonly _cancelHandlers = new Map<number, () => void>(); | ||
|
|
| null, | ||
| new ExtensionActivationTimes(reason.startup, result.codeLoadingTime, result.activateCallTime, result.activateResolveTime), | ||
| { activate: undefined, deactivate: () => result.deactivate() }, | ||
| result.hasExports ? {} : undefined, |
Comment on lines
+40
to
+44
| supervisorProxy.$registerCommand(command); | ||
| return { | ||
| dispose() { | ||
| commandHandlers.delete(command); | ||
| supervisorProxy.$unregisterCommand(command); |
Comment on lines
+135
to
+136
| const modulePath = URI.revive(extensionDescription.extensionLocation).fsPath + '/' + entryPoint; | ||
|
|
| assert.ok(registeredCommands.has('test.cleanup')); | ||
|
|
||
| // Disposing the activated extension disposes the host, which cleans up commands | ||
| activated.disposable.dispose(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.