Skip to content

Commit 422aed6

Browse files
memory optimization and revert test command in package.json to main
1 parent 93b96c3 commit 422aed6

File tree

7 files changed

+316
-265
lines changed

7 files changed

+316
-265
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,9 +401,9 @@
401401
"package": "npm-run-all -l -p build:webview build:esbuild check-types lint",
402402
"pretest": "npm run compile",
403403
"dev": "cd webview-ui && npm run dev",
404-
"test": "npm run test:extension",
405-
"test:extension": "node --max-old-space-size=8192 ./node_modules/.bin/jest --silent --runInBand --detectOpenHandles --testTimeout=10000",
406-
"test:extension:debug-memory": "node --max-old-space-size=8192 --trace-gc --expose-gc --heap-prof ./node_modules/.bin/jest --runInBand --logHeapUsage --detectOpenHandles --testTimeout=10000",
404+
"test": "node scripts/run-tests.js",
405+
"test:extension": "jest -w=40%",
406+
"test:extension:debug-memory": "node --max-old-space-size=4096 --trace-gc --expose-gc --heap-prof ./node_modules/.bin/jest --runInBand --logHeapUsage --detectOpenHandles --testTimeout=10000",
407407
"test:webview": "cd webview-ui && npm run test",
408408
"prepare": "husky",
409409
"publish:marketplace": "vsce publish && ovsx publish",

src/core/webview/packageManagerMessageHandler.ts

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -203,23 +203,13 @@ export async function handlePackageManagerMessages(
203203
case "filterPackageManagerItems": {
204204
if (message.filters) {
205205
try {
206-
// Get current items from the manager
207-
const items = packageManagerManager.getCurrentItems()
208-
// Apply filters using the manager's filtering logic
209-
const filteredItems = packageManagerManager.filterItems(items, {
206+
// Update filtered items and post state
207+
packageManagerManager.updateWithFilteredItems({
210208
type: message.filters.type as ComponentType | undefined,
211209
search: message.filters.search,
212210
tags: message.filters.tags,
213211
})
214-
// Get current state and merge filtered items
215-
const currentState = await provider.getStateToPostToWebview()
216-
await provider.postMessageToWebview({
217-
type: "state",
218-
state: {
219-
...currentState,
220-
packageManagerItems: filteredItems,
221-
},
222-
})
212+
await provider.postStateToWebview()
223213
} catch (error) {
224214
console.error("Package Manager: Error filtering items:", error)
225215
vscode.window.showErrorMessage("Failed to filter package manager items")

src/services/package-manager/PackageManagerManager.ts

Lines changed: 146 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,45 @@ export class PackageManagerManager {
287287
* @param filters The filter criteria
288288
* @returns Filtered items
289289
*/
290+
// Cache size limit to prevent memory issues
291+
private static readonly MAX_CACHE_SIZE = 100
292+
private filterCache = new Map<
293+
string,
294+
{
295+
items: PackageManagerItem[]
296+
timestamp: number
297+
}
298+
>()
299+
300+
/**
301+
* Clear old entries from the filter cache
302+
*/
303+
private cleanupFilterCache(): void {
304+
if (this.filterCache.size > PackageManagerManager.MAX_CACHE_SIZE) {
305+
// Sort by timestamp and keep only the most recent entries
306+
const entries = Array.from(this.filterCache.entries())
307+
.sort(([, a], [, b]) => b.timestamp - a.timestamp)
308+
.slice(0, PackageManagerManager.MAX_CACHE_SIZE)
309+
310+
this.filterCache.clear()
311+
entries.forEach(([key, value]) => this.filterCache.set(key, value))
312+
}
313+
}
314+
290315
filterItems(
291316
items: PackageManagerItem[],
292317
filters: { type?: ComponentType; search?: string; tags?: string[] },
293318
): PackageManagerItem[] {
319+
// Create cache key from filters
320+
const cacheKey = JSON.stringify(filters)
321+
const cached = this.filterCache.get(cacheKey)
322+
if (cached) {
323+
return cached.items
324+
}
325+
326+
// Clean up old cache entries
327+
this.cleanupFilterCache()
328+
294329
// Helper function to normalize text for case/whitespace-insensitive comparison
295330
const normalizeText = (text: string) => text.toLowerCase().replace(/\s+/g, " ").trim()
296331

@@ -303,144 +338,118 @@ export class PackageManagerManager {
303338
return normalizeText(text).includes(normalizeText(searchTerm))
304339
}
305340

306-
// Create a deep clone of all items
307-
const clonedItems = items.map((originalItem) => JSON.parse(JSON.stringify(originalItem)) as PackageManagerItem)
341+
// Filter items with shallow copies
342+
const filteredItems = items
343+
.map((item) => {
344+
// Create shallow copy of item
345+
const itemCopy = { ...item }
346+
347+
// Check parent item matches
348+
const itemMatches = {
349+
type: !filters.type || itemCopy.type === filters.type,
350+
search:
351+
!searchTerm || containsSearchTerm(itemCopy.name) || containsSearchTerm(itemCopy.description),
352+
tags:
353+
!filters.tags?.length ||
354+
(itemCopy.tags && filters.tags.some((tag) => itemCopy.tags!.includes(tag))),
355+
}
308356

309-
// Apply filters
310-
const filteredItems = clonedItems.filter((item) => {
311-
// Check parent item matches
312-
const itemMatches = {
313-
type: !filters.type || item.type === filters.type,
314-
search: !searchTerm || containsSearchTerm(item.name) || containsSearchTerm(item.description),
315-
tags: !filters.tags?.length || (item.tags && filters.tags.some((tag) => item.tags!.includes(tag))),
316-
}
357+
// Process subcomponents and track if any match
358+
let hasMatchingSubcomponents = false
359+
if (itemCopy.items?.length) {
360+
itemCopy.items = itemCopy.items.map((subItem) => {
361+
const subMatches = {
362+
type: !filters.type || subItem.type === filters.type,
363+
search:
364+
!searchTerm ||
365+
(subItem.metadata &&
366+
(containsSearchTerm(subItem.metadata.name) ||
367+
containsSearchTerm(subItem.metadata.description))),
368+
tags:
369+
!filters.tags?.length ||
370+
!!(
371+
subItem.metadata?.tags &&
372+
filters.tags.some((tag) => subItem.metadata!.tags!.includes(tag))
373+
),
374+
}
317375

318-
// Check subcomponent matches
319-
const subcomponentMatches =
320-
item.items?.some((subItem) => {
321-
const subMatches = {
322-
type: !filters.type || subItem.type === filters.type,
323-
search:
324-
!searchTerm ||
325-
(subItem.metadata &&
326-
(containsSearchTerm(subItem.metadata.name) ||
327-
containsSearchTerm(subItem.metadata.description))),
328-
tags:
329-
!filters.tags?.length ||
330-
(subItem.metadata?.tags &&
331-
filters.tags.some((tag) => subItem.metadata!.tags!.includes(tag))),
332-
}
376+
const subItemMatched =
377+
subMatches.type &&
378+
(!searchTerm || subMatches.search) &&
379+
(!filters.tags?.length || subMatches.tags)
380+
381+
if (subItemMatched) {
382+
hasMatchingSubcomponents = true
383+
// Set matchInfo for matching subcomponent
384+
// Build match reason based on active filters
385+
const matchReason: Record<string, boolean> = {}
386+
387+
if (searchTerm) {
388+
matchReason.nameMatch = containsSearchTerm(subItem.metadata?.name || "")
389+
matchReason.descriptionMatch = containsSearchTerm(subItem.metadata?.description || "")
390+
}
391+
392+
// Always include typeMatch when filtering by type
393+
if (filters.type) {
394+
matchReason.typeMatch = subMatches.type
395+
}
396+
397+
subItem.matchInfo = {
398+
matched: true,
399+
matchReason,
400+
}
401+
} else {
402+
subItem.matchInfo = { matched: false }
403+
}
333404

334-
// When filtering by type, require exact type match
335-
// For other filters (search/tags), any match is sufficient
336-
return (
337-
subMatches.type &&
338-
(!searchTerm || subMatches.search) &&
339-
(!filters.tags?.length || subMatches.tags)
340-
)
341-
}) ?? false
342-
343-
// Include item if either:
344-
// 1. Parent matches all active filters, or
345-
// 2. Parent is a package and any subcomponent matches any active filter
346-
const hasActiveFilters = filters.type || searchTerm || filters.tags?.length
347-
if (!hasActiveFilters) return true
348-
349-
const parentMatchesAll = itemMatches.type && itemMatches.search && itemMatches.tags
350-
const isPackageWithMatchingSubcomponent = item.type === "package" && subcomponentMatches
351-
return parentMatchesAll || isPackageWithMatchingSubcomponent
352-
})
405+
return subItem
406+
})
407+
}
353408

354-
// Add match info to filtered items
355-
return filteredItems.map((item) => {
356-
// Calculate parent item matches
357-
const itemMatches = {
358-
type: !filters.type || item.type === filters.type,
359-
search: !searchTerm || containsSearchTerm(item.name) || containsSearchTerm(item.description),
360-
tags: !filters.tags?.length || (item.tags && filters.tags.some((tag) => item.tags!.includes(tag))),
361-
}
409+
const hasActiveFilters = filters.type || searchTerm || filters.tags?.length
410+
if (!hasActiveFilters) return itemCopy
362411

363-
// Process subcomponents
364-
let hasMatchingSubcomponents = false
365-
if (item.items) {
366-
item.items = item.items.map((subItem) => {
367-
// Calculate individual filter matches for subcomponent
368-
const subMatches = {
369-
type: !filters.type || subItem.type === filters.type,
370-
search:
371-
!searchTerm ||
372-
(subItem.metadata &&
373-
(containsSearchTerm(subItem.metadata.name) ||
374-
containsSearchTerm(subItem.metadata.description))),
375-
tags:
376-
!filters.tags?.length ||
377-
(subItem.metadata?.tags &&
378-
filters.tags.some((tag) => subItem.metadata!.tags!.includes(tag))),
379-
}
412+
const parentMatchesAll = itemMatches.type && itemMatches.search && itemMatches.tags
413+
const isPackageWithMatchingSubcomponent = itemCopy.type === "package" && hasMatchingSubcomponents
380414

381-
// A subcomponent matches if it matches all active filters
382-
const subMatched = subMatches.type && subMatches.search && subMatches.tags
383-
384-
if (subMatched) {
385-
hasMatchingSubcomponents = true
386-
// Build match reason for matched subcomponent
387-
const matchReason: Record<string, boolean> = {
388-
...(searchTerm && {
389-
nameMatch: !!subItem.metadata && containsSearchTerm(subItem.metadata.name),
390-
descriptionMatch:
391-
!!subItem.metadata && containsSearchTerm(subItem.metadata.description),
392-
}),
393-
...(filters.type && { typeMatch: subMatches.type }),
394-
...(filters.tags?.length && { tagMatch: !!subMatches.tags }),
395-
}
415+
if (parentMatchesAll || isPackageWithMatchingSubcomponent) {
416+
// Add match info without deep cloning
417+
// Build parent match reason based on active filters
418+
const matchReason: Record<string, boolean> = {}
396419

397-
subItem.matchInfo = {
398-
matched: true,
399-
matchReason,
400-
}
420+
if (searchTerm) {
421+
matchReason.nameMatch = containsSearchTerm(itemCopy.name)
422+
matchReason.descriptionMatch = containsSearchTerm(itemCopy.description)
401423
} else {
402-
subItem.matchInfo = {
403-
matched: false,
404-
}
424+
matchReason.nameMatch = false
425+
matchReason.descriptionMatch = false
405426
}
406427

407-
return subItem
408-
})
409-
}
410-
411-
// Build match reason for parent item
412-
const matchReason: Record<string, boolean> = {
413-
nameMatch: searchTerm ? containsSearchTerm(item.name) : true,
414-
descriptionMatch: searchTerm ? containsSearchTerm(item.description) : true,
415-
}
416-
417-
if (filters.type) {
418-
matchReason.typeMatch = itemMatches.type
419-
}
420-
if (filters.tags?.length) {
421-
matchReason.tagMatch = !!itemMatches.tags
422-
}
423-
if (hasMatchingSubcomponents) {
424-
matchReason.hasMatchingSubcomponents = true
425-
}
426-
427-
// Parent item is matched if:
428-
// 1. It matches all active filters directly, or
429-
// 2. It's a package and has any matching subcomponents
430-
const parentMatchesAll =
431-
(!filters.type || itemMatches.type) &&
432-
(!searchTerm || itemMatches.search) &&
433-
(!filters.tags?.length || itemMatches.tags)
428+
// Always include typeMatch when filtering by type
429+
if (filters.type) {
430+
matchReason.typeMatch = itemMatches.type
431+
}
434432

435-
const isPackageWithMatchingSubcomponent = item.type === "package" && hasMatchingSubcomponents
433+
if (hasMatchingSubcomponents) {
434+
matchReason.hasMatchingSubcomponents = true
435+
}
436436

437-
item.matchInfo = {
438-
matched: parentMatchesAll || isPackageWithMatchingSubcomponent,
439-
matchReason,
440-
}
437+
itemCopy.matchInfo = {
438+
matched: true,
439+
matchReason,
440+
}
441+
return itemCopy
442+
}
443+
return null
444+
})
445+
.filter((item): item is PackageManagerItem => item !== null)
441446

442-
return item
447+
// Cache the results with timestamp
448+
this.filterCache.set(cacheKey, {
449+
items: filteredItems,
450+
timestamp: Date.now(),
443451
})
452+
return filteredItems
444453
}
445454

446455
/**
@@ -491,6 +500,17 @@ export class PackageManagerManager {
491500
return this.currentItems
492501
}
493502

503+
/**
504+
* Updates current items with filtered results
505+
* @param filters The filter criteria
506+
* @returns Filtered items
507+
*/
508+
updateWithFilteredItems(filters: { type?: ComponentType; search?: string; tags?: string[] }): PackageManagerItem[] {
509+
const filteredItems = this.filterItems(this.currentItems, filters)
510+
this.currentItems = filteredItems
511+
return filteredItems
512+
}
513+
494514
/**
495515
* Cleans up resources used by the package manager
496516
*/
@@ -499,6 +519,8 @@ export class PackageManagerManager {
499519
const sources = Array.from(this.cache.keys()).map((url) => ({ url, enabled: true }))
500520
await this.cleanupCacheDirectories(sources)
501521
this.clearCache()
522+
// Clear filter cache
523+
this.filterCache.clear()
502524
}
503525

504526
/**

0 commit comments

Comments
 (0)