Skip to content

Commit

Permalink
feat(agent): add back agent timeout config. (#739)
Browse files Browse the repository at this point in the history
* feat(agent): add back agent timeout config, add option to disable warning for slow response time.

* feat(agent): Update completion timeout to single number. Add config type validation.
  • Loading branch information
icycodes authored Nov 10, 2023
1 parent 138b745 commit ff03e2a
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 23 deletions.
58 changes: 48 additions & 10 deletions clients/tabby-agent/src/AgentConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isBrowser } from "./env";
import { getProperty, deleteProperty } from "dot-prop";

export type AgentConfig = {
server: {
Expand All @@ -17,10 +18,7 @@ export type AgentConfig = {
mode: "adaptive" | "fixed";
interval: number;
};
timeout: {
auto: number;
manually: number;
};
timeout: number;
};
postprocess: {
limitScopeByIndentation: {
Expand Down Expand Up @@ -65,11 +63,7 @@ export const defaultAgentConfig: AgentConfig = {
mode: "adaptive",
interval: 250, // ms
},
// Deprecated: There is a timeout of 3s on the server side since v0.3.0.
timeout: {
auto: 4000, // 4s
manually: 4000, // 4s
},
timeout: 4000, // ms
},
postprocess: {
limitScopeByIndentation: {
Expand Down Expand Up @@ -101,6 +95,12 @@ const configTomlTemplate = `## Tabby agent configuration file
# Header1 = "Value1" # list your custom headers here
# Header2 = "Value2" # values can be strings, numbers or booleans
## Completion
## (Since 1.1.0) You can set the completion request timeout here.
## Note that there is also a timeout config at the server side.
# [completion]
# timeout = 4000 # 4s
## Logs
## You can set the log level here. The log file is located at ~/.tabby-client/agent/logs/.
# [logs]
Expand All @@ -116,6 +116,44 @@ const configTomlTemplate = `## Tabby agent configuration file
`;

const typeCheckSchema = {
server: "object",
"server.endpoint": "string",
"server.token": "string",
"server.requestHeaders": "object",
"server.requestTimeout": "number",
completion: "object",
"completion.prompt": "object",
"completion.prompt.experimentalStripAutoClosingCharacters": "boolean",
"completion.prompt.maxPrefixLines": "number",
"completion.prompt.maxSuffixLines": "number",
"completion.debounce": "object",
"completion.debounce.mode": "string",
"completion.debounce.interval": "number",
"completion.timeout": "number",
postprocess: "object",
"postprocess.limitScopeByIndentation": "object",
"postprocess.limitScopeByIndentation.experimentalKeepBlockScopeWhenCompletingLine": "boolean",
logs: "object",
"logs.level": "string",
anonymousUsageTracking: "object",
"anonymousUsageTracking.disable": "boolean",
};

function checkValueType(object, key, type) {
if (typeof getProperty(object, key) !== type) {
deleteProperty(object, key);
}
}

function validateConfig(config: PartialAgentConfig): PartialAgentConfig {
const validatedConfig = { ...config };
for (const key in typeCheckSchema) {
checkValueType(validatedConfig, key, typeCheckSchema[key]);
}
return validatedConfig;
}

export const userAgentConfig = isBrowser
? null
: (() => {
Expand Down Expand Up @@ -149,7 +187,7 @@ export const userAgentConfig = isBrowser
await this.createTemplate();
return;
}
this.data = data;
this.data = validateConfig(data);
} catch (error) {
if (error.code === "ENOENT") {
await this.createTemplate();
Expand Down
43 changes: 34 additions & 9 deletions clients/tabby-agent/src/CompletionProviderStats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ type WindowedStats = {

export class CompletionProviderStats {
private readonly logger = rootLogger.child({ component: "CompletionProviderStats" });
private config = {
windowSize: 10,
checks: {
disable: false,
// Mark status as healthy if the latency is less than the threshold for each latest windowSize requests.
healthy: { windowSize: 3, latency: 2400 },
// If there is at least {count} requests, and the average response time is higher than the {latency}, show warning
slowResponseTime: { latency: 3200, count: 3 },
// If there is at least {count} timeouts, and the timeout rate is higher than the {rate}, show warning
highTimeoutRate: { rate: 0.5, count: 3 },
},
};

private autoCompletionCount = 0;
private manualCompletionCount = 0;
Expand All @@ -71,7 +83,13 @@ export class CompletionProviderStats {
private completionRequestCanceledStats = new Average();
private completionRequestTimeoutCount = 0;

private recentCompletionRequestLatencies = new Windowed(10);
private recentCompletionRequestLatencies: Windowed;

updateConfigByRequestTimeout(timeout: number) {
this.config.checks.healthy.latency = timeout * 0.6;
this.config.checks.slowResponseTime.latency = timeout * 0.8;
this.resetWindowed();
}

add(value: CompletionProviderStatsEntry): void {
const { triggerMode, cacheHit, aborted, requestSent, requestLatency, requestCanceled, requestTimeout } = value;
Expand Down Expand Up @@ -120,7 +138,7 @@ export class CompletionProviderStats {
}

resetWindowed() {
this.recentCompletionRequestLatencies = new Windowed(10);
this.recentCompletionRequestLatencies = new Windowed(this.config.windowSize);
}

// stats for anonymous usage report
Expand Down Expand Up @@ -170,21 +188,28 @@ export class CompletionProviderStats {
};
}

static check(windowed: WindowedStats): "healthy" | "highTimeoutRate" | "slowResponseTime" | null {
check(windowed: WindowedStats): "healthy" | "highTimeoutRate" | "slowResponseTime" | null {
if (!!this.config.checks.disable) {
return null;
}
const config = this.config.checks;

const {
values: latencies,
stats: { total, timeouts, responses, averageResponseTime },
} = windowed;
// if the recent 3 requests all have latency less than 3s
if (latencies.slice(-3).every((latency) => latency < 3000)) {

if (
latencies
.slice(-Math.max(this.config.windowSize, config.healthy.windowSize))
.every((latency) => latency < config.healthy.latency)
) {
return "healthy";
}
// if the recent requests timeout percentage is more than 50%, at least 3 timeouts
if (timeouts / total > 0.5 && timeouts >= 3) {
if (timeouts / total > config.highTimeoutRate.rate && timeouts >= config.highTimeoutRate.count) {
return "highTimeoutRate";
}
// if average response time is more than 4s, at least 3 requests
if (responses >= 3 && averageResponseTime > 4000) {
if (averageResponseTime > config.slowResponseTime.latency && responses >= config.slowResponseTime.count) {
return "slowResponseTime";
}
return null;
Expand Down
12 changes: 8 additions & 4 deletions clients/tabby-agent/src/TabbyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,12 @@ export class TabbyAgent extends EventEmitter implements Agent {
}
}

if (oldConfig.completion.timeout !== this.config.completion.timeout) {
this.completionProviderStats.updateConfigByRequestTimeout(this.config.completion.timeout);
this.popIssue("slowCompletionResponseTime");
this.popIssue("highCompletionTimeoutRate");
}

const event: AgentEvent = { event: "configUpdated", config: this.config };
this.logger.debug({ event }, "Config updated");
super.emit("configUpdated", event);
Expand Down Expand Up @@ -524,9 +530,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
},
{
signal,
timeout: request.manually
? this.config.completion.timeout.manually
: this.config.completion.timeout.auto,
timeout: this.config.completion.timeout,
},
);
stats.requestLatency = performance.now() - requestStartedAt;
Expand Down Expand Up @@ -588,7 +592,7 @@ export class TabbyAgent extends EventEmitter implements Agent {

if (stats.requestSent && !stats.requestCanceled) {
const windowedStats = this.completionProviderStats.windowed();
const checkResult = CompletionProviderStats.check(windowedStats);
const checkResult = this.completionProviderStats.check(windowedStats);
switch (checkResult) {
case "healthy":
this.popIssue("slowCompletionResponseTime");
Expand Down
12 changes: 12 additions & 0 deletions website/docs/extensions/configurations.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ Header1 = "Value1" # list your custom headers here
Header2 = "Value2" # values can be strings, numbers or booleans
```

## Completion

If you have changed the default response timeout at Tabby server side, you may also need to change the timeout configurations here.

```toml
# Completion
# (Since 1.1.0) You can set the completion request timeout here.
# Note that there is also a timeout config at the server side.
[completion]
timeout = 4000 # 4s
```

## Logs

If you encounter any issues with the Tabby IDE extensions and need to report a bug, you can enable debug logs to help us investigate the issue.
Expand Down

0 comments on commit ff03e2a

Please sign in to comment.