Skip to content

Commit bd213a7

Browse files
authored
implements ai stats (microsoft#256843)
* implements ai stats
1 parent ec0f0be commit bd213a7

20 files changed

+512
-168
lines changed

src/vs/editor/common/textModelEditSource.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,25 @@ function createEditSource<T extends Record<string, any>>(metadata: T): TextModel
6262
return new TextModelEditSource(metadata as any, privateSymbol) as any;
6363
}
6464

65+
export function isAiEdit(source: TextModelEditSource): boolean {
66+
switch (source.metadata.source) {
67+
case 'inlineCompletionAccept':
68+
case 'inlineCompletionPartialAccept':
69+
case 'inlineChat.applyEdits':
70+
case 'Chat.applyEdits':
71+
return true;
72+
}
73+
return false;
74+
}
75+
76+
export function isUserEdit(source: TextModelEditSource): boolean {
77+
switch (source.metadata.source) {
78+
case 'cursor':
79+
return source.metadata.kind === 'type';
80+
}
81+
return false;
82+
}
83+
6584
export const EditSources = {
6685
unknown(data: { name?: string | null }) {
6786
return createEditSource({

src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions
3636
import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol.js';
3737
import { InlineCompletionEndOfLifeReasonKind } from '../common/extHostTypes.js';
3838
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
39-
import { DataChannelForwardingTelemetryService } from '../../contrib/editTelemetry/browser/forwardingTelemetryService.js';
39+
import { DataChannelForwardingTelemetryService } from '../../contrib/editTelemetry/browser/telemetry/forwardingTelemetryService.js';
4040

4141
@extHostNamedCustomer(MainContext.MainThreadLanguageFeatures)
4242
export class MainThreadLanguageFeatures extends Disposable implements MainThreadLanguageFeaturesShape {

src/vs/workbench/browser/parts/statusbar/statusbarItem.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export class StatusbarEntryItem extends Disposable {
8080
this.beakContainer = $('.status-bar-item-beak-container');
8181
this.container.appendChild(this.beakContainer);
8282

83+
if (entry.content) {
84+
this.container.appendChild(entry.content);
85+
}
86+
8387
this.update(entry);
8488
}
8589

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*---------------------------------------------------------------------------------------------
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 { sumBy } from '../../../../../base/common/arrays.js';
7+
import { TaskQueue, timeout } from '../../../../../base/common/async.js';
8+
import { Lazy } from '../../../../../base/common/lazy.js';
9+
import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';
10+
import { autorun, mapObservableArrayCached, observableValue, runOnChange } from '../../../../../base/common/observable.js';
11+
import { AnnotatedStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';
12+
import { isAiEdit, isUserEdit } from '../../../../../editor/common/textModelEditSource.js';
13+
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
14+
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
15+
import { AnnotatedDocuments } from '../helpers/annotatedDocuments.js';
16+
import { AiStatsStatusBar } from './aiStatsStatusBar.js';
17+
18+
export class AiStatsFeature extends Disposable {
19+
private readonly _data: IValue<IData>;
20+
private readonly _dataVersion = observableValue(this, 0);
21+
22+
constructor(
23+
annotatedDocuments: AnnotatedDocuments,
24+
@IStorageService private readonly _storageService: IStorageService,
25+
@IInstantiationService private readonly _instantiationService: IInstantiationService,
26+
) {
27+
super();
28+
29+
const storedValue = getStoredValue<IData>(this._storageService, 'aiStats', StorageScope.WORKSPACE, StorageTarget.USER);
30+
this._data = rateLimitWrite<IData>(storedValue, 1 / 60, this._store);
31+
32+
this.aiRate.recomputeInitiallyAndOnChange(this._store);
33+
34+
this._register(autorun(reader => {
35+
reader.store.add(this._instantiationService.createInstance(AiStatsStatusBar.hot.read(reader), this.aiRate));
36+
}));
37+
38+
const obs = mapObservableArrayCached(this, annotatedDocuments.documents, (doc, store) => {
39+
store.add(runOnChange(doc.documentWithAnnotations.value, (_val, _prev, edit) => {
40+
const e = AnnotatedStringEdit.compose(edit.map(e => e.edit));
41+
42+
const curSession = new Lazy(() => this._getDataAndSession());
43+
44+
for (const r of e.replacements) {
45+
if (isAiEdit(r.data.editSource)) {
46+
curSession.value.currentSession.aiCharacters += r.newText.length;
47+
} else if (isUserEdit(r.data.editSource)) {
48+
curSession.value.currentSession.typedCharacters += r.newText.length;
49+
}
50+
}
51+
52+
if (curSession.hasValue) {
53+
this._data.writeValue(curSession.value.data);
54+
this._dataVersion.set(this._dataVersion.get() + 1, undefined);
55+
}
56+
}));
57+
});
58+
59+
obs.recomputeInitiallyAndOnChange(this._store);
60+
61+
}
62+
63+
public readonly aiRate = this._dataVersion.map(() => {
64+
const val = this._data.getValue();
65+
if (!val) {
66+
return 0;
67+
}
68+
69+
const r = average(val.sessions, session => {
70+
const sum = session.typedCharacters + session.aiCharacters;
71+
if (sum === 0) {
72+
return 0;
73+
}
74+
return session.aiCharacters / sum;
75+
});
76+
77+
return r;
78+
});
79+
80+
private _getDataAndSession(): { data: IData; currentSession: ISession } {
81+
const state = this._data.getValue() ?? { sessions: [] };
82+
83+
const sessionLengthMs = 5 * 60 * 1000; // 5 minutes
84+
85+
let lastSession = state.sessions.at(-1);
86+
const nowTime = Date.now();
87+
if (!lastSession || nowTime - lastSession.startTime > sessionLengthMs) {
88+
state.sessions.push({
89+
startTime: nowTime,
90+
typedCharacters: 0,
91+
aiCharacters: 0
92+
});
93+
lastSession = state.sessions.at(-1)!;
94+
95+
const dayMs = 24 * 60 * 60 * 1000; // 24h
96+
// Clean up old sessions, keep only the last 24h worth of sessions
97+
while (state.sessions.length > dayMs / sessionLengthMs) {
98+
state.sessions.shift();
99+
}
100+
}
101+
return { data: state, currentSession: lastSession };
102+
}
103+
}
104+
105+
interface IData {
106+
sessions: ISession[];
107+
}
108+
109+
// 5 min window
110+
interface ISession {
111+
startTime: number;
112+
typedCharacters: number;
113+
aiCharacters: number;
114+
}
115+
116+
117+
function average<T>(arr: T[], selector: (item: T) => number): number {
118+
if (arr.length === 0) {
119+
return 0;
120+
}
121+
const s = sumBy(arr, selector);
122+
return s / arr.length;
123+
}
124+
125+
126+
interface IValue<T> {
127+
writeValue(value: T | undefined): void;
128+
getValue(): T | undefined;
129+
}
130+
131+
function rateLimitWrite<T>(targetValue: IValue<T>, maxWritesPerSecond: number, store: DisposableStore): IValue<T> {
132+
const queue = new TaskQueue();
133+
let _value: T | undefined = undefined;
134+
let valueVersion = 0;
135+
let savedVersion = 0;
136+
store.add(toDisposable(() => {
137+
if (valueVersion !== savedVersion) {
138+
targetValue.writeValue(_value);
139+
savedVersion = valueVersion;
140+
}
141+
}));
142+
143+
return {
144+
writeValue(value: T | undefined): void {
145+
valueVersion++;
146+
const v = valueVersion;
147+
_value = value;
148+
149+
queue.clearPending();
150+
queue.schedule(async () => {
151+
targetValue.writeValue(value);
152+
savedVersion = v;
153+
await timeout(5000);
154+
});
155+
},
156+
getValue(): T | undefined {
157+
if (valueVersion > 0) {
158+
return _value;
159+
}
160+
return targetValue.getValue();
161+
}
162+
};
163+
}
164+
165+
function getStoredValue<T>(service: IStorageService, key: string, scope: StorageScope, target: StorageTarget): IValue<T> {
166+
let lastValue: T | undefined = undefined;
167+
let hasLastValue = false;
168+
return {
169+
writeValue(value: T | undefined): void {
170+
if (value === undefined) {
171+
service.remove(key, scope);
172+
} else {
173+
service.store(key, JSON.stringify(value), scope, target);
174+
}
175+
lastValue = value;
176+
},
177+
getValue(): T | undefined {
178+
if (hasLastValue) {
179+
return lastValue;
180+
}
181+
const strVal = service.get(key, scope);
182+
lastValue = strVal === undefined ? undefined : JSON.parse(strVal) as T | undefined;
183+
hasLastValue = true;
184+
return lastValue;
185+
}
186+
};
187+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*---------------------------------------------------------------------------------------------
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 { n } from '../../../../../base/browser/dom.js';
7+
import { createHotClass } from '../../../../../base/common/hotReloadHelpers.js';
8+
import { Disposable } from '../../../../../base/common/lifecycle.js';
9+
import { autorun, IObservable } from '../../../../../base/common/observable.js';
10+
import { localize } from '../../../../../nls.js';
11+
import { IStatusbarService, StatusbarAlignment } from '../../../../services/statusbar/browser/statusbar.js';
12+
13+
export class AiStatsStatusBar extends Disposable {
14+
public static readonly hot = createHotClass(AiStatsStatusBar);
15+
16+
constructor(
17+
aiRate: IObservable<number>,
18+
@IStatusbarService private readonly _statusbarService: IStatusbarService,
19+
) {
20+
super();
21+
22+
this._register(autorun((reader) => {
23+
const container = n.div({
24+
style: {
25+
height: '100%',
26+
display: 'flex',
27+
alignItems: 'center',
28+
justifyContent: 'center',
29+
}
30+
}, [
31+
n.div(
32+
{
33+
class: 'ai-stats-status-bar',
34+
style: {
35+
display: 'flex',
36+
flexDirection: 'column',
37+
38+
width: 50,
39+
height: 10,
40+
41+
borderRadius: 6,
42+
border: '1.5px solid var(--vscode-statusBar-foreground)',
43+
}
44+
},
45+
[
46+
n.div({
47+
style: {
48+
flex: 1,
49+
50+
display: 'flex',
51+
overflow: 'hidden',
52+
53+
borderRadius: 6,
54+
border: '2px solid transparent',
55+
}
56+
}, [
57+
n.div({
58+
style: {
59+
width: aiRate.map(v => `${v * 100}%`),
60+
backgroundColor: 'var(--vscode-statusBar-foreground)',
61+
}
62+
})
63+
])
64+
]
65+
)
66+
]).keepUpdated(reader.store);
67+
68+
69+
reader.store.add(this._statusbarService.addEntry({
70+
name: localize('inlineSuggestions', "Inline Suggestions"),
71+
ariaLabel: localize('inlineSuggestionsStatusBar', "Inline suggestions status bar"),
72+
text: '',
73+
content: container.element,
74+
}, 'aiStatsStatusBar', StatusbarAlignment.RIGHT, 100));
75+
}));
76+
}
77+
}

src/vs/workbench/contrib/editTelemetry/browser/editTelemetry.contribution.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Registry } from '../../../../platform/registry/common/platform.js';
7-
import { EditTelemetryService } from './editTelemetryService.js';
7+
import { EDIT_TELEMETRY_SETTING_ID, AI_STATS_SETTING_ID, EditTelemetryContribution } from './editTelemetryContribution.js';
88
import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js';
99
import { localize } from '../../../../nls.js';
10-
import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js';
10+
import { EDIT_TELEMETRY_DETAILS_SETTING_ID, EDIT_TELEMETRY_SHOW_DECORATIONS, EDIT_TELEMETRY_SHOW_STATUS_BAR } from './settings.js';
1111
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
1212

13-
registerWorkbenchContribution2('EditTelemetryService', EditTelemetryService, WorkbenchPhase.AfterRestored);
13+
registerWorkbenchContribution2('EditTelemetryContribution', EditTelemetryContribution, WorkbenchPhase.AfterRestored);
1414

1515
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
1616
configurationRegistry.registerConfiguration({
@@ -25,6 +25,12 @@ configurationRegistry.registerConfiguration({
2525
default: true,
2626
tags: ['experimental'],
2727
},
28+
[AI_STATS_SETTING_ID]: {
29+
markdownDescription: localize('editor.aiStats.enabled', "Controls whether to enable AI statistics in the editor."),
30+
type: 'boolean',
31+
default: false,
32+
tags: ['experimental'],
33+
},
2834
[EDIT_TELEMETRY_DETAILS_SETTING_ID]: {
2935
markdownDescription: localize('telemetry.editStats.detailed.enabled', "Controls whether to enable telemetry for detailed edit statistics (only sends statistics if general telemetry is enabled)."),
3036
type: 'boolean',

src/vs/workbench/contrib/editTelemetry/browser/editTelemetryService.ts renamed to src/vs/workbench/contrib/editTelemetry/browser/editTelemetryContribution.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,47 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { Disposable } from '../../../../base/common/lifecycle.js';
7-
import { autorun } from '../../../../base/common/observable.js';
7+
import { autorun, derived } from '../../../../base/common/observable.js';
88
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
99
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
1010
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
1111
import { ITelemetryService, TelemetryLevel, telemetryLevelEnabled } from '../../../../platform/telemetry/common/telemetry.js';
12-
import { EditTrackingFeature } from './editSourceTrackingFeature.js';
13-
import { EDIT_TELEMETRY_SETTING_ID } from './settings.js';
14-
import { VSCodeWorkspace } from './vscodeObservableWorkspace.js';
12+
import { AnnotatedDocuments } from './helpers/annotatedDocuments.js';
13+
import { EditTrackingFeature } from './telemetry/editSourceTrackingFeature.js';
14+
import { VSCodeWorkspace } from './helpers/vscodeObservableWorkspace.js';
15+
import { AiStatsFeature } from './editStats/aiStatsFeature.js';
1516

16-
export class EditTelemetryService extends Disposable {
17-
private readonly _editSourceTrackingEnabled;
17+
export const EDIT_TELEMETRY_SETTING_ID = 'telemetry.editStats.enabled';
18+
export const AI_STATS_SETTING_ID = 'editor.aiStats.enabled';
1819

20+
export class EditTelemetryContribution extends Disposable {
1921
constructor(
2022
@IInstantiationService private readonly _instantiationService: IInstantiationService,
2123
@IConfigurationService private readonly _configurationService: IConfigurationService,
2224
@ITelemetryService private readonly _telemetryService: ITelemetryService,
2325
) {
2426
super();
2527

26-
this._editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService);
28+
const workspace = derived(reader => reader.store.add(this._instantiationService.createInstance(VSCodeWorkspace)));
29+
const annotatedDocuments = derived(reader => reader.store.add(this._instantiationService.createInstance(AnnotatedDocuments, workspace.read(reader))));
2730

31+
const editSourceTrackingEnabled = observableConfigValue(EDIT_TELEMETRY_SETTING_ID, true, this._configurationService);
2832
this._register(autorun(r => {
29-
const enabled = this._editSourceTrackingEnabled.read(r);
33+
const enabled = editSourceTrackingEnabled.read(r);
3034
if (!enabled || !telemetryLevelEnabled(this._telemetryService, TelemetryLevel.USAGE)) {
3135
return;
3236
}
37+
r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace.read(r), annotatedDocuments.read(r)));
38+
}));
3339

34-
const workspace = this._instantiationService.createInstance(VSCodeWorkspace);
40+
const aiStatsEnabled = observableConfigValue(AI_STATS_SETTING_ID, true, this._configurationService);
41+
this._register(autorun(r => {
42+
const enabled = aiStatsEnabled.read(r);
43+
if (!enabled) {
44+
return;
45+
}
3546

36-
r.store.add(this._instantiationService.createInstance(EditTrackingFeature, workspace));
47+
r.store.add(this._instantiationService.createInstance(AiStatsFeature, annotatedDocuments.read(r)));
3748
}));
3849
}
3950
}

0 commit comments

Comments
 (0)