Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/vs/platform/agentHost/common/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,9 @@ export interface IAgentChats {
/** Dispose the addressed chat and free its backing. */
disposeChat(chat: URI, context: AgentChatOperationContext): Promise<void>;

/** Return whether the addressed chat can currently release its in-memory backing. */
canReleaseChat?(chat: URI, context: AgentChatOperationContext): Promise<boolean>;

/** Release the addressed chat's in-memory backing without deleting durable data. */
releaseChat(chat: URI, context: AgentChatOperationContext): Promise<void>;

Expand Down
141 changes: 141 additions & 0 deletions src/vs/platform/agentHost/common/agentHostStartupTelemetry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { AgentHostClientType } from './agentHostClientInfo.js';
import { AgentHostClientConnectionKind } from './agentHostTelemetry.js';

type AgentHostStartupOutcome = 'success' | 'error' | 'timeout';
type AgentHostStartupFailureStage = 'protocolConnection' | 'sessionList';

export const AgentHostStartupTimeoutMs = 2 * 60 * 1000;

interface IAgentHostStartupEvent {
clientType: AgentHostClientType;
connectionKind: AgentHostClientConnectionKind;
outcome: AgentHostStartupOutcome;
failureStage: AgentHostStartupFailureStage | undefined;
timeToMessagePortMs: number | undefined;
timeToProtocolConnectionMs: number | undefined;
timeToAuthenticationSettledMs: number | undefined;
timeToSessionListRequestMs: number | undefined;
timeToSessionListCompleteMs: number | undefined;
sessionListDurationMs: number | undefined;
sessionListAttemptCount: number;
sessionListFailureCount: number;
}

type AgentHostStartupClassification = {
clientType: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded type of the Agent Host client.' };
connectionKind: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The route the client used to reach the Agent Host.' };
outcome: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Whether Agent Host startup reached the first successful session list or failed to connect.' };
failureStage: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The bounded startup stage that failed, when startup did not succeed.' };
timeToMessagePortMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the local Agent Host start request until its initial MessagePort was acquired.' };
timeToProtocolConnectionMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the Agent Host start request until AHP initialization completed.' };
timeToAuthenticationSettledMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the Agent Host start request until the initial authentication pass settled.' };
timeToSessionListRequestMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the Agent Host start request until the first session-list request.' };
timeToSessionListCompleteMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the Agent Host start request until the first successful session-list response.' };
sessionListDurationMs: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Milliseconds from the first session-list request until the first successful response, including retries or overlapping requests.' };
sessionListAttemptCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of session-list requests started before the first successful response or connection failure.' };
sessionListFailureCount: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; isMeasurement: true; comment: 'Number of session-list requests that failed before the first successful response or connection failure.' };
owner: 'roblourens';
comment: 'Tracks Agent Host startup performance from the client start request through AHP connection, authentication, and the first successful session list.';
};

export class AgentHostStartupTelemetry extends Disposable {

private readonly _stopWatch;
private readonly _timeout: IDisposable;
private _reported = false;
private _timeToMessagePortMs: number | undefined;
private _timeToProtocolConnectionMs: number | undefined;
private _timeToAuthenticationSettledMs: number | undefined;
private _timeToSessionListRequestMs: number | undefined;
private _sessionListAttemptCount = 0;
private _sessionListFailureCount = 0;

constructor(
private readonly _clientType: AgentHostClientType,
private readonly _connectionKind: AgentHostClientConnectionKind,
stopWatchFactory: () => Pick<StopWatch, 'elapsed'>,
timeoutFactory: (callback: () => void, timeoutMs: number) => IDisposable,
@ITelemetryService private readonly _telemetryService: ITelemetryService,
) {
super();
this._stopWatch = stopWatchFactory();
this._timeout = this._register(timeoutFactory(() => this._report('timeout', this._failureStage()), AgentHostStartupTimeoutMs));
}

messagePortAcquired(): void {
this._timeToMessagePortMs ??= this._stopWatch.elapsed();
}

protocolConnected(): void {
this._timeToProtocolConnectionMs ??= this._stopWatch.elapsed();
}

authenticationSettled(): void {
this._timeToAuthenticationSettledMs ??= this._stopWatch.elapsed();
}

sessionListRequested(): void {
if (this._reported) {
return;
}
this._sessionListAttemptCount++;
this._timeToSessionListRequestMs ??= this._stopWatch.elapsed();
}

sessionListFailed(): void {
if (!this._reported) {
this._sessionListFailureCount++;
}
}

sessionListSucceeded(): void {
this._report('success', undefined);
}

connectionFailed(): void {
this._report('error', this._failureStage());
}

override dispose(): void {
this._reported = true;
super.dispose();
}

/** Startup reaches the session-list stage as soon as the protocol connects. */
private _failureStage(): AgentHostStartupFailureStage {
return this._timeToProtocolConnectionMs === undefined ? 'protocolConnection' : 'sessionList';
}

private _report(outcome: AgentHostStartupOutcome, failureStage: AgentHostStartupFailureStage | undefined): void {
if (this._reported) {
return;
}
this._reported = true;
this._timeout.dispose();
const timeToSessionListCompleteMs = outcome === 'success' ? this._stopWatch.elapsed() : undefined;
this._telemetryService.publicLog2<IAgentHostStartupEvent, AgentHostStartupClassification>('agentHost.startup', {
clientType: this._clientType,
connectionKind: this._connectionKind,
outcome,
failureStage,
timeToMessagePortMs: this._timeToMessagePortMs,
timeToProtocolConnectionMs: this._timeToProtocolConnectionMs,
timeToAuthenticationSettledMs: this._timeToAuthenticationSettledMs,
timeToSessionListRequestMs: this._timeToSessionListRequestMs,
timeToSessionListCompleteMs,
sessionListDurationMs: timeToSessionListCompleteMs !== undefined && this._timeToSessionListRequestMs !== undefined
? Math.max(0, timeToSessionListCompleteMs - this._timeToSessionListRequestMs)
: undefined,
sessionListAttemptCount: this._sessionListAttemptCount,
sessionListFailureCount: this._sessionListFailureCount,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { DeferredPromise } from '../../../base/common/async.js';
import { DeferredPromise, disposableTimeout } from '../../../base/common/async.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, DisposableStore, IReference, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { constObservable, IObservable, ISettableObservable, observableValue } from '../../../base/common/observable.js';
import { mark } from '../../../base/common/performance.js';
import { StopWatch } from '../../../base/common/stopwatch.js';
import { URI } from '../../../base/common/uri.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { getDelayedChannel, IChannelClient, IChannelServer, ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
Expand All @@ -22,8 +23,10 @@ import { AgentHostIpcChannelTransport } from '../browser/agentHostIpcChannelTran
import { AgentHostClientState, RemoteAgentHostProtocolClient } from '../browser/remoteAgentHostProtocolClient.js';
import { AhpJsonlLogger } from '../common/ahpJsonlLogger.js';
import { AGENT_HOST_CLIENT_BYOK_LM_CHANNEL, AgentHostClientByokLmChannel } from '../common/agentHostClientByokLmChannel.js';
import { getAgentHostClientType } from '../common/agentHostClientInfo.js';
import { AGENT_HOST_CLIENT_PROXY_CHANNEL, AgentHostClientProxyChannel } from '../common/agentHostClientProxyChannel.js';
import { LOCAL_AGENT_HOST_RESOURCE_IDENTITY } from '../common/agentHostResourceService.js';
import { AgentHostStartupTelemetry } from '../common/agentHostStartupTelemetry.js';
import { AgentHostClientConnectionKind } from '../common/agentHostTelemetry.js';
import {
AgentHostAhpJsonlLoggingSettingId,
Expand Down Expand Up @@ -139,6 +142,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
private _didConnectInitially = false;
private _didStartInitialSessionList = false;
private _didCompleteInitialSessionList = false;
private _startupTelemetry: AgentHostStartupTelemetry | undefined;

private readonly _onAgentHostExit = this._register(new Emitter<number>());
readonly onAgentHostExit = this._onAgentHostExit.event;
Expand Down Expand Up @@ -187,6 +191,13 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
startAgentHost(): void {
if (!this._protocolClient) {
mark('code/agentHost/willStart');
this._startupTelemetry = this._register(this._instantiationService.createInstance(
AgentHostStartupTelemetry,
getAgentHostClientType(this._clientInfo),
AgentHostClientConnectionKind.Local,
() => StopWatch.create(true),
(callback, timeoutMs) => disposableTimeout(callback, timeoutMs),
));
this._protocolClient = this._register(this._instantiationService.createInstance(
RemoteAgentHostProtocolClient,
LOCAL_AGENT_HOST_RESOURCE_IDENTITY,
Expand Down Expand Up @@ -242,6 +253,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
}
if (!this._didAcquireInitialMessagePort) {
this._didAcquireInitialMessagePort = true;
this._startupTelemetry?.messagePortAcquired();
mark('code/agentHost/didAcquireMessagePort');
}
this._logService.info(`${LOG_PREFIX} MessagePort acquired, creating client...`);
Expand Down Expand Up @@ -273,6 +285,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
}
if (state === AgentHostClientState.Connected) {
this._managementConnection.connected();
this._startupTelemetry?.protocolConnected();
if (!this._didConnectInitially) {
this._didConnectInitially = true;
mark('code/agentHost/didConnect');
Expand All @@ -284,6 +297,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
if (state === AgentHostClientState.Reconnecting) {
this._managementConnection.reconnecting();
} else {
this._startupTelemetry?.connectionFailed();
this._managementConnection.closed(state === AgentHostClientState.Incompatible
? 'Local agent host protocol is incompatible.'
: 'Local agent host connection closed.');
Expand All @@ -305,6 +319,7 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
}
if (!pending) {
this._authenticationSettled = true;
this._startupTelemetry?.authenticationSettled();
}
this._authenticationPending.set(pending, undefined);
}
Expand Down Expand Up @@ -354,17 +369,25 @@ export class LocalAgentHostServiceClient extends Disposable implements IAgentHos
}

listSessions(): Promise<IAgentSessionMetadata[]> {
this._startupTelemetry?.sessionListRequested();
if (!this._didStartInitialSessionList) {
this._didStartInitialSessionList = true;
mark('code/agentHost/willListSessions');
}
return this._requireClient().listSessions().then(sessions => {
if (!this._didCompleteInitialSessionList) {
this._didCompleteInitialSessionList = true;
mark('code/agentHost/didListSessions');
}
return sessions;
});
return this._requireClient().listSessions().then(
sessions => {
this._startupTelemetry?.sessionListSucceeded();
if (!this._didCompleteInitialSessionList) {
this._didCompleteInitialSessionList = true;
mark('code/agentHost/didListSessions');
}
return sessions;
},
error => {
this._startupTelemetry?.sessionListFailed();
throw error;
},
);
}

createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
Expand Down
2 changes: 1 addition & 1 deletion src/vs/platform/agentHost/node/agentHostBootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function registerAgentHostNetworkServices(
const configurationService = disposables.add(new ConfigurationService(settingsResource, fileService, policyService, logService));
await configurationService.initialize();
diServices.set(IConfigurationService, configurationService);
const proxyResolver = new AgentHostProxyResolver(configurationService, logService);
const proxyResolver = disposables.add(new AgentHostProxyResolver(configurationService, logService));
diServices.set(IAgentHostProxyResolver, proxyResolver);
const requestService = disposables.add(new AgentHostRequestService(configurationService, environmentService, logService, proxyResolver));
diServices.set(IRequestService, requestService);
Expand Down
18 changes: 15 additions & 3 deletions src/vs/platform/agentHost/node/agentHostProxyResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
*--------------------------------------------------------------------------------------------*/

import { LogLevel as ProxyLogLevel, ProxyAgentParams, ProxySupportSetting, createFetchPatch, createProxyAuthorizationLookup, createProxyResolver, loadSystemCertificates } from '@vscode/proxy-agent';
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ILogService, LogLevel } from '../../log/common/log.js';
Expand All @@ -26,6 +27,8 @@ export const IAgentHostProxyResolver = createDecorator<IAgentHostProxyResolver>(
export interface IAgentHostProxyResolver {
readonly _serviceBrand: undefined;

readonly onDidRegisterConnection: Event<void>;

/** Register a renderer connection. Disposing the result removes it. */
register(clientId: string, connection: IAgentHostClientProxyConnection): IDisposable;

Expand All @@ -42,10 +45,13 @@ export interface IAgentHostProxyResolver {
fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
}

export class AgentHostProxyResolver implements IAgentHostProxyResolver {
export class AgentHostProxyResolver extends Disposable implements IAgentHostProxyResolver {

declare readonly _serviceBrand: undefined;

private readonly _onDidRegisterConnection = this._register(new Emitter<void>());
readonly onDidRegisterConnection = this._onDidRegisterConnection.event;

private readonly _connections = new Map<string, IAgentHostClientProxyConnection>();
private _proxyResolver: ReturnType<typeof createProxyResolver> | undefined;
private _proxyAgentParams: ProxyAgentParams | undefined;
Expand All @@ -54,10 +60,16 @@ export class AgentHostProxyResolver implements IAgentHostProxyResolver {
constructor(
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ILogService private readonly _logService: ILogService,
) { }
) {
super();
}

register(clientId: string, connection: IAgentHostClientProxyConnection): IDisposable {
const hadConnections = this._connections.size > 0;
this._connections.set(clientId, connection);
if (!hadConnections) {
this._onDidRegisterConnection.fire();
}
return toDisposable(() => {
if (this._connections.get(clientId) === connection) {
this._connections.delete(clientId);
Expand Down
Loading
Loading