src/vs/workbench/services/assignment/common/assignmentService.ts

356 LOC · 130 covered · 226 uncovered · 16 ranges · 198 concepts · 1 introducers · 76 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- chatServiceImpl.ts ×75
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import { localize } from '../../../../nls.js';
7 > import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
8 > import type { IKeyValueStorage, IExperimentationTelemetry, ExperimentationService as TASClient } from 'tas-client';
9 > import { Memento } from '../../../common/memento.js';
10 > import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
11 > import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
12 > import { ITelemetryData } from '../../../../base/common/actions.js';
13 > import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
14 > import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
15 > import { IProductService } from '../../../../platform/product/common/productService.js';
16 > import { ASSIGNMENT_REFETCH_INTERVAL, ASSIGNMENT_STORAGE_KEY, AssignmentFilterProvider, IAssignmentService, TargetPopulation, WindowKind } from '../../../../platform/assignment/common/assignment.js';
17 > import { Registry } from '../../../../platform/registry/common/platform.js';
18 > import { workbenchConfigurationNodeBase } from '../../../common/configuration.js';
19 > import { IConfigurationRegistry, Extensions as ConfigurationExtensions, ConfigurationScope } from '../../../../platform/configuration/common/configurationRegistry.js';
20 > import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
21 > import { importAMDNodeModule } from '../../../../amdX.js';
22 > import { timeout } from '../../../../base/common/async.js';
23 > import { StopWatch } from '../../../../base/common/stopwatch.js';
24 > import { CopilotAssignmentFilterProvider } from './assignmentFilters.js';
25 > import { AssignmentContextFilter } from './assignmentContextFilter.js';
26 > import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
27 > import { Emitter, Event } from '../../../../base/common/event.js';
28 > import { experimentsEnabled } from '../../telemetry/common/workbenchTelemetryUtils.js';
29 >
30 > export interface IAssignmentFilter {
31 > /**
32 > * Stable identifier for this filter. Used to persist and reconcile the set of
33 > * assignment-context ids this filter has excluded, independently of other filters.
34 > */
35 > readonly id: string;
36 > exclude(assignment: string): boolean;
37 > onDidChange: Event<void>;
38 > }
39 >
40 > export const IWorkbenchAssignmentService = createDecorator<IWorkbenchAssignmentService>('assignmentService');
41 >
42 > export interface IWorkbenchAssignmentService extends IAssignmentService {
43 > getCurrentExperiments(): Promise<string[] | undefined>;
44 > addTelemetryAssignmentFilter(filter: IAssignmentFilter): void;
45 > }
46 >
47 > class MementoKeyValueStorage implements IKeyValueStorage {
48 >
49 > private readonly mementoObj: Record<string, unknown>;
50 >
51 > constructor(private readonly memento: Memento<Record<string, unknown>>) {
52 this.mementoObj = memento.getMemento(StorageScope.APPLICATION, StorageTarget.MACHINE);
53 }
55 > async getValue<T>(key: string, defaultValue?: T | undefined): Promise<T | undefined> {
56 const value = await this.mementoObj[key] as T | undefined;
57
58 return value || defaultValue;
59 }
61 > setValue<T>(key: string, value: T): void {
62 this.mementoObj[key] = value;
63 this.memento.saveMemento();
64 }
66 >
67 > class WorkbenchAssignmentServiceTelemetry extends Disposable implements IExperimentationTelemetry {
68 >
69 > private readonly _onDidUpdateAssignmentContext = this._register(new Emitter<void>());
70 > readonly onDidUpdateAssignmentContext = this._onDidUpdateAssignmentContext.event;
71 >
72 > private _previousAssignmentContext: string | undefined;
73 > private _lastAssignmentContext: string | undefined;
74 > get assignmentContext(): string[] | undefined {
75 > return this._lastAssignmentContext?.split(';');
76 > }
77 >
78 > constructor(
79 private readonly telemetryService: ITelemetryService,
80 private readonly productService: IProductService,
81 private readonly contextFilter: AssignmentContextFilter
82 ) {
83 super();
84
85 // Re-apply the filters whenever a filter is added or changes its exclusion decisions.
86 this._register(this.contextFilter.onDidChange(() => {
87 if (this._previousAssignmentContext) {
88 this._setAssignmentContext(this._previousAssignmentContext);
89 }
90 }));
91 }
93 > private _setAssignmentContext(value: string): void {
94 const filteredValue = this.contextFilter.filter(value);
95 this._lastAssignmentContext = filteredValue;
96 this._onDidUpdateAssignmentContext.fire();
97
98 if (this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {
99 this.telemetryService.setExperimentProperty(this.productService.tasConfig.assignmentContextTelemetryPropertyName, filteredValue);
100 }
101 }
103 > // __GDPR__COMMON__ "abexp.assignmentcontext" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
104 > setSharedProperty(name: string, value: string): void {
105 if (name === this.productService.tasConfig?.assignmentContextTelemetryPropertyName) {
106 this._previousAssignmentContext = value;
107 return this._setAssignmentContext(value);
108 }
109
110 this.telemetryService.setExperimentProperty(name, value);
111 }
113 > postEvent(eventName: string, props: Map<string, string>): void {
114 const data: ITelemetryData = {};
115 for (const [key, value] of props.entries()) {
116 data[key] = value;
117 }
118
119 /* __GDPR__
120 "query-expfeature" : {
121 "owner": "sbatten",
122 "comment": "Logs queries to the experiment service by feature for metric calculations",
123 "ABExp.queriedFeature": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The experimental feature being queried" }
124 }
125 */
126 this.telemetryService.publicLog(eventName, data);
127 }
129 >
130 > export class WorkbenchAssignmentService extends Disposable implements IAssignmentService {
131 >
132 > declare readonly _serviceBrand: undefined;
133 >
134 > private readonly tasClient: Promise<TASClient> | undefined;
135 > private readonly tasSetupDisposables = new DisposableStore();
136 >
137 > private networkInitialized = false;
138 > private readonly overrideInitDelay: Promise<void>;
139 >
140 > private readonly contextFilter: AssignmentContextFilter;
141 > private readonly telemetry: WorkbenchAssignmentServiceTelemetry;
142 > private readonly keyValueStorage: IKeyValueStorage;
143 >
144 > private readonly experimentsEnabled: boolean;
145 >
146 > private readonly _onDidRefetchAssignments = this._register(new Emitter<void>());
147 > public readonly onDidRefetchAssignments = this._onDidRefetchAssignments.event;
148 >
149 > constructor(
150 @ITelemetryService private readonly telemetryService: ITelemetryService,
151 @IStorageService storageService: IStorageService,
152 @IConfigurationService private readonly configurationService: IConfigurationService,
153 @IProductService private readonly productService: IProductService,
154 @IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
155 @IInstantiationService private readonly instantiationService: IInstantiationService,
156 ) {
157 super();
158
159 this.experimentsEnabled = experimentsEnabled(configurationService, productService, this.environmentService);
160
161 if (this.experimentsEnabled) {
162 this.tasClient = this.setupTASClient();
163 }
164
165 this.contextFilter = this._register(new AssignmentContextFilter(storageService));
166 this.telemetry = this._register(new WorkbenchAssignmentServiceTelemetry(telemetryService, productService, this.contextFilter));
167 this._register(this.telemetry.onDidUpdateAssignmentContext(() => this._onDidRefetchAssignments.fire()));
168 this._register(this.configurationService.onDidChangeConfiguration(e => {
169 if (e.affectsConfiguration('experiments.override')) {
170 this._onDidRefetchAssignments.fire();
171 }
172 }));
173
174 this.keyValueStorage = new MementoKeyValueStorage(new Memento<Record<string, unknown>>('experiment.service.memento', storageService));
175
176 // For development purposes, configure the delay until tas local tas treatment ovverrides are available
177 const overrideDelaySetting = configurationService.getValue('experiments.overrideDelay');
178 const overrideDelay = typeof overrideDelaySetting === 'number' ? overrideDelaySetting : 0;
179 this.overrideInitDelay = timeout(overrideDelay);
180 }
182 > async getTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
183 const result = await this.doGetTreatment<T>(name);
184
185 type TASClientReadTreatmentData = {
186 treatmentName: string;
187 treatmentValue: string;
188 };
189
190 type TASClientReadTreatmentClassification = {
191 owner: 'sbatten';
192 comment: 'Logged when a treatment value is read from the experiment service';
193 treatmentValue: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The value of the read treatment' };
194 treatmentName: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The name of the treatment that was read' };
195 };
196
197 this.telemetryService.publicLog2<TASClientReadTreatmentData, TASClientReadTreatmentClassification>('tasClientReadTreatmentComplete', {
198 treatmentName: name,
199 treatmentValue: JSON.stringify(result)
200 });
201
202 return result;
203 }
205 > private async doGetTreatment<T extends string | number | boolean>(name: string): Promise<T | undefined> {
206 await this.overrideInitDelay; // For development purposes, allow overriding tas assignments to test variants locally.
207
208 const override = this.configurationService.getValue<T>(`experiments.override.${name}`);
209 if (override !== undefined) {
210 return override;
211 }
212
213 if (!this.tasClient) {
214 return undefined;
215 }
216
217 if (!this.experimentsEnabled) {
218 return undefined;
219 }
220
221 let result: T | undefined;
222 const client = await this.tasClient;
223
224 // The TAS client is initialized but we need to check if the initial fetch has completed yet
225 // If it is complete, return a cached value for the treatment
226 // If not, use the async call with `checkCache: true`. This will allow the module to return a cached value if it is present.
227 // Otherwise it will await the initial fetch to return the most up to date value.
228 if (this.networkInitialized) {
229 result = client.getTreatmentVariable<T>('vscode', name);
230 } else {
231 result = await client.getTreatmentVariableAsync<T>('vscode', name, true);
232 }
233
234 result = client.getTreatmentVariable<T>('vscode', name);
235 return result;
236 }
238 > private async setupTASClient(): Promise<TASClient> {
239 this.tasSetupDisposables.clear();
240
241 const targetPopulation = this.productService.quality === 'stable' ?
242 TargetPopulation.Public : (this.productService.quality === 'exploration' ?
243 TargetPopulation.Exploration : TargetPopulation.Insiders);
244
245 const filterProvider = new AssignmentFilterProvider(
246 this.productService.version,
247 this.productService.nameLong,
248 this.telemetryService.machineId,
249 this.telemetryService.devDeviceId,
250 targetPopulation,
251 this.productService.date ?? '',
252 this.environmentService.isSessionsWindow ? WindowKind.Agents : WindowKind.Editor
253 );
254
255 const extensionsFilterProvider = this.instantiationService.createInstance(CopilotAssignmentFilterProvider);
256 this.tasSetupDisposables.add(extensionsFilterProvider);
257 this.tasSetupDisposables.add(extensionsFilterProvider.onDidChangeFilters(() => this.refetchAssignments()));
258
259 const tasConfig = this.productService.tasConfig!;
260
261 const tasClientModule = await importAMDNodeModule<typeof import('tas-client')>('tas-client', 'dist/tas-client.min.js');
262
263 // Measure the client-side latency of the first network call to the
264 // Treatment Assignment Service. The fetch is triggered by constructing
265 // the client, so start timing right before construction to exclude
266 // module loading time from the measurement.
267 const fetchStopWatch = StopWatch.create();
268 const tasClient = new tasClientModule.ExperimentationService({
269 filterProviders: [filterProvider, extensionsFilterProvider],
270 telemetry: this.telemetry,
271 storageKey: ASSIGNMENT_STORAGE_KEY,
272 keyValueStorage: this.keyValueStorage,
273 assignmentContextTelemetryPropertyName: tasConfig.assignmentContextTelemetryPropertyName,
274 telemetryEventName: tasConfig.telemetryEventName,
275 endpoint: tasConfig.endpoint,
276 refetchInterval: ASSIGNMENT_REFETCH_INTERVAL,
277 });
278
279 await tasClient.initializePromise;
280 tasClient.initialFetch.then(() => {
281 this.networkInitialized = true;
282 this.logFetchLatency('initial', fetchStopWatch.elapsed());
283 }).catch(() => undefined);
284
285 return tasClient;
286 }
288 > private logFetchLatency(fetchType: 'initial' | 'refetch', durationMs: number): void {
289 type TASClientFetchLatencyData = {
290 fetchType: string;
291 durationMs: number;
292 };
293
294 type TASClientFetchLatencyClassification = {
295 owner: 'sbatten';
296 comment: 'Measures the client-side latency of fetching treatment assignments from the experiment service (TAS)';
297 fetchType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether this was the initial fetch or a refetch' };
298 durationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Time in milliseconds the fetch took to complete' };
299 };
300
301 this.telemetryService.publicLog2<TASClientFetchLatencyData, TASClientFetchLatencyClassification>('tasClientFetchLatency', {
302 fetchType,
303 durationMs
304 });
305 }
307 > private async refetchAssignments(): Promise<void> {
308 if (!this.tasClient) {
309 return; // Setup has not started, assignments will use latest filters
310 }
311
312 // Await the client to be setup and the initial fetch to complete
313 const tasClient = await this.tasClient;
314 await tasClient.initialFetch;
315
316 // Refresh the assignments and measure the network latency of the refetch.
317 const refetchStopWatch = StopWatch.create();
318 await tasClient.getTreatmentVariableAsync('vscode', 'refresh', false);
319 this.logFetchLatency('refetch', refetchStopWatch.elapsed());
320 }
322 > async getCurrentExperiments(): Promise<string[] | undefined> {
323 if (!this.tasClient) {
324 return undefined;
325 }
326
327 if (!this.experimentsEnabled) {
328 return undefined;
329 }
330
331 await this.tasClient;
332
333 return this.telemetry.assignmentContext;
334 }
336 > addTelemetryAssignmentFilter(filter: IAssignmentFilter): void {
337 this.contextFilter.addFilter(filter);
338 }
340 >
341 > registerSingleton(IWorkbenchAssignmentService, WorkbenchAssignmentService, InstantiationType.Delayed);
342 >
343 > const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
344 > registry.registerConfiguration({
345 > ...workbenchConfigurationNodeBase,
346 > 'properties': {
347 > 'workbench.enableExperiments': {
348 > 'type': 'boolean',
349 > 'description': localize('workbench.enableExperiments', "Fetches experiments to run from a Microsoft online service."),
350 > 'default': true,
351 > 'scope': ConfigurationScope.APPLICATION,
352 > 'restricted': true,
353 > 'tags': ['usesOnlineServices']
354 > }
355 > }
356 > });