Skip to content

Commit bcdcda3

Browse files
committed
PR
1 parent 36ef007 commit bcdcda3

15 files changed

Lines changed: 925 additions & 44 deletions

File tree

.github/workflows/chat-perf.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ jobs:
7171
libnotify-bin libkrb5-dev \
7272
xvfb sqlite3 \
7373
libnss3 libatk1.0-0 libatk-bridge2.0-0 \
74-
libcups2 libdrm2 libxcomposite1 libxdamage1 \
74+
libcups2t64 libdrm2 libxcomposite1 libxdamage1 \
7575
libxrandr2 libgbm1 libpango-1.0-0 libcairo2 \
76-
libasound2 libxshmfence1 libgtk-3-0
76+
libasound2t64 libxshmfence1 libgtk-3-0
7777
7878
- name: Install dependencies
7979
run: npm ci

build/filters.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export const copyrightFilter = Object.freeze<string[]>([
162162
'**',
163163
'!**/*.desktop',
164164
'!**/*.json',
165+
'!**/*.jsonc',
165166
'!**/*.jsonl',
166167
'!**/*.html',
167168
'!**/*.template',

scripts/chat-simulation/common/perf-scenarios.js

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
const path = require('path');
1616
const { ScenarioBuilder, registerScenario } = require('./mock-llm-server');
1717

18-
const ROOT = path.join(__dirname, '..', '..', '..');
18+
const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures');
1919

2020
// -- Content-only scenarios ---------------------------------------------------
2121

@@ -232,14 +232,14 @@ const TOOL_CALL_SCENARIOS = {
232232
// a real agent gathering context before answering.
233233
'tool-read-file': /** @type {import('./mock-llm-server').MultiTurnScenario} */ ((() => {
234234
const filesToRead = [
235-
'src/vs/base/common/lifecycle.ts',
236-
'src/vs/base/common/event.ts',
237-
'src/vs/base/common/uri.ts',
238-
'src/vs/base/common/errors.ts',
239-
'src/vs/base/common/async.ts',
240-
'src/vs/base/common/strings.ts',
241-
'src/vs/base/common/arrays.ts',
242-
'src/vs/base/common/types.ts',
235+
'lifecycle.ts',
236+
'event.ts',
237+
'uri.ts',
238+
'errors.ts',
239+
'async.ts',
240+
'strings.ts',
241+
'arrays.ts',
242+
'types.ts',
243243
];
244244
// Round 1: parallel read of first 4 files
245245
// Round 2: parallel read of next 4 files
@@ -251,14 +251,14 @@ const TOOL_CALL_SCENARIOS = {
251251
kind: 'tool-calls',
252252
toolCalls: filesToRead.slice(0, 4).map(f => ({
253253
toolNamePattern: /read.?file/i,
254-
arguments: { filePath: path.join(ROOT, f), startLine: 1, endLine: 50 },
254+
arguments: { filePath: path.join(FIXTURES_DIR, f), startLine: 1, endLine: 50 },
255255
})),
256256
},
257257
{
258258
kind: 'tool-calls',
259259
toolCalls: filesToRead.slice(4).map(f => ({
260260
toolNamePattern: /read.?file/i,
261-
arguments: { filePath: path.join(ROOT, f), startLine: 1, endLine: 50 },
261+
arguments: { filePath: path.join(FIXTURES_DIR, f), startLine: 1, endLine: 50 },
262262
})),
263263
},
264264
{
@@ -296,9 +296,9 @@ const TOOL_CALL_SCENARIOS = {
296296
// a real agent reading context and making multiple edits.
297297
'tool-edit-file': /** @type {import('./mock-llm-server').MultiTurnScenario} */ ((() => {
298298
const readFiles = [
299-
'src/vs/base/common/lifecycle.ts',
300-
'src/vs/base/common/event.ts',
301-
'src/vs/base/common/errors.ts',
299+
'lifecycle.ts',
300+
'event.ts',
301+
'errors.ts',
302302
];
303303
return {
304304
type: 'multi-turn',
@@ -308,7 +308,7 @@ const TOOL_CALL_SCENARIOS = {
308308
kind: 'tool-calls',
309309
toolCalls: readFiles.map(f => ({
310310
toolNamePattern: /read.?file/i,
311-
arguments: { filePath: path.join(ROOT, f), startLine: 1, endLine: 40 },
311+
arguments: { filePath: path.join(FIXTURES_DIR, f), startLine: 1, endLine: 40 },
312312
})),
313313
},
314314
// Round 2: edit 2 files in parallel
@@ -318,7 +318,7 @@ const TOOL_CALL_SCENARIOS = {
318318
{
319319
toolNamePattern: /replace.?string|apply.?patch|insert.?edit/i,
320320
arguments: {
321-
filePath: path.join(ROOT, 'src/vs/base/common/lifecycle.ts'),
321+
filePath: path.join(FIXTURES_DIR, 'lifecycle.ts'),
322322
oldString: '// perf-benchmark-marker',
323323
newString: '// perf-benchmark-marker (updated)',
324324
explanation: 'Update the benchmark marker comment in lifecycle.ts',
@@ -327,7 +327,7 @@ const TOOL_CALL_SCENARIOS = {
327327
{
328328
toolNamePattern: /replace.?string|apply.?patch|insert.?edit/i,
329329
arguments: {
330-
filePath: path.join(ROOT, 'src/vs/base/common/event.ts'),
330+
filePath: path.join(FIXTURES_DIR, 'event.ts'),
331331
oldString: '// perf-benchmark-marker',
332332
newString: '// perf-benchmark-marker (updated)',
333333
explanation: 'Update the benchmark marker comment in event.ts',
@@ -399,7 +399,7 @@ const MULTI_TURN_SCENARIOS = {
399399
{
400400
toolNamePattern: /read.?file/i,
401401
arguments: {
402-
filePath: path.join(ROOT, 'src/vs/base/common/lifecycle.ts'),
402+
filePath: path.join(FIXTURES_DIR, 'lifecycle.ts'),
403403
offset: 1,
404404
limit: 50,
405405
},

scripts/chat-simulation/common/utils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ const { execSync, spawn } = require('child_process');
1818
const ROOT = path.join(__dirname, '..', '..', '..');
1919
const DATA_DIR = path.join(ROOT, '.chat-simulation-data');
2020

21+
// -- Config loading ----------------------------------------------------------
22+
23+
/** @param {string} text */
24+
function stripJsoncComments(text) { return text.replace(/\/\/.*/g, '').replace(/\/\*[\s\S]*?\*\//g, ''); }
25+
26+
/**
27+
* Load a namespaced section from config.jsonc.
28+
* @param {string} section - Top-level key (e.g. 'perfRegression', 'memLeaks')
29+
* @returns {Record<string, any>}
30+
*/
31+
function loadConfig(section) {
32+
const raw = fs.readFileSync(path.join(__dirname, '..', 'config.jsonc'), 'utf-8');
33+
const config = JSON.parse(stripJsoncComments(raw));
34+
return config[section] ?? {};
35+
}
36+
2137
// -- Electron path resolution ------------------------------------------------
2238

2339
function getElectronPath() {
@@ -625,6 +641,7 @@ module.exports = {
625641
ROOT,
626642
DATA_DIR,
627643
METRIC_DEFS,
644+
loadConfig,
628645
getElectronPath,
629646
isVersionString,
630647
resolveBuild,
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"perfRegression": {
3+
// VS Code version, "insiders", or a commit hash (7-40 hex chars)
4+
"baselineBuild": "1.115.0",
5+
6+
// Number of benchmark iterations per scenario
7+
"runsPerScenario": 5,
8+
9+
// Fraction above baseline that triggers a regression (0.2 = 20%)
10+
"regressionThreshold": 0.2
11+
},
12+
"memLeaks": {
13+
// Number of chat messages to send during the leak check
14+
"messages": 10,
15+
16+
// Max acceptable heap growth per message in MB
17+
"leakThresholdMB": 2
18+
}
19+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
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+
// perf-benchmark-marker
7+
8+
/**
9+
* Fixture for chat-simulation benchmarks.
10+
* Simplified from src/vs/base/common/arrays.ts for stable perf testing.
11+
*/
12+
13+
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
14+
return array.filter((e): e is T => e !== undefined && e !== null);
15+
}
16+
17+
export function groupBy<T>(data: ReadonlyArray<T>, groupFn: (element: T) => string): { [key: string]: T[] } {
18+
const result: { [key: string]: T[] } = {};
19+
for (const element of data) {
20+
const key = groupFn(element);
21+
(result[key] ??= []).push(element);
22+
}
23+
return result;
24+
}
25+
26+
export function distinct<T>(array: ReadonlyArray<T>, keyFn: (t: T) => any = t => t): T[] {
27+
const seen = new Set<any>();
28+
return array.filter(element => {
29+
const key = keyFn(element);
30+
if (seen.has(key)) { return false; }
31+
seen.add(key);
32+
return true;
33+
});
34+
}
35+
36+
export function firstOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
37+
export function firstOrDefault<T>(array: ReadonlyArray<T>, defaultValue: T): T;
38+
export function firstOrDefault<T>(array: ReadonlyArray<T>, defaultValue?: T): T | undefined {
39+
return array.length > 0 ? array[0] : defaultValue;
40+
}
41+
42+
export function lastOrDefault<T>(array: ReadonlyArray<T>): T | undefined;
43+
export function lastOrDefault<T>(array: ReadonlyArray<T>, defaultValue: T): T;
44+
export function lastOrDefault<T>(array: ReadonlyArray<T>, defaultValue?: T): T | undefined {
45+
return array.length > 0 ? array[array.length - 1] : defaultValue;
46+
}
47+
48+
export function binarySearch<T>(array: ReadonlyArray<T>, key: T, comparator: (a: T, b: T) => number): number {
49+
let low = 0;
50+
let high = array.length - 1;
51+
while (low <= high) {
52+
const mid = ((low + high) / 2) | 0;
53+
const comp = comparator(array[mid], key);
54+
if (comp < 0) { low = mid + 1; }
55+
else if (comp > 0) { high = mid - 1; }
56+
else { return mid; }
57+
}
58+
return -(low + 1);
59+
}
60+
61+
export function insertSorted<T>(array: T[], element: T, comparator: (a: T, b: T) => number): void {
62+
const idx = binarySearch(array, element, comparator);
63+
const insertIdx = idx < 0 ? ~idx : idx;
64+
array.splice(insertIdx, 0, element);
65+
}
66+
67+
export function flatten<T>(arr: T[][]): T[] {
68+
return ([] as T[]).concat(...arr);
69+
}
70+
71+
export function range(to: number): number[];
72+
export function range(from: number, to: number): number[];
73+
export function range(arg: number, to?: number): number[] {
74+
const from = to !== undefined ? arg : 0;
75+
const end = to !== undefined ? to : arg;
76+
const result: number[] = [];
77+
for (let i = from; i < end; i++) { result.push(i); }
78+
return result;
79+
}
80+
81+
export function tail<T>(array: T[]): [T[], T] {
82+
if (array.length === 0) { throw new Error('Invalid tail call'); }
83+
return [array.slice(0, array.length - 1), array[array.length - 1]];
84+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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+
// perf-benchmark-marker
7+
8+
/**
9+
* Fixture for chat-simulation benchmarks.
10+
* Simplified from src/vs/base/common/async.ts for stable perf testing.
11+
*/
12+
13+
import { IDisposable } from './lifecycle';
14+
import { CancellationError } from './errors';
15+
16+
export class Throttler {
17+
private activePromise: Promise<any> | null = null;
18+
private queuedPromiseFactory: (() => Promise<any>) | null = null;
19+
20+
queue<T>(promiseFactory: () => Promise<T>): Promise<T> {
21+
if (this.activePromise) {
22+
this.queuedPromiseFactory = promiseFactory;
23+
return this.activePromise as Promise<T>;
24+
}
25+
this.activePromise = promiseFactory();
26+
return this.activePromise.finally(() => {
27+
this.activePromise = null;
28+
if (this.queuedPromiseFactory) {
29+
const factory = this.queuedPromiseFactory;
30+
this.queuedPromiseFactory = null;
31+
return this.queue(factory);
32+
}
33+
});
34+
}
35+
}
36+
37+
export class Delayer<T> implements IDisposable {
38+
private timeout: any;
39+
private task: (() => T | Promise<T>) | null = null;
40+
41+
constructor(public defaultDelay: number) { }
42+
43+
trigger(task: () => T | Promise<T>, delay: number = this.defaultDelay): Promise<T> {
44+
this.task = task;
45+
this.cancelTimeout();
46+
return new Promise<T>((resolve, reject) => {
47+
this.timeout = setTimeout(() => {
48+
this.timeout = null;
49+
try { resolve(this.task!()); } catch (e) { reject(e); }
50+
this.task = null;
51+
}, delay);
52+
});
53+
}
54+
55+
private cancelTimeout(): void {
56+
if (this.timeout !== null) {
57+
clearTimeout(this.timeout);
58+
this.timeout = null;
59+
}
60+
}
61+
62+
dispose(): void {
63+
this.cancelTimeout();
64+
}
65+
}
66+
67+
export class RunOnceScheduler implements IDisposable {
68+
private runner: (() => void) | null;
69+
private timeout: any;
70+
71+
constructor(runner: () => void, private delay: number) {
72+
this.runner = runner;
73+
}
74+
75+
schedule(delay = this.delay): void {
76+
this.cancel();
77+
this.timeout = setTimeout(() => {
78+
this.timeout = null;
79+
this.runner?.();
80+
}, delay);
81+
}
82+
83+
cancel(): void {
84+
if (this.timeout !== null) {
85+
clearTimeout(this.timeout);
86+
this.timeout = null;
87+
}
88+
}
89+
90+
isScheduled(): boolean { return this.timeout !== null; }
91+
92+
dispose(): void {
93+
this.cancel();
94+
this.runner = null;
95+
}
96+
}
97+
98+
export class Queue<T> {
99+
private readonly queue: Array<() => Promise<T>> = [];
100+
private running = false;
101+
102+
async enqueue(factory: () => Promise<T>): Promise<T> {
103+
return new Promise<T>((resolve, reject) => {
104+
this.queue.push(() => factory().then(resolve, reject));
105+
if (!this.running) { this.processQueue(); }
106+
});
107+
}
108+
109+
private async processQueue(): Promise<void> {
110+
this.running = true;
111+
while (this.queue.length > 0) {
112+
const task = this.queue.shift()!;
113+
await task();
114+
}
115+
this.running = false;
116+
}
117+
118+
get size(): number { return this.queue.length; }
119+
}
120+
121+
export function timeout(millis: number): Promise<void> {
122+
return new Promise<void>(resolve => setTimeout(resolve, millis));
123+
}
124+
125+
export async function retry<T>(task: () => Promise<T>, delay: number, retries: number): Promise<T> {
126+
let lastError: Error | undefined;
127+
for (let i = 0; i < retries; i++) {
128+
try { return await task(); }
129+
catch (error) { lastError = error as Error; await timeout(delay); }
130+
}
131+
throw lastError;
132+
}

0 commit comments

Comments
 (0)