Path: blob/main/src/vs/workbench/contrib/editTelemetry/browser/editStats/aiStatsFeature.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { sumBy } from '../../../../../base/common/arrays.js';6import { TaskQueue, timeout } from '../../../../../base/common/async.js';7import { Lazy } from '../../../../../base/common/lazy.js';8import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';9import { autorun, derived, mapObservableArrayCached, observableValue, runOnChange } from '../../../../../base/common/observable.js';10import { AnnotatedStringEdit } from '../../../../../editor/common/core/edits/stringEdit.js';11import { isAiEdit, isUserEdit } from '../../../../../editor/common/textModelEditSource.js';12import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';13import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';14import { AnnotatedDocuments } from '../helpers/annotatedDocuments.js';15import { AiStatsStatusBar } from './aiStatsStatusBar.js';1617export class AiStatsFeature extends Disposable {18private readonly _data: IValue<IData>;19private readonly _dataVersion = observableValue(this, 0);2021constructor(22annotatedDocuments: AnnotatedDocuments,23@IStorageService private readonly _storageService: IStorageService,24@IInstantiationService private readonly _instantiationService: IInstantiationService,25) {26super();2728const storedValue = getStoredValue<IData>(this._storageService, 'aiStats', StorageScope.WORKSPACE, StorageTarget.USER);29this._data = rateLimitWrite<IData>(storedValue, 1 / 60, this._store);3031this.aiRate.recomputeInitiallyAndOnChange(this._store);3233this._register(autorun(reader => {34reader.store.add(this._instantiationService.createInstance(AiStatsStatusBar.hot.read(reader), this));35}));363738const lastRequestIds: string[] = [];3940const obs = mapObservableArrayCached(this, annotatedDocuments.documents, (doc, store) => {41store.add(runOnChange(doc.documentWithAnnotations.value, (_val, _prev, edit) => {42const e = AnnotatedStringEdit.compose(edit.map(e => e.edit));4344const curSession = new Lazy(() => this._getDataAndSession());4546for (const r of e.replacements) {47if (isAiEdit(r.data.editSource)) {48curSession.value.currentSession.aiCharacters += r.newText.length;49} else if (isUserEdit(r.data.editSource)) {50curSession.value.currentSession.typedCharacters += r.newText.length;51}52}5354if (e.replacements.length > 0) {55const sessionToUpdate = curSession.value.currentSession;56const s = e.replacements[0].data.editSource;57if (s.metadata.source === 'inlineCompletionAccept') {58if (sessionToUpdate.acceptedInlineSuggestions === undefined) {59sessionToUpdate.acceptedInlineSuggestions = 0;60}61sessionToUpdate.acceptedInlineSuggestions += 1;62}6364if (s.metadata.source === 'Chat.applyEdits' && s.metadata.$$requestId !== undefined) {65const didSeeRequestId = lastRequestIds.includes(s.metadata.$$requestId);66if (!didSeeRequestId) {67lastRequestIds.push(s.metadata.$$requestId);68if (lastRequestIds.length > 10) {69lastRequestIds.shift();70}71if (sessionToUpdate.chatEditCount === undefined) {72sessionToUpdate.chatEditCount = 0;73}74sessionToUpdate.chatEditCount += 1;75}76}77}7879if (curSession.hasValue) {80this._data.writeValue(curSession.value.data);81this._dataVersion.set(this._dataVersion.get() + 1, undefined);82}83}));84});8586obs.recomputeInitiallyAndOnChange(this._store);87}8889public readonly aiRate = this._dataVersion.map(() => {90const val = this._data.getValue();91if (!val) {92return 0;93}9495const r = average(val.sessions, session => {96const sum = session.typedCharacters + session.aiCharacters;97if (sum === 0) {98return 0;99}100return session.aiCharacters / sum;101});102103return r;104});105106public readonly sessionCount = derived(this, r => {107this._dataVersion.read(r);108const val = this._data.getValue();109if (!val) {110return 0;111}112return val.sessions.length;113});114115public readonly acceptedInlineSuggestionsToday = derived(this, r => {116this._dataVersion.read(r);117const val = this._data.getValue();118if (!val) {119return 0;120}121const startOfToday = new Date();122startOfToday.setHours(0, 0, 0, 0);123124const sessionsToday = val.sessions.filter(s => s.startTime > startOfToday.getTime());125return sumBy(sessionsToday, s => s.acceptedInlineSuggestions ?? 0);126});127128private _getDataAndSession(): { data: IData; currentSession: ISession } {129const state = this._data.getValue() ?? { sessions: [] };130131const sessionLengthMs = 5 * 60 * 1000; // 5 minutes132133let lastSession = state.sessions.at(-1);134const nowTime = Date.now();135if (!lastSession || nowTime - lastSession.startTime > sessionLengthMs) {136state.sessions.push({137startTime: nowTime,138typedCharacters: 0,139aiCharacters: 0,140acceptedInlineSuggestions: 0,141chatEditCount: 0,142});143lastSession = state.sessions.at(-1)!;144145const dayMs = 24 * 60 * 60 * 1000; // 24h146// Clean up old sessions, keep only the last 24h worth of sessions147while (state.sessions.length > dayMs / sessionLengthMs) {148state.sessions.shift();149}150}151return { data: state, currentSession: lastSession };152}153}154155interface IData {156sessions: ISession[];157}158159// 5 min window160interface ISession {161startTime: number;162typedCharacters: number;163aiCharacters: number;164acceptedInlineSuggestions: number | undefined;165chatEditCount: number | undefined;166}167168169function average<T>(arr: T[], selector: (item: T) => number): number {170if (arr.length === 0) {171return 0;172}173const s = sumBy(arr, selector);174return s / arr.length;175}176177178interface IValue<T> {179writeValue(value: T | undefined): void;180getValue(): T | undefined;181}182183function rateLimitWrite<T>(targetValue: IValue<T>, maxWritesPerSecond: number, store: DisposableStore): IValue<T> {184const queue = new TaskQueue();185let _value: T | undefined = undefined;186let valueVersion = 0;187let savedVersion = 0;188store.add(toDisposable(() => {189if (valueVersion !== savedVersion) {190targetValue.writeValue(_value);191savedVersion = valueVersion;192}193}));194195return {196writeValue(value: T | undefined): void {197valueVersion++;198const v = valueVersion;199_value = value;200201queue.clearPending();202queue.schedule(async () => {203targetValue.writeValue(value);204savedVersion = v;205await timeout(5000);206});207},208getValue(): T | undefined {209if (valueVersion > 0) {210return _value;211}212return targetValue.getValue();213}214};215}216217function getStoredValue<T>(service: IStorageService, key: string, scope: StorageScope, target: StorageTarget): IValue<T> {218let lastValue: T | undefined = undefined;219let hasLastValue = false;220return {221writeValue(value: T | undefined): void {222if (value === undefined) {223service.remove(key, scope);224} else {225service.store(key, JSON.stringify(value), scope, target);226}227lastValue = value;228},229getValue(): T | undefined {230if (hasLastValue) {231return lastValue;232}233const strVal = service.get(key, scope);234lastValue = strVal === undefined ? undefined : JSON.parse(strVal) as T | undefined;235hasLastValue = true;236return lastValue;237}238};239}240241242