Skip to content

Commit 31ebc11

Browse files
authored
perf(codewhisperer): connection reuse for generationCompletions #4040
Problem: [microsoft/vscode#173861](microsoft/vscode#173861) Solution: Inject http agent only for the GenerateCompletions API, do this per IDE session. Tested and verified: 1. This inject won't happen for any other API calls. It only applies to GenerateCompletions API when user is NOT using `http.proxy` vscode feature. 2. No regression found with Toolkit features or CodeWhisperer features. 3. Regardless of user VSC proxy setting `http.proxySupport`, it still enables HTTP connection reuse. 4. Tested in AWS Cloud9.
1 parent a31ce1c commit 31ebc11

File tree

5 files changed

+100
-0
lines changed

5 files changed

+100
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "CodeWhisperer: faster code completion by enabling HTTP connection reuse for completions API"
4+
}

src/codewhisperer/activation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { openUrl } from '../shared/utilities/vsCodeUtils'
5656
import { notifyNewCustomizations } from './util/customizationUtil'
5757
import { CodeWhispererCommandBackend, CodeWhispererCommandDeclarations } from './commands/gettingStartedPageCommands'
5858
import { listCodeWhispererCommands } from './commands/statusBarCommands'
59+
import { updateUserProxyUrl } from './client/agent'
5960
const performance = globalThis.performance ?? require('perf_hooks').performance
6061

6162
export async function activate(context: ExtContext): Promise<void> {
@@ -147,6 +148,10 @@ export async function activate(context: ExtContext): Promise<void> {
147148
}
148149
})
149150
}
151+
152+
if (configurationChangeEvent.affectsConfiguration('http.proxy')) {
153+
updateUserProxyUrl()
154+
}
150155
}),
151156
/**
152157
* Open Configuration

src/codewhisperer/client/agent.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*
5+
* Copyright 2022 Sourcegraph, Inc.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
*/
14+
import * as vscode from 'vscode'
15+
import http from 'http'
16+
import https from 'https'
17+
import { getLogger } from '../../shared/logger'
18+
19+
// The path to the exported class can be found in the npm contents
20+
// https://www.npmjs.com/package/@vscode/proxy-agent?activeTab=code
21+
const nodeModules = '_VSCODE_NODE_MODULES'
22+
const proxyAgentPath = '@vscode/proxy-agent/out/agent'
23+
const proxyAgent = 'PacProxyAgent'
24+
export const keepAliveHeader = 'keep-alive-codewhisperer'
25+
let userProxyUrl = ''
26+
27+
export function updateUserProxyUrl() {
28+
userProxyUrl = vscode.workspace.getConfiguration('http').get('proxy') || ''
29+
}
30+
31+
export function initializeNetworkAgent(): void {
32+
/**
33+
* We use keepAlive agents here to avoid excessive SSL/TLS handshakes for autocomplete requests.
34+
* Socket timeout at client is the same as service connection idle timeout
35+
*/
36+
const httpAgent = new http.Agent({ keepAlive: true, timeout: 60000 })
37+
const httpsAgent = new https.Agent({ keepAlive: true, timeout: 60000 })
38+
39+
const customAgent = ({ protocol }: Pick<URL, 'protocol'>): http.Agent => {
40+
if (protocol === 'http:') {
41+
return httpAgent
42+
}
43+
return httpsAgent
44+
}
45+
updateUserProxyUrl()
46+
/**
47+
* This works around an issue in the default VS Code proxy agent code. When `http.proxySupport`
48+
* is set to its default value and no proxy setting is being used, the proxy library does not
49+
* properly reuse the agent set on the http(s) method and is instead always using a new agent
50+
* per request.
51+
*
52+
* To work around this, we patch the default proxy agent method and overwrite the
53+
* `originalAgent` value before invoking it for requests that want to keep their connection
54+
* alive only when user is not using their own http proxy and the request contains keepAliveHeader
55+
*
56+
* c.f. https://github.com/microsoft/vscode/issues/173861
57+
* code reference: https://github.com/sourcegraph/cody/pull/868/files
58+
*/
59+
try {
60+
const PacProxyAgent = (globalThis as any)?.[nodeModules]?.[proxyAgentPath]?.[proxyAgent] ?? undefined
61+
if (PacProxyAgent) {
62+
const originalConnect = PacProxyAgent.prototype.connect
63+
// Patches the implementation defined here:
64+
// https://github.com/microsoft/vscode-proxy-agent/blob/d340b9d34684da494d6ebde3bcd18490a8bbd071/src/agent.ts#L53
65+
PacProxyAgent.prototype.connect = function (req: http.ClientRequest, opts: { protocol: string }): any {
66+
try {
67+
const connectionHeader = req.getHeader('connection')
68+
const connectionHeaderHasKeepAlive =
69+
connectionHeader === keepAliveHeader ||
70+
(Array.isArray(connectionHeader) && connectionHeader.includes(keepAliveHeader))
71+
if (connectionHeaderHasKeepAlive && userProxyUrl === '') {
72+
this.opts.originalAgent = customAgent(opts)
73+
return originalConnect.call(this, req, opts)
74+
}
75+
return originalConnect.call(this, req, opts)
76+
} catch {
77+
return originalConnect.call(this, req, opts)
78+
}
79+
}
80+
} else {
81+
getLogger().info('PacProxyAgent not found')
82+
}
83+
} catch (error) {
84+
// Log any errors in the patching logic
85+
getLogger().error('Failed to patch http agent', error)
86+
}
87+
}

src/codewhisperer/client/codewhisperer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import userApiConfig = require('./user-service-2.json')
2020
import { session } from '../util/codeWhispererSession'
2121
import { getLogger } from '../../shared/logger'
2222
import { indent } from '../../shared/utilities/textUtilities'
23+
import { keepAliveHeader } from './agent'
2324

2425
export type ProgrammingLanguage = Readonly<
2526
CodeWhispererClient.ProgrammingLanguage | CodeWhispererUserClient.ProgrammingLanguage
@@ -125,6 +126,7 @@ export class DefaultCodeWhispererClient {
125126
if (req.operation === 'generateCompletions') {
126127
req.on('build', () => {
127128
req.httpRequest.headers['x-amzn-codewhisperer-optout'] = `${isOptedOut}`
129+
req.httpRequest.headers['Connection'] = keepAliveHeader
128130
})
129131
}
130132
},

src/extension.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ import { showMessageWithUrl, showViewLogsMessage } from './shared/utilities/mess
7474
import { registerWebviewErrorHandler } from './webviews/server'
7575
import { initializeManifestPaths } from './extensionShared'
7676
import { ChildProcess } from './shared/utilities/childProcess'
77+
import { initializeNetworkAgent } from './codewhisperer/client/agent'
7778

7879
let localize: nls.LocalizeFunc
7980

8081
export async function activate(context: vscode.ExtensionContext) {
82+
initializeNetworkAgent()
8183
await initializeComputeRegion()
8284
const activationStartedOn = Date.now()
8385
localize = nls.loadMessageBundle()

0 commit comments

Comments
 (0)