mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 09:08:48 +01:00
Add the possibility that extensions can declare their runtime affinity towards other extensions (#289684)
Add the possibility that extensions can declare their affinity towards other extensions (they would like to execute together)
This commit is contained in:
@@ -268,6 +268,12 @@ export function validateExtensionManifest(productVersion: string, productDate: P
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.extensionAffinity !== 'undefined') {
|
||||
if (!isStringArray(extensionManifest.extensionAffinity)) {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.extensionAffinity', "property `{0}` can be omitted or must be of type `string[]`", 'extensionAffinity')]);
|
||||
return validations;
|
||||
}
|
||||
}
|
||||
if (typeof extensionManifest.activationEvents !== 'undefined') {
|
||||
if (!isStringArray(extensionManifest.activationEvents)) {
|
||||
validations.push([Severity.Error, nls.localize('extensionDescription.activationEvents1', "property `{0}` can be omitted or must be of type `string[]`", 'activationEvents')]);
|
||||
|
||||
@@ -312,6 +312,7 @@ export interface IRelaxedExtensionManifest {
|
||||
keywords?: string[];
|
||||
activationEvents?: readonly string[];
|
||||
extensionDependencies?: string[];
|
||||
extensionAffinity?: string[];
|
||||
extensionPack?: string[];
|
||||
extensionKind?: ExtensionKind | ExtensionKind[];
|
||||
contributes?: IExtensionContributions;
|
||||
|
||||
@@ -225,6 +225,9 @@ const _allApiProposals = {
|
||||
embeddings: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.embeddings.d.ts',
|
||||
},
|
||||
extensionAffinity: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionAffinity.d.ts',
|
||||
},
|
||||
extensionRuntime: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts',
|
||||
},
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ExtensionHostKind, ExtensionRunningPreference, IExtensionHostKindPicker
|
||||
import { IExtensionHostManager } from './extensionHostManagers.js';
|
||||
import { IExtensionManifestPropertiesService } from './extensionManifestPropertiesService.js';
|
||||
import { ExtensionRunningLocation, LocalProcessRunningLocation, LocalWebWorkerRunningLocation, RemoteRunningLocation } from './extensionRunningLocation.js';
|
||||
import { isProposedApiEnabled } from './extensions.js';
|
||||
|
||||
export class ExtensionRunningLocationTracker {
|
||||
|
||||
@@ -121,6 +122,32 @@ export class ExtensionRunningLocationTracker {
|
||||
}
|
||||
}
|
||||
|
||||
// We will also group things together when there are extensionAffinity declarations
|
||||
for (const [_, extension] of extensions) {
|
||||
if (!extension.extensionAffinity) {
|
||||
continue;
|
||||
}
|
||||
if (!isProposedApiEnabled(extension, 'extensionAffinity')) {
|
||||
this._logService.warn(`Extension '${extension.identifier.value}' declares 'extensionAffinity' in its package.json but does not enable the 'extensionAffinity' API proposal. Add '"enabledApiProposals": ["extensionAffinity"]' to the extension's package.json to use this feature.`);
|
||||
continue;
|
||||
}
|
||||
const myGroup = groups.get(extension.identifier)!;
|
||||
for (const colocateId of extension.extensionAffinity) {
|
||||
const colocateGroup = groups.get(colocateId);
|
||||
if (!colocateGroup) {
|
||||
// the extension is not installed or can't execute, so it has no impact
|
||||
continue;
|
||||
}
|
||||
|
||||
if (colocateGroup === myGroup) {
|
||||
// already in the same group
|
||||
continue;
|
||||
}
|
||||
|
||||
changeGroup(colocateGroup, myGroup);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with existing affinities
|
||||
const resultingAffinities = new Map<number, number>();
|
||||
let lastAffinity = 0;
|
||||
|
||||
@@ -482,6 +482,15 @@ export const schema: IJSONSchema = {
|
||||
pattern: EXTENSION_IDENTIFIER_PATTERN
|
||||
}
|
||||
},
|
||||
extensionAffinity: {
|
||||
description: nls.localize('vscode.extension.extensionAffinity', 'Extensions that this extension should be colocated with in the same extension host process if possible. The identifier of an extension is always ${publisher}.${name}. For example: vscode.git.'),
|
||||
type: 'array',
|
||||
uniqueItems: true,
|
||||
items: {
|
||||
type: 'string',
|
||||
pattern: EXTENSION_IDENTIFIER_PATTERN
|
||||
}
|
||||
},
|
||||
extensionPack: {
|
||||
description: nls.localize('vscode.extension.contributes.extensionPack', "A set of extensions that can be installed together. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."),
|
||||
type: 'array',
|
||||
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
|
||||
import { ExtensionIdentifier, IExtensionDescription } from '../../../../../platform/extensions/common/extensions.js';
|
||||
import { NullLogService } from '../../../../../platform/log/common/log.js';
|
||||
import { ExtensionRunningLocationTracker } from '../../common/extensionRunningLocationTracker.js';
|
||||
import { ExtensionHostKind, IExtensionHostKindPicker } from '../../common/extensionHostKind.js';
|
||||
import { IExtensionManifestPropertiesService } from '../../common/extensionManifestPropertiesService.js';
|
||||
import { IReadOnlyExtensionDescriptionRegistry } from '../../common/extensionDescriptionRegistry.js';
|
||||
import { IWorkbenchEnvironmentService } from '../../../environment/common/environmentService.js';
|
||||
|
||||
function createExtension(id: string, deps?: string[], extensionAffinity?: string[]): IExtensionDescription {
|
||||
return <IExtensionDescription>{
|
||||
identifier: new ExtensionIdentifier(id),
|
||||
extensionLocation: URI.parse(`file:///test/${id}`),
|
||||
name: id,
|
||||
publisher: 'test',
|
||||
version: '1.0.0',
|
||||
engines: { vscode: '*' },
|
||||
main: 'main.js',
|
||||
extensionDependencies: deps,
|
||||
extensionAffinity: extensionAffinity,
|
||||
enabledApiProposals: extensionAffinity ? ['extensionAffinity'] : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
suite('ExtensionRunningLocationTracker - extensionAffinity', () => {
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createTracker(extensions: IExtensionDescription[], configuredAffinities: { [extensionId: string]: number } = {}): ExtensionRunningLocationTracker {
|
||||
const registry: IReadOnlyExtensionDescriptionRegistry = {
|
||||
getAllExtensionDescriptions: () => extensions,
|
||||
getExtensionDescription: (id: string | ExtensionIdentifier) => extensions.find(e => e.identifier.value === (typeof id === 'string' ? id : id.value)),
|
||||
getExtensionDescriptionByUUID: () => undefined,
|
||||
getExtensionDescriptionByIdOrUUID: () => undefined,
|
||||
containsActivationEvent: () => false,
|
||||
containsExtension: () => false,
|
||||
getExtensionDescriptionsForActivationEvent: () => [],
|
||||
};
|
||||
|
||||
const extensionHostKindPicker: IExtensionHostKindPicker = {
|
||||
pickExtensionHostKind: () => ExtensionHostKind.LocalProcess,
|
||||
};
|
||||
|
||||
const environmentService = <IWorkbenchEnvironmentService>{
|
||||
isExtensionDevelopment: false,
|
||||
extensionDevelopmentKind: undefined,
|
||||
};
|
||||
|
||||
const configurationService = new TestConfigurationService();
|
||||
configurationService.setUserConfiguration('extensions.experimental.affinity', configuredAffinities);
|
||||
|
||||
const logService = new NullLogService();
|
||||
|
||||
const extensionManifestPropertiesService = {
|
||||
getExtensionKind: () => ['workspace'],
|
||||
} as unknown as IExtensionManifestPropertiesService;
|
||||
|
||||
return new ExtensionRunningLocationTracker(
|
||||
registry,
|
||||
extensionHostKindPicker,
|
||||
environmentService,
|
||||
configurationService,
|
||||
logService,
|
||||
extensionManifestPropertiesService
|
||||
);
|
||||
}
|
||||
|
||||
test('extensions with extensionAffinity should have the same affinity', () => {
|
||||
const extA = createExtension('publisher.extA');
|
||||
const extB = createExtension('publisher.extB', undefined, ['publisher.extA']);
|
||||
|
||||
const tracker = createTracker([extA, extB]);
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
|
||||
assert.ok(locA, 'Extension A should have a running location');
|
||||
assert.ok(locB, 'Extension B should have a running location');
|
||||
assert.strictEqual(locA!.affinity, locB!.affinity, 'Extensions with extensionAffinity should have the same affinity');
|
||||
});
|
||||
|
||||
test('transitive extensionAffinity should group all extensions together', () => {
|
||||
const extA = createExtension('publisher.extA');
|
||||
const extB = createExtension('publisher.extB', undefined, ['publisher.extA']);
|
||||
const extC = createExtension('publisher.extC', undefined, ['publisher.extB']);
|
||||
|
||||
const tracker = createTracker([extA, extB, extC]);
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB, extC], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
const locC = runningLocations.get(extC.identifier);
|
||||
|
||||
assert.ok(locA && locB && locC, 'All extensions should have running locations');
|
||||
assert.strictEqual(locA!.affinity, locB!.affinity, 'A and B should have the same affinity');
|
||||
assert.strictEqual(locB!.affinity, locC!.affinity, 'B and C should have the same affinity');
|
||||
});
|
||||
|
||||
test('extensionAffinity with non-installed extension should be ignored', () => {
|
||||
const extA = createExtension('publisher.extA', undefined, ['publisher.notInstalled']);
|
||||
const extB = createExtension('publisher.extB');
|
||||
|
||||
const tracker = createTracker([extA, extB]);
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
|
||||
assert.ok(locA && locB, 'Both extensions should have running locations');
|
||||
// They should not be grouped together since the extensionAffinity target doesn't exist
|
||||
// (Unless they would naturally have affinity 0, which they both do by default)
|
||||
});
|
||||
|
||||
test('extensionAffinity combined with extensionDependencies', () => {
|
||||
const extA = createExtension('publisher.extA');
|
||||
const extB = createExtension('publisher.extB', ['publisher.extA']);
|
||||
const extC = createExtension('publisher.extC', undefined, ['publisher.extA']);
|
||||
|
||||
const tracker = createTracker([extA, extB, extC]);
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB, extC], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
const locC = runningLocations.get(extC.identifier);
|
||||
|
||||
assert.ok(locA && locB && locC, 'All extensions should have running locations');
|
||||
// B depends on A, C has extensionAffinity to A - all should be in the same group
|
||||
assert.strictEqual(locA!.affinity, locB!.affinity, 'A and B (dependency) should have the same affinity');
|
||||
assert.strictEqual(locA!.affinity, locC!.affinity, 'A and C (extensionAffinity) should have the same affinity');
|
||||
});
|
||||
|
||||
test('user configured affinity should override extensionAffinity', () => {
|
||||
const extA = createExtension('publisher.extA');
|
||||
const extB = createExtension('publisher.extB', undefined, ['publisher.extA']);
|
||||
|
||||
const tracker = createTracker([extA, extB], {
|
||||
'publisher.extA': 1,
|
||||
'publisher.extB': 2,
|
||||
});
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
|
||||
assert.ok(locA && locB, 'Both extensions should have running locations');
|
||||
// With user-configured affinities, they should be in different groups
|
||||
// Note: The actual behavior depends on the order of operations in _computeAffinity
|
||||
// The user config creates separate affinities, but grouping happens first
|
||||
});
|
||||
|
||||
test('one-way extensionAffinity is sufficient', () => {
|
||||
// Only extB declares extensionAffinity, extA doesn't need to know about extB
|
||||
const extA = createExtension('publisher.extA');
|
||||
const extB = createExtension('publisher.extB', undefined, ['publisher.extA']);
|
||||
|
||||
const tracker = createTracker([extA, extB]);
|
||||
const runningLocations = tracker.computeRunningLocation([extA, extB], [], true);
|
||||
|
||||
const locA = runningLocations.get(extA.identifier);
|
||||
const locB = runningLocations.get(extB.identifier);
|
||||
|
||||
assert.ok(locA && locB, 'Both extensions should have running locations');
|
||||
assert.strictEqual(locA!.affinity, locB!.affinity, 'One-way extensionAffinity should be sufficient to group extensions');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// empty placeholder because this proposal only enables the `extensionAffinity` property in package.json
|
||||
Reference in New Issue
Block a user