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
5263 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 sessions = derived(this, r => {
117
this._dataVersion.read(r);
118
const val = this._data.getValue();
119
if (!val) {
120
return [];
121
}
122
return val.sessions;
123
});
124
125
public readonly acceptedInlineSuggestionsToday = derived(this, r => {
126
this._dataVersion.read(r);
127
const val = this._data.getValue();
128
if (!val) {
129
return 0;
130
}
131
const startOfToday = new Date();
132
startOfToday.setHours(0, 0, 0, 0);
133
134
const sessionsToday = val.sessions.filter(s => s.startTime > startOfToday.getTime());
135
return sumBy(sessionsToday, s => s.acceptedInlineSuggestions ?? 0);
136
});
137
138
private _getDataAndSession(): { data: IData; currentSession: ISession } {
139
const state = this._data.getValue() ?? { sessions: [] };
140
141
const sessionLengthMs = 5 * 60 * 1000; // 5 minutes
142
143
let lastSession = state.sessions.at(-1);
144
const nowTime = Date.now();
145
if (!lastSession || nowTime - lastSession.startTime > sessionLengthMs) {
146
state.sessions.push({
147
startTime: nowTime,
148
typedCharacters: 0,
149
aiCharacters: 0,
150
acceptedInlineSuggestions: 0,
151
chatEditCount: 0,
152
});
153
lastSession = state.sessions.at(-1)!;
154
155
const dayMs = 24 * 60 * 60 * 1000; // 24h
156
// Clean up old sessions, keep only the last 24h worth of sessions
157
while (state.sessions.length > dayMs / sessionLengthMs) {
158
state.sessions.shift();
159
}
160
}
161
return { data: state, currentSession: lastSession };
162
}
163
}
164
165
interface IData {
166
sessions: ISession[];
167
}
168
169
// 5 min window
170
interface ISession {
171
startTime: number;
172
typedCharacters: number;
173
aiCharacters: number;
174
acceptedInlineSuggestions: number | undefined;
175
chatEditCount: number | undefined;
176
}
177
178
179
function average<T>(arr: T[], selector: (item: T) => number): number {
180
if (arr.length === 0) {
181
return 0;
182
}
183
const s = sumBy(arr, selector);
184
return s / arr.length;
185
}
186
187
188
interface IValue<T> {
189
writeValue(value: T | undefined): void;
190
getValue(): T | undefined;
191
}
192
193
function rateLimitWrite<T>(targetValue: IValue<T>, maxWritesPerSecond: number, store: DisposableStore): IValue<T> {
194
const queue = new TaskQueue();
195
let _value: T | undefined = undefined;
196
let valueVersion = 0;
197
let savedVersion = 0;
198
store.add(toDisposable(() => {
199
if (valueVersion !== savedVersion) {
200
targetValue.writeValue(_value);
201
savedVersion = valueVersion;
202
}
203
}));
204
205
return {
206
writeValue(value: T | undefined): void {
207
valueVersion++;
208
const v = valueVersion;
209
_value = value;
210
211
queue.clearPending();
212
queue.schedule(async () => {
213
targetValue.writeValue(value);
214
savedVersion = v;
215
await timeout(5000);
216
});
217
},
218
getValue(): T | undefined {
219
if (valueVersion > 0) {
220
return _value;
221
}
222
return targetValue.getValue();
223
}
224
};
225
}
226
227
function getStoredValue<T>(service: IStorageService, key: string, scope: StorageScope, target: StorageTarget): IValue<T> {
228
let lastValue: T | undefined = undefined;
229
let hasLastValue = false;
230
return {
231
writeValue(value: T | undefined): void {
232
if (value === undefined) {
233
service.remove(key, scope);
234
} else {
235
service.store(key, JSON.stringify(value), scope, target);
236
}
237
lastValue = value;
238
},
239
getValue(): T | undefined {
240
if (hasLastValue) {
241
return lastValue;
242
}
243
const strVal = service.get(key, scope);
244
lastValue = strVal === undefined ? undefined : JSON.parse(strVal) as T | undefined;
245
hasLastValue = true;
246
return lastValue;
247
}
248
};
249
}
250
251