Skip to content

Explore extension isolation#318146

Draft
alexdima wants to merge 6 commits into
mainfrom
excessive-crab
Draft

Explore extension isolation#318146
alexdima wants to merge 6 commits into
mainfrom
excessive-crab

Conversation

@alexdima
Copy link
Copy Markdown
Member

No description provided.

alexdima added 6 commits May 22, 2026 20:11
…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.
Copilot AI review requested due to automatic review settings May 24, 2026 06:41
@alexdima alexdima self-assigned this May 24, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 WorkerExtensionHost to spawn a worker_threads worker and activate an extension inside it.
  • Plumbed extensions.experimental.workerIsolated (and workerIsolatedSeparateProcess) through running-location tracking, extension host creation, and extension host init data.
  • Updated RPCProtocol to 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();
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.

2 participants