mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
refactor: remove configurable memory optimization settings
- Remove memory optimization settings from codebase-index schema - Remove memory optimization properties from config-manager - Update qdrant-client to always use memory optimization defaults - Update service-factory to remove memory optimization config passing - Update tests to remove memory optimization configuration expectations Memory optimizations are now always enabled as requested by @daniel-lxs
This commit is contained in:
parent
2b3d8aa4d1
commit
c519495201
6 changed files with 10 additions and 158 deletions
|
|
@ -34,10 +34,6 @@ export const codebaseIndexConfigSchema = z.object({
|
|||
// OpenAI Compatible specific fields
|
||||
codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
|
||||
codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(),
|
||||
// Memory optimization settings
|
||||
codebaseIndexUseOnDiskStorage: z.boolean().optional(),
|
||||
codebaseIndexMemoryMapThreshold: z.number().optional(),
|
||||
codebaseIndexHnswEfSearch: z.number().optional(),
|
||||
})
|
||||
|
||||
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>
|
||||
|
|
|
|||
|
|
@ -113,9 +113,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
expect(result.requiresRestart).toBe(false)
|
||||
})
|
||||
|
|
@ -151,9 +148,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://qdrant.local",
|
||||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -194,9 +188,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://qdrant.local",
|
||||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -237,9 +228,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://qdrant.local",
|
||||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -281,9 +269,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://qdrant.local",
|
||||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -324,9 +309,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantUrl: "http://qdrant.local",
|
||||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1337,9 +1319,6 @@ describe("CodeIndexConfigManager", () => {
|
|||
qdrantApiKey: "test-qdrant-key",
|
||||
searchMinScore: 0.4,
|
||||
searchMaxResults: 50,
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
|
||||
mockConfigManager = {
|
||||
getConfig: vitest.fn(),
|
||||
memoryOptimizationConfig: {
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
}
|
||||
|
||||
mockCacheManager = {}
|
||||
|
|
@ -372,11 +367,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
3072,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -402,11 +392,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
768,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -432,11 +417,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
3072,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -469,11 +449,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
modelDimension, // Should use model's built-in dimension, not manual
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -505,11 +480,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
manualDimension, // Should use manual dimension as fallback
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -539,11 +509,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
768,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -613,11 +578,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
3072,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -643,11 +603,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
3072,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
@ -672,11 +627,6 @@ describe("CodeIndexServiceFactory", () => {
|
|||
"http://localhost:6333",
|
||||
1536,
|
||||
"test-key",
|
||||
{
|
||||
useOnDiskStorage: true,
|
||||
memoryMapThreshold: 50000,
|
||||
hnswEfSearch: 128,
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@ export class CodeIndexConfigManager {
|
|||
private qdrantApiKey?: string
|
||||
private searchMinScore?: number
|
||||
private searchMaxResults?: number
|
||||
// Memory optimization settings
|
||||
private useOnDiskStorage?: boolean
|
||||
private memoryMapThreshold?: number
|
||||
private hnswEfSearch?: number
|
||||
|
||||
constructor(private readonly contextProxy: ContextProxy) {
|
||||
// Initialize with current configuration to avoid false restart triggers
|
||||
|
|
@ -55,9 +51,6 @@ export class CodeIndexConfigManager {
|
|||
codebaseIndexEmbedderModelId: "",
|
||||
codebaseIndexSearchMinScore: undefined,
|
||||
codebaseIndexSearchMaxResults: undefined,
|
||||
codebaseIndexUseOnDiskStorage: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
codebaseIndexMemoryMapThreshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
codebaseIndexHnswEfSearch: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH,
|
||||
}
|
||||
|
||||
const {
|
||||
|
|
@ -68,9 +61,6 @@ export class CodeIndexConfigManager {
|
|||
codebaseIndexEmbedderModelId,
|
||||
codebaseIndexSearchMinScore,
|
||||
codebaseIndexSearchMaxResults,
|
||||
codebaseIndexUseOnDiskStorage,
|
||||
codebaseIndexMemoryMapThreshold,
|
||||
codebaseIndexHnswEfSearch,
|
||||
} = codebaseIndexConfig
|
||||
|
||||
const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
|
||||
|
|
@ -87,10 +77,6 @@ export class CodeIndexConfigManager {
|
|||
this.qdrantApiKey = qdrantApiKey ?? ""
|
||||
this.searchMinScore = codebaseIndexSearchMinScore
|
||||
this.searchMaxResults = codebaseIndexSearchMaxResults
|
||||
this.useOnDiskStorage = codebaseIndexUseOnDiskStorage ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE
|
||||
this.memoryMapThreshold =
|
||||
codebaseIndexMemoryMapThreshold ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD
|
||||
this.hnswEfSearch = codebaseIndexHnswEfSearch ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH
|
||||
|
||||
// Validate and set model dimension
|
||||
const rawDimension = codebaseIndexConfig.codebaseIndexEmbedderModelDimension
|
||||
|
|
@ -159,9 +145,6 @@ export class CodeIndexConfigManager {
|
|||
qdrantUrl?: string
|
||||
qdrantApiKey?: string
|
||||
searchMinScore?: number
|
||||
useOnDiskStorage?: boolean
|
||||
memoryMapThreshold?: number
|
||||
hnswEfSearch?: number
|
||||
}
|
||||
requiresRestart: boolean
|
||||
}> {
|
||||
|
|
@ -205,9 +188,6 @@ export class CodeIndexConfigManager {
|
|||
qdrantUrl: this.qdrantUrl,
|
||||
qdrantApiKey: this.qdrantApiKey,
|
||||
searchMinScore: this.currentSearchMinScore,
|
||||
useOnDiskStorage: this.useOnDiskStorage,
|
||||
memoryMapThreshold: this.memoryMapThreshold,
|
||||
hnswEfSearch: this.hnswEfSearch,
|
||||
},
|
||||
requiresRestart,
|
||||
}
|
||||
|
|
@ -400,9 +380,6 @@ export class CodeIndexConfigManager {
|
|||
qdrantApiKey: this.qdrantApiKey,
|
||||
searchMinScore: this.currentSearchMinScore,
|
||||
searchMaxResults: this.currentSearchMaxResults,
|
||||
useOnDiskStorage: this.useOnDiskStorage,
|
||||
memoryMapThreshold: this.memoryMapThreshold,
|
||||
hnswEfSearch: this.hnswEfSearch,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -437,21 +414,6 @@ export class CodeIndexConfigManager {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the memory optimization settings
|
||||
*/
|
||||
public get memoryOptimizationConfig(): {
|
||||
useOnDiskStorage?: boolean
|
||||
memoryMapThreshold?: number
|
||||
hnswEfSearch?: number
|
||||
} {
|
||||
return {
|
||||
useOnDiskStorage: this.useOnDiskStorage,
|
||||
memoryMapThreshold: this.memoryMapThreshold,
|
||||
hnswEfSearch: this.hnswEfSearch,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current model ID being used for embeddings.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -136,17 +136,8 @@ export class CodeIndexServiceFactory {
|
|||
throw new Error(t("embeddings:serviceFactory.qdrantUrlMissing"))
|
||||
}
|
||||
|
||||
// Get memory optimization config from config manager
|
||||
const memoryOptimization = this.configManager.memoryOptimizationConfig
|
||||
|
||||
// Create QdrantVectorStore with memory optimization settings
|
||||
return new QdrantVectorStore(
|
||||
this.workspacePath,
|
||||
config.qdrantUrl,
|
||||
vectorSize,
|
||||
config.qdrantApiKey,
|
||||
memoryOptimization,
|
||||
)
|
||||
// Create QdrantVectorStore with memory optimization always enabled
|
||||
return new QdrantVectorStore(this.workspacePath, config.qdrantUrl, vectorSize, config.qdrantApiKey)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { Payload, VectorStoreSearchResult } from "../interfaces"
|
|||
import { DEFAULT_MAX_SEARCH_RESULTS, DEFAULT_SEARCH_MIN_SCORE } from "../constants"
|
||||
import { t } from "../../../i18n"
|
||||
import {
|
||||
QdrantMemoryOptimizationConfig,
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS,
|
||||
QDRANT_HNSW_CONFIG_DEFAULTS,
|
||||
QDRANT_OPTIMIZER_CONFIG_DEFAULTS,
|
||||
|
|
@ -24,7 +23,6 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
private client: QdrantClient
|
||||
private readonly collectionName: string
|
||||
private readonly qdrantUrl: string = "http://localhost:6333"
|
||||
private readonly memoryOptimization: QdrantMemoryOptimizationConfig
|
||||
|
||||
/**
|
||||
* Creates a new Qdrant vector store
|
||||
|
|
@ -32,15 +30,8 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
* @param url Optional URL to the Qdrant server
|
||||
* @param vectorSize Size of the vectors
|
||||
* @param apiKey Optional API key for authentication
|
||||
* @param memoryOptimization Optional memory optimization settings
|
||||
*/
|
||||
constructor(
|
||||
workspacePath: string,
|
||||
url: string,
|
||||
vectorSize: number,
|
||||
apiKey?: string,
|
||||
memoryOptimization?: QdrantMemoryOptimizationConfig,
|
||||
) {
|
||||
constructor(workspacePath: string, url: string, vectorSize: number, apiKey?: string) {
|
||||
// Parse the URL to determine the appropriate QdrantClient configuration
|
||||
const parsedUrl = this.parseQdrantUrl(url)
|
||||
|
||||
|
|
@ -96,11 +87,6 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
const hash = createHash("sha256").update(workspacePath).digest("hex")
|
||||
this.vectorSize = vectorSize
|
||||
this.collectionName = `ws-${hash.substring(0, 16)}`
|
||||
this.memoryOptimization = memoryOptimization || {
|
||||
useOnDiskStorage: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
memoryMapThreshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
hnswEfSearch: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -177,9 +163,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
vectors: {
|
||||
size: this.vectorSize,
|
||||
distance: this.DISTANCE_METRIC,
|
||||
on_disk:
|
||||
this.memoryOptimization.useOnDiskStorage ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
on_disk: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
},
|
||||
// Configure HNSW index for memory efficiency
|
||||
hnsw_config: {
|
||||
|
|
@ -187,9 +171,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
ef_construct: QDRANT_HNSW_CONFIG_DEFAULTS.EF_CONSTRUCT,
|
||||
full_scan_threshold: QDRANT_HNSW_CONFIG_DEFAULTS.FULL_SCAN_THRESHOLD,
|
||||
max_indexing_threads: QDRANT_HNSW_CONFIG_DEFAULTS.MAX_INDEXING_THREADS,
|
||||
on_disk:
|
||||
this.memoryOptimization.useOnDiskStorage ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
on_disk: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
payload_m: QDRANT_HNSW_CONFIG_DEFAULTS.PAYLOAD_M,
|
||||
},
|
||||
// Enable memory-mapped storage for better memory management
|
||||
|
|
@ -198,9 +180,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
vacuum_min_vector_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.VACUUM_MIN_VECTOR_NUMBER,
|
||||
default_segment_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DEFAULT_SEGMENT_NUMBER,
|
||||
max_segment_size: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_SEGMENT_SIZE,
|
||||
memmap_threshold:
|
||||
this.memoryOptimization.memoryMapThreshold ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
memmap_threshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
indexing_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.INDEXING_THRESHOLD,
|
||||
flush_interval_sec: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.FLUSH_INTERVAL_SEC,
|
||||
max_optimization_threads: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_OPTIMIZATION_THREADS,
|
||||
|
|
@ -293,9 +273,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
vectors: {
|
||||
size: this.vectorSize,
|
||||
distance: this.DISTANCE_METRIC,
|
||||
on_disk:
|
||||
this.memoryOptimization.useOnDiskStorage ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
on_disk: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
},
|
||||
// Configure HNSW index for memory efficiency
|
||||
hnsw_config: {
|
||||
|
|
@ -303,9 +281,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
ef_construct: QDRANT_HNSW_CONFIG_DEFAULTS.EF_CONSTRUCT,
|
||||
full_scan_threshold: QDRANT_HNSW_CONFIG_DEFAULTS.FULL_SCAN_THRESHOLD,
|
||||
max_indexing_threads: QDRANT_HNSW_CONFIG_DEFAULTS.MAX_INDEXING_THREADS,
|
||||
on_disk:
|
||||
this.memoryOptimization.useOnDiskStorage ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
on_disk: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.USE_ON_DISK_STORAGE,
|
||||
payload_m: QDRANT_HNSW_CONFIG_DEFAULTS.PAYLOAD_M,
|
||||
},
|
||||
// Enable memory-mapped storage for better memory management
|
||||
|
|
@ -314,9 +290,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
vacuum_min_vector_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.VACUUM_MIN_VECTOR_NUMBER,
|
||||
default_segment_number: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.DEFAULT_SEGMENT_NUMBER,
|
||||
max_segment_size: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_SEGMENT_SIZE,
|
||||
memmap_threshold:
|
||||
this.memoryOptimization.memoryMapThreshold ??
|
||||
QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
memmap_threshold: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.MEMORY_MAP_THRESHOLD,
|
||||
indexing_threshold: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.INDEXING_THRESHOLD,
|
||||
flush_interval_sec: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.FLUSH_INTERVAL_SEC,
|
||||
max_optimization_threads: QDRANT_OPTIMIZER_CONFIG_DEFAULTS.MAX_OPTIMIZATION_THREADS,
|
||||
|
|
@ -467,7 +441,7 @@ export class QdrantVectorStore implements IVectorStore {
|
|||
score_threshold: minScore ?? DEFAULT_SEARCH_MIN_SCORE,
|
||||
limit: maxResults ?? DEFAULT_MAX_SEARCH_RESULTS,
|
||||
params: {
|
||||
hnsw_ef: this.memoryOptimization.hnswEfSearch ?? QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH,
|
||||
hnsw_ef: QDRANT_MEMORY_OPTIMIZATION_DEFAULTS.HNSW_EF_SEARCH,
|
||||
exact: false,
|
||||
quantization: {
|
||||
ignore: QDRANT_QUANTIZATION_CONFIG_DEFAULTS.IGNORE,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue