Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/base/cache.ts
13388 views
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
import KeyvSqlite from '@keyv/sqlite';
7
import { exec } from 'child_process';
8
import fs from 'fs';
9
import Keyv from 'keyv';
10
import { EventEmitter } from 'node:stream';
11
import path from 'path';
12
import { promisify } from 'util';
13
import zlib from 'zlib';
14
import { LockMap } from '../../src/util/common/lock';
15
import { generateUuid } from '../../src/util/vs/base/common/uuid';
16
import { CurrentTestRunInfo } from './simulationContext';
17
18
const compress = promisify(zlib.brotliCompress);
19
const decompress = promisify(zlib.brotliDecompress);
20
21
const DefaultCachePath = process.env.VITEST ? path.resolve(__dirname, '..', 'simulation', 'cache') : path.resolve(__dirname, '..', 'test', 'simulation', 'cache');
22
23
async function getGitRoot(cwd: string): Promise<string> {
24
const execAsync = promisify(exec);
25
const { stdout } = await execAsync('git rev-parse --show-toplevel', { cwd });
26
return stdout.trim();
27
}
28
29
export class Cache extends EventEmitter {
30
private static _Instance: Cache | undefined;
31
static get Instance() {
32
return this._Instance ?? (this._Instance = new Cache());
33
}
34
35
private readonly cachePath: string;
36
private readonly layersPath: string;
37
private readonly externalLayersPath?: string;
38
39
private readonly base: Keyv;
40
private readonly layers: Map<string, Keyv>;
41
private activeLayer: Promise<Keyv> | undefined;
42
43
private gcBase: Keyv | undefined;
44
private gcBaseKeys: Set<string> | undefined;
45
46
constructor(cachePath = DefaultCachePath) {
47
super();
48
49
this.cachePath = cachePath;
50
this.layersPath = path.join(this.cachePath, 'layers');
51
this.externalLayersPath = process.env.EXTERNAL_CACHE_LAYERS_PATH;
52
53
if (!fs.existsSync(path.join(this.cachePath, 'base.sqlite'))) {
54
throw new Error(`Base cache file does not exist as ${path.join(this.cachePath, 'base.sqlite')}.`);
55
}
56
57
if (this.externalLayersPath && !fs.existsSync(this.externalLayersPath)) {
58
throw new Error(`External layers cache directory provided but it does not exist at ${this.externalLayersPath}.`);
59
}
60
61
fs.mkdirSync(this.layersPath, { recursive: true });
62
this.base = new Keyv(new KeyvSqlite(path.join(this.cachePath, 'base.sqlite')));
63
64
this.layers = new Map();
65
let layerFiles = fs.readdirSync(this.layersPath)
66
.filter(file => file.endsWith('.sqlite'))
67
.map(file => path.join(this.layersPath, file));
68
69
if (this.externalLayersPath !== undefined) {
70
const externalLayerFiles = fs.readdirSync(this.externalLayersPath)
71
.filter(file => file.endsWith('.sqlite'))
72
.map(file => path.join(this.externalLayersPath!, file));
73
layerFiles = layerFiles.concat(externalLayerFiles);
74
}
75
76
for (const layerFile of layerFiles) {
77
const name = path.basename(layerFile, path.extname(layerFile));
78
this.layers.set(name, new Keyv(new KeyvSqlite(layerFile)));
79
}
80
}
81
82
async get(key: string): Promise<string | undefined> {
83
let data: string | undefined;
84
85
// First check base database
86
data = await this.base.get(key) as string;
87
88
if (!data) {
89
// Check layer databases
90
for (const [, layer] of this.layers) {
91
data = await layer.get(key) as string;
92
93
if (data) {
94
break;
95
}
96
}
97
}
98
99
if (!data) {
100
return undefined;
101
}
102
103
// GC mode in progress
104
if (this.gcBase && this.gcBaseKeys) {
105
if (!this.gcBaseKeys.has(key)) {
106
if (await this.gcBase.set(key, data)) {
107
this.gcBaseKeys.add(key);
108
}
109
}
110
}
111
112
return this._decompress(data);
113
}
114
115
async set(key: string, value: string, layer?: 'base' | string): Promise<void> {
116
if (await this.has(key)) {
117
throw new Error(`Key already exists in cache: ${key}`);
118
}
119
120
const data = await this._compress(value);
121
122
switch (layer) {
123
case undefined: {
124
const layerDatabase = await this._getActiveLayerDatabase();
125
await layerDatabase.set(key, data);
126
break;
127
}
128
case 'base': {
129
await this.base.set(key, data);
130
break;
131
}
132
default: {
133
const layerDatabase = this.layers.get(layer);
134
if (!layerDatabase) {
135
throw new Error(`Layer with UUID not found: ${layer}`);
136
}
137
await layerDatabase.set(key, data);
138
break;
139
}
140
}
141
142
}
143
144
async has(key: string): Promise<boolean> {
145
// Check primary first
146
if (await this.base.has(key)) {
147
return true;
148
}
149
150
// Check layers
151
for (const layer of this.layers.values()) {
152
if (await layer.has(key)) {
153
return true;
154
}
155
}
156
return false;
157
}
158
159
async checkDatabase(): Promise<Map<string, string[]>> {
160
const keys = new Map<string, string>();
161
const result = new Map<string, string[]>();
162
163
const checkDatabase = async (name: string, database: Keyv) => {
164
for await (const [key] of database.store.iterator()) {
165
if (result.has(key)) {
166
result.get(key)!.push(name);
167
} else if (keys.has(key)) {
168
result.set(key, [keys.get(key)!, name]);
169
keys.delete(key);
170
} else {
171
keys.set(key, name);
172
}
173
}
174
};
175
176
// Base database
177
await checkDatabase('base', this.base);
178
179
// Layer databases
180
for (const [uuid, database] of this.layers.entries()) {
181
await checkDatabase(uuid, database);
182
}
183
184
return result;
185
}
186
187
async gcStart(): Promise<void> {
188
if (this.gcBase || this.gcBaseKeys) {
189
throw new Error('GC is currently in progress');
190
}
191
192
this.gcBaseKeys = new Set<string>();
193
this.gcBase = new Keyv(new KeyvSqlite(path.join(this.cachePath, '_base.sqlite')));
194
}
195
196
async gcEnd(): Promise<void> {
197
if (!this.gcBase || !this.gcBaseKeys) {
198
throw new Error('GC is not in progress');
199
}
200
201
// Close the connections
202
await this.base.disconnect();
203
await this.gcBase.disconnect();
204
205
// Delete base.sqlite
206
fs.unlinkSync(path.join(this.cachePath, 'base.sqlite'));
207
208
// Rename _base.sqlite to base.sqlite
209
fs.renameSync(
210
path.join(this.cachePath, '_base.sqlite'),
211
path.join(this.cachePath, 'base.sqlite'));
212
213
// Delete the layer databases
214
for (const [uuid, layer] of this.layers.entries()) {
215
try {
216
// Close the connection
217
await layer.disconnect();
218
} catch (error) { }
219
220
try {
221
// Delete the layer database
222
fs.unlinkSync(path.join(this.layersPath, `${uuid}.sqlite`));
223
} catch (error) { }
224
}
225
226
this.activeLayer = undefined;
227
this.layers.clear();
228
229
this.gcBase = undefined;
230
this.gcBaseKeys.clear();
231
this.gcBaseKeys = undefined;
232
}
233
234
private async _getActiveLayerDatabase(): Promise<Keyv> {
235
if (!this.activeLayer) {
236
this.activeLayer = (async () => {
237
const execAsync = promisify(exec);
238
239
const activeLayerPath = this.externalLayersPath ?? this.layersPath;
240
const gitStatusPath = this.externalLayersPath
241
? `${path.relative(await getGitRoot(activeLayerPath), activeLayerPath)}/*`
242
: 'test/simulation/cache/layers/*';
243
244
// Check git for an uncommitted layer database file
245
try {
246
const gitRoot = await getGitRoot(activeLayerPath);
247
const { stdout: statusStdout } = await execAsync(`git status -z ${gitStatusPath}`, { cwd: gitRoot });
248
if (statusStdout !== '') {
249
const layerDatabaseEntries = statusStdout.split('\0').filter(entry => entry.endsWith('.sqlite'));
250
if (layerDatabaseEntries.length > 0) {
251
const regex = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.sqlite$/;
252
const match = layerDatabaseEntries[0].match(regex);
253
if (match && this.layers.has(match[1])) {
254
return this.layers.get(match[1])!;
255
}
256
}
257
}
258
} catch (error) {
259
// If git operations fail, continue to create new layer
260
}
261
262
// Create a new layer database
263
const uuid = generateUuid();
264
const activeLayer = new Keyv(new KeyvSqlite(path.join(activeLayerPath, `${uuid}.sqlite`)));
265
this.layers.set(uuid, activeLayer);
266
return activeLayer;
267
})();
268
}
269
270
return this.activeLayer;
271
}
272
273
private async _compress(value: string): Promise<string> {
274
const buffer = await compress(value, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 6, } });
275
return buffer.toString('base64');
276
}
277
278
private async _decompress(data: string): Promise<string> {
279
const buffer = await decompress(Buffer.from(data, 'base64'));
280
return buffer.toString('utf8');
281
}
282
}
283
284
export type CacheableRequest = {
285
readonly hash: string;
286
toJSON?(): unknown;
287
};
288
289
export interface ICache<TRequest, TResponse> {
290
get(req: TRequest): Promise<TResponse | undefined>;
291
set(req: TRequest, cachedResponse: TResponse): Promise<void>;
292
}
293
294
export class SQLiteCache<TRequest extends CacheableRequest, TResponse> implements ICache<TRequest, TResponse> {
295
296
private readonly namespace: string;
297
private readonly locks = new LockMap();
298
299
constructor(name: string, salt?: string, info?: CurrentTestRunInfo) {
300
this.namespace = `${name}${salt ? `|${salt}` : ''}`;
301
}
302
303
async hasRequest(hash: string): Promise<boolean> {
304
return Cache.Instance.has(`${this.namespace}:request:${hash}`);
305
}
306
307
async getRequest(hash: string): Promise<TRequest | undefined> {
308
const result = await Cache.Instance.get(`${this.namespace}:request:${hash}`);
309
return result ? JSON.parse(result) : undefined;
310
}
311
312
async setRequest(hash: string, value: TRequest): Promise<void> {
313
await Cache.Instance.set(`${this.namespace}:request:${hash}`, JSON.stringify(value));
314
}
315
316
async has(req: TRequest): Promise<boolean> {
317
return Cache.Instance.has(`${this.namespace}:response:${req.hash}`);
318
}
319
320
async get(req: TRequest): Promise<TResponse | undefined> {
321
const result = await Cache.Instance.get(`${this.namespace}:response:${req.hash}`);
322
return result ? JSON.parse(result) : undefined;
323
}
324
325
async set(req: TRequest, value: TResponse): Promise<void> {
326
await this.locks.withLock(req.hash, async () => {
327
if (!!req.toJSON && !await this.hasRequest(req.hash)) {
328
await this.setRequest(req.hash, req);
329
}
330
});
331
332
await Cache.Instance.set(`${this.namespace}:response:${req.hash}`, JSON.stringify(value));
333
}
334
}
335
336
export interface ISlottedCache<TRequest, TResponse> {
337
get(req: TRequest, cacheSlot: number): Promise<TResponse | undefined>;
338
set(req: TRequest, cacheSlot: number, cachedResponse: TResponse): Promise<void>;
339
}
340
341
export class SQLiteSlottedCache<TRequest extends CacheableRequest, TResponse> implements ISlottedCache<TRequest, TResponse> {
342
343
private readonly namespace: string;
344
private readonly locks = new LockMap();
345
346
constructor(name: string, salt: string, info?: CurrentTestRunInfo) {
347
this.namespace = `${name}|${salt}`;
348
}
349
350
async hasRequest(hash: string): Promise<boolean> {
351
return Cache.Instance.has(`${this.namespace}:request:${hash}`);
352
}
353
354
async getRequest(hash: string): Promise<TRequest | undefined> {
355
const result = await Cache.Instance.get(`${this.namespace}:request:${hash}`);
356
return result ? JSON.parse(result) : undefined;
357
}
358
359
async setRequest(hash: string, value: TRequest): Promise<void> {
360
await Cache.Instance.set(`${this.namespace}:request:${hash}`, JSON.stringify(value));
361
}
362
363
async has(req: TRequest, cacheSlot: number): Promise<boolean> {
364
return Cache.Instance.has(`${this.namespace}:response:${req.hash}:${cacheSlot}`);
365
}
366
367
async get(req: TRequest, cacheSlot: number): Promise<TResponse | undefined> {
368
const result = await Cache.Instance.get(`${this.namespace}:response:${req.hash}:${cacheSlot}`);
369
return result ? JSON.parse(result) : undefined;
370
}
371
372
async set(req: TRequest, cacheSlot: number, value: TResponse): Promise<void> {
373
await this.locks.withLock(req.hash, async () => {
374
if (!await this.hasRequest(req.hash)) {
375
await this.setRequest(req.hash, req);
376
}
377
});
378
379
await Cache.Instance.set(`${this.namespace}:response:${req.hash}:${cacheSlot}`, JSON.stringify(value));
380
}
381
}
382
383