Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts
3296 views
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, derived, 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));
36
}));
37
38
39
const lastRequestIds: string[] = [];
40
41
const obs = mapObservableArrayCached(this, annotatedDocuments.documents, (doc, store) => {
42
store.add(runOnChange(doc.documentWithAnnotations.value, (_val, _prev, edit) => {
43
const e = AnnotatedStringEdit.compose(edit.map(e => e.edit));
44
45
const curSession = new Lazy(() => this._getDataAndSession());
46
47
for (const r of e.replacements) {
48
if (isAiEdit(r.data.editSource)) {
49
curSession.value.currentSession.aiCharacters += r.newText.length;
50
} else if (isUserEdit(r.data.editSource)) {
51
curSession.value.currentSession.typedCharacters += r.newText.length;
52
}
53
}
54
55
if (e.replacements.length > 0) {
56
const sessionToUpdate = curSession.value.currentSession;
57
const s = e.replacements[0].data.editSource;
58
if (s.metadata.source === 'inlineCompletionAccept') {
59
if (sessionToUpdate.acceptedInlineSuggestions === undefined) {
60
sessionToUpdate.acceptedInlineSuggestions = 0;
61
}
62
sessionToUpdate.acceptedInlineSuggestions += 1;
63
}
64
65
if (s.metadata.source === 'Chat.applyEdits' && s.metadata.$$requestId !== undefined) {
66
const didSeeRequestId = lastRequestIds.includes(s.metadata.$$requestId);
67
if (!didSeeRequestId) {
68
lastRequestIds.push(s.metadata.$$requestId);
69
if (lastRequestIds.length > 10) {
70
lastRequestIds.shift();
71
}
72
if (sessionToUpdate.chatEditCount === undefined) {
73
sessionToUpdate.chatEditCount = 0;
74
}
75
sessionToUpdate.chatEditCount += 1;
76
}
77
}
78
}
79
80
if (curSession.hasValue) {
81
this._data.writeValue(curSession.value.data);
82
this._dataVersion.set(this._dataVersion.get() + 1, undefined);
83
}
84
}));
85
});
86
87
obs.recomputeInitiallyAndOnChange(this._store);
88
}
89
90
public readonly aiRate = this._dataVersion.map(() => {
91
const val = this._data.getValue();
92
if (!val) {
93
return 0;
94
}
95
96
const r = average(val.sessions, session => {
97
const sum = session.typedCharacters + session.aiCharacters;
98
if (sum === 0) {
99
return 0;
100
}
101
return session.aiCharacters / sum;
102
});
103
104
return r;
105
});
106
107
public readonly sessionCount = derived(this, r => {
108
this._dataVersion.read(r);
109
const val = this._data.getValue();
110
if (!val) {
111
return 0;
112
}
113
return val.sessions.length;
114
});
115
116
public readonly acceptedInlineSuggestionsToday = derived(this, r => {
117
this._dataVersion.read(r);
118
const val = this._data.getValue();
119
if (!val) {
120
return 0;
121
}
122
const startOfToday = new Date();
123
startOfToday.setHours(0, 0, 0, 0);
124
125
const sessionsToday = val.sessions.filter(s => s.startTime > startOfToday.getTime());
126
return sumBy(sessionsToday, s => s.acceptedInlineSuggestions ?? 0);
127
});
128
129
private _getDataAndSession(): { data: IData; currentSession: ISession } {
130
const state = this._data.getValue() ?? { sessions: [] };
131
132
const sessionLengthMs = 5 * 60 * 1000; // 5 minutes
133
134
let lastSession = state.sessions.at(-1);
135
const nowTime = Date.now();
136
if (!lastSession || nowTime - lastSession.startTime > sessionLengthMs) {
137
state.sessions.push({
138
startTime: nowTime,
139
typedCharacters: 0,
140
aiCharacters: 0,
141
acceptedInlineSuggestions: 0,
142
chatEditCount: 0,
143
});
144
lastSession = state.sessions.at(-1)!;
145
146
const dayMs = 24 * 60 * 60 * 1000; // 24h
147
// Clean up old sessions, keep only the last 24h worth of sessions
148
while (state.sessions.length > dayMs / sessionLengthMs) {
149
state.sessions.shift();
150
}
151
}
152
return { data: state, currentSession: lastSession };
153
}
154
}
155
156
interface IData {
157
sessions: ISession[];
158
}
159
160
// 5 min window
161
interface ISession {
162
startTime: number;
163
typedCharacters: number;
164
aiCharacters: number;
165
acceptedInlineSuggestions: number | undefined;
166
chatEditCount: number | undefined;
167
}
168
169
170
function average<T>(arr: T[], selector: (item: T) => number): number {
171
if (arr.length === 0) {
172
return 0;
173
}
174
const s = sumBy(arr, selector);
175
return s / arr.length;
176
}
177
178
179
interface IValue<T> {
180
writeValue(value: T | undefined): void;
181
getValue(): T | undefined;
182
}
183
184
function rateLimitWrite<T>(targetValue: IValue<T>, maxWritesPerSecond: number, store: DisposableStore): IValue<T> {
185
const queue = new TaskQueue();
186
let _value: T | undefined = undefined;
187
let valueVersion = 0;
188
let savedVersion = 0;
189
store.add(toDisposable(() => {
190
if (valueVersion !== savedVersion) {
191
targetValue.writeValue(_value);
192
savedVersion = valueVersion;
193
}
194
}));
195
196
return {
197
writeValue(value: T | undefined): void {
198
valueVersion++;
199
const v = valueVersion;
200
_value = value;
201
202
queue.clearPending();
203
queue.schedule(async () => {
204
targetValue.writeValue(value);
205
savedVersion = v;
206
await timeout(5000);
207
});
208
},
209
getValue(): T | undefined {
210
if (valueVersion > 0) {
211
return _value;
212
}
213
return targetValue.getValue();
214
}
215
};
216
}
217
218
function getStoredValue<T>(service: IStorageService, key: string, scope: StorageScope, target: StorageTarget): IValue<T> {
219
let lastValue: T | undefined = undefined;
220
let hasLastValue = false;
221
return {
222
writeValue(value: T | undefined): void {
223
if (value === undefined) {
224
service.remove(key, scope);
225
} else {
226
service.store(key, JSON.stringify(value), scope, target);
227
}
228
lastValue = value;
229
},
230
getValue(): T | undefined {
231
if (hasLastValue) {
232
return lastValue;
233
}
234
const strVal = service.get(key, scope);
235
lastValue = strVal === undefined ? undefined : JSON.parse(strVal) as T | undefined;
236
hasLastValue = true;
237
return lastValue;
238
}
239
};
240
}
241
242