mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
revert: revert gantt component
This commit is contained in:
parent
d20df838de
commit
3a0dd7f4ff
9 changed files with 2349 additions and 410 deletions
|
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
"<rootDir>/src/__mocks__/codemirror-language.ts",
|
||||
"^@codemirror/search$": "<rootDir>/src/__mocks__/codemirror-search.ts",
|
||||
"\\.(css|less|scss|sass)$": "<rootDir>/src/__mocks__/styleMock.js",
|
||||
".*\\.worker$": "<rootDir>/src/__mocks__/ProjectData.worker.ts",
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
|
|
|
|||
47
src/__mocks__/ProjectData.worker.ts
Normal file
47
src/__mocks__/ProjectData.worker.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* Mock for ProjectData.worker.ts in test environment
|
||||
*/
|
||||
|
||||
export default class MockProjectDataWorker {
|
||||
private messageHandler: ((event: MessageEvent) => void) | null = null;
|
||||
|
||||
constructor() {
|
||||
// Mock worker constructor
|
||||
}
|
||||
|
||||
postMessage(message: any) {
|
||||
// Mock postMessage - simulate immediate response
|
||||
setTimeout(() => {
|
||||
if (this.messageHandler) {
|
||||
const mockResponse = {
|
||||
data: {
|
||||
type: "projectDataResult",
|
||||
requestId: message.requestId,
|
||||
success: true,
|
||||
data: {
|
||||
filePath: message.filePath || "test.md",
|
||||
tgProject: {
|
||||
type: "test",
|
||||
name: "Test Project",
|
||||
source: "mock",
|
||||
readonly: true,
|
||||
},
|
||||
enhancedMetadata: {},
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
},
|
||||
};
|
||||
this.messageHandler(mockResponse as MessageEvent);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
set onmessage(handler: (event: MessageEvent) => void) {
|
||||
this.messageHandler = handler;
|
||||
}
|
||||
|
||||
terminate() {
|
||||
// Mock terminate
|
||||
this.messageHandler = null;
|
||||
}
|
||||
}
|
||||
198
src/__tests__/ProjectDataWorkerManager.test.ts
Normal file
198
src/__tests__/ProjectDataWorkerManager.test.ts
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
/**
|
||||
* Test for ProjectDataWorkerManager
|
||||
*/
|
||||
|
||||
import { ProjectDataWorkerManager } from "../utils/ProjectDataWorkerManager";
|
||||
import { ProjectConfigManager } from "../utils/ProjectConfigManager";
|
||||
import { Vault, MetadataCache } from "obsidian";
|
||||
|
||||
// Mock the worker
|
||||
jest.mock("../utils/workers/ProjectData.worker");
|
||||
|
||||
describe("ProjectDataWorkerManager", () => {
|
||||
let vault: Vault;
|
||||
let metadataCache: MetadataCache;
|
||||
let projectConfigManager: ProjectConfigManager;
|
||||
let workerManager: ProjectDataWorkerManager;
|
||||
|
||||
beforeEach(() => {
|
||||
vault = {
|
||||
getAbstractFileByPath: jest.fn(),
|
||||
read: jest.fn(),
|
||||
} as any;
|
||||
|
||||
metadataCache = {
|
||||
getFileCache: jest.fn(),
|
||||
} as any;
|
||||
|
||||
projectConfigManager = new ProjectConfigManager({
|
||||
vault,
|
||||
metadataCache,
|
||||
configFileName: "task-genius.config.md",
|
||||
searchRecursively: true,
|
||||
metadataKey: "project",
|
||||
pathMappings: [],
|
||||
metadataMappings: [],
|
||||
defaultProjectNaming: {
|
||||
strategy: "filename",
|
||||
stripExtension: true,
|
||||
enabled: false,
|
||||
},
|
||||
enhancedProjectEnabled: true,
|
||||
});
|
||||
|
||||
workerManager = new ProjectDataWorkerManager({
|
||||
vault,
|
||||
metadataCache,
|
||||
projectConfigManager,
|
||||
maxWorkers: 2,
|
||||
enableWorkers: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
workerManager.destroy();
|
||||
});
|
||||
|
||||
describe("Worker Management", () => {
|
||||
it("should initialize workers when enabled", () => {
|
||||
expect(workerManager.isWorkersEnabled()).toBe(true);
|
||||
const stats = workerManager.getMemoryStats();
|
||||
expect(stats.workersEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it("should not initialize workers when disabled", () => {
|
||||
const disabledWorkerManager = new ProjectDataWorkerManager({
|
||||
vault,
|
||||
metadataCache,
|
||||
projectConfigManager,
|
||||
maxWorkers: 2,
|
||||
enableWorkers: false,
|
||||
});
|
||||
|
||||
expect(disabledWorkerManager.isWorkersEnabled()).toBe(false);
|
||||
const stats = disabledWorkerManager.getMemoryStats();
|
||||
expect(stats.workersEnabled).toBe(false);
|
||||
|
||||
disabledWorkerManager.destroy();
|
||||
});
|
||||
|
||||
it("should enable/disable workers dynamically", () => {
|
||||
workerManager.setWorkersEnabled(false);
|
||||
expect(workerManager.isWorkersEnabled()).toBe(false);
|
||||
|
||||
workerManager.setWorkersEnabled(true);
|
||||
expect(workerManager.isWorkersEnabled()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Project Data Computation", () => {
|
||||
it("should get project data using cache first", async () => {
|
||||
const filePath = "test.md";
|
||||
|
||||
// Mock file metadata
|
||||
(projectConfigManager.getFileMetadata as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
project: "Test Project",
|
||||
});
|
||||
|
||||
const result = await workerManager.getProjectData(filePath);
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle batch project data requests", async () => {
|
||||
const filePaths = ["test1.md", "test2.md", "test3.md"];
|
||||
|
||||
// Mock file metadata for all files
|
||||
(projectConfigManager.getFileMetadata as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockReturnValue({
|
||||
project: "Test Project",
|
||||
});
|
||||
|
||||
const results = await workerManager.getBatchProjectData(filePaths);
|
||||
expect(results).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it("should fallback to sync computation when workers fail", async () => {
|
||||
// Disable workers to force sync computation
|
||||
workerManager.setWorkersEnabled(false);
|
||||
|
||||
const filePath = "test.md";
|
||||
|
||||
// Mock the sync computation methods
|
||||
(projectConfigManager.determineTgProject as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
type: "test",
|
||||
name: "Test Project",
|
||||
source: "mock",
|
||||
readonly: true,
|
||||
});
|
||||
|
||||
(projectConfigManager.getEnhancedMetadata as jest.Mock) = jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
project: "Test Project",
|
||||
});
|
||||
|
||||
const result = await workerManager.getProjectData(filePath);
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.tgProject?.name).toBe("Test Project");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cache Management", () => {
|
||||
it("should clear cache when requested", () => {
|
||||
workerManager.clearCache();
|
||||
const stats = workerManager.getCacheStats();
|
||||
expect(stats).toBeDefined();
|
||||
});
|
||||
|
||||
it("should handle file events", async () => {
|
||||
const filePath = "test.md";
|
||||
|
||||
await workerManager.onFileCreated(filePath);
|
||||
await workerManager.onFileModified(filePath);
|
||||
await workerManager.onFileRenamed("old.md", "new.md");
|
||||
workerManager.onFileDeleted(filePath);
|
||||
|
||||
// Should not throw errors
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Settings Management", () => {
|
||||
it("should handle settings changes", () => {
|
||||
workerManager.onSettingsChange();
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle enhanced project setting changes", () => {
|
||||
workerManager.onEnhancedProjectSettingChange(false);
|
||||
workerManager.onEnhancedProjectSettingChange(true);
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Memory Management", () => {
|
||||
it("should provide memory statistics", () => {
|
||||
const stats = workerManager.getMemoryStats();
|
||||
expect(stats).toHaveProperty("fileCacheSize");
|
||||
expect(stats).toHaveProperty("directoryCacheSize");
|
||||
expect(stats).toHaveProperty("pendingRequests");
|
||||
expect(stats).toHaveProperty("activeWorkers");
|
||||
expect(stats).toHaveProperty("workersEnabled");
|
||||
});
|
||||
|
||||
it("should cleanup resources on destroy", () => {
|
||||
const stats1 = workerManager.getMemoryStats();
|
||||
workerManager.destroy();
|
||||
const stats2 = workerManager.getMemoryStats();
|
||||
|
||||
expect(stats2.activeWorkers).toBe(0);
|
||||
expect(stats2.pendingRequests).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
393
src/components/gantt/virtualization-manager.ts
Normal file
393
src/components/gantt/virtualization-manager.ts
Normal file
|
|
@ -0,0 +1,393 @@
|
|||
/**
|
||||
* Gantt Chart Virtualization Manager
|
||||
*
|
||||
* Handles virtual scrolling for grouped content, efficient group rendering,
|
||||
* lazy loading of group content, and memory management for complex group hierarchies.
|
||||
*/
|
||||
|
||||
import { Component } from "obsidian";
|
||||
import { Task } from "../../types/task";
|
||||
import { TaskGroup } from "../../types/gantt-grouping";
|
||||
|
||||
// Import GanttTaskItem from the main gantt file since it's not a grouping-specific type
|
||||
interface GanttTaskItem {
|
||||
task: Task;
|
||||
y: number;
|
||||
startX?: number;
|
||||
endX?: number;
|
||||
width?: number;
|
||||
isMilestone: boolean;
|
||||
level: number;
|
||||
}
|
||||
|
||||
export interface VirtualizationConfig {
|
||||
// Viewport settings
|
||||
viewportHeight: number;
|
||||
viewportWidth: number;
|
||||
|
||||
// Item dimensions
|
||||
rowHeight: number;
|
||||
groupHeaderHeight: number;
|
||||
|
||||
// Performance settings
|
||||
overscanCount: number; // Number of items to render outside viewport
|
||||
bufferSize: number; // Buffer size for smooth scrolling
|
||||
lazyLoadThreshold: number; // Distance from viewport to start loading
|
||||
|
||||
// Memory management
|
||||
maxCachedItems: number;
|
||||
cleanupInterval: number; // Cleanup interval in ms
|
||||
}
|
||||
|
||||
export interface VirtualItem {
|
||||
index: number;
|
||||
y: number;
|
||||
height: number;
|
||||
type: "task" | "group-header";
|
||||
data: Task | TaskGroup;
|
||||
groupId?: string;
|
||||
level?: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
export interface ViewportInfo {
|
||||
startIndex: number;
|
||||
endIndex: number;
|
||||
visibleItems: VirtualItem[];
|
||||
totalHeight: number;
|
||||
scrollTop: number;
|
||||
}
|
||||
|
||||
export class VirtualizationManager extends Component {
|
||||
private config: VirtualizationConfig;
|
||||
private virtualItems: VirtualItem[] = [];
|
||||
private cachedItems: Map<number, VirtualItem> = new Map();
|
||||
private viewportInfo: ViewportInfo;
|
||||
private cleanupTimer: number | null = null;
|
||||
|
||||
// Performance tracking
|
||||
private renderCount = 0;
|
||||
private lastRenderTime = 0;
|
||||
|
||||
constructor(config: VirtualizationConfig) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.viewportInfo = {
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
visibleItems: [],
|
||||
totalHeight: 0,
|
||||
scrollTop: 0,
|
||||
};
|
||||
}
|
||||
|
||||
onload(): void {
|
||||
this.startCleanupTimer();
|
||||
}
|
||||
|
||||
onunload(): void {
|
||||
this.stopCleanupTimer();
|
||||
this.cachedItems.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update configuration
|
||||
*/
|
||||
updateConfig(newConfig: Partial<VirtualizationConfig>): void {
|
||||
this.config = { ...this.config, ...newConfig };
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build virtual items from task groups
|
||||
*/
|
||||
buildVirtualItems(groups: TaskGroup[]): VirtualItem[] {
|
||||
const items: VirtualItem[] = [];
|
||||
let currentY = 0;
|
||||
let index = 0;
|
||||
|
||||
const processGroup = (group: TaskGroup, level: number = 0) => {
|
||||
// Add group header
|
||||
const headerItem: VirtualItem = {
|
||||
index: index++,
|
||||
y: currentY,
|
||||
height: this.config.groupHeaderHeight,
|
||||
type: "group-header",
|
||||
data: group,
|
||||
groupId: group.id,
|
||||
level: level,
|
||||
visible: true,
|
||||
};
|
||||
items.push(headerItem);
|
||||
currentY += this.config.groupHeaderHeight;
|
||||
|
||||
// Add tasks if group is expanded
|
||||
if (group.expanded) {
|
||||
// Process subgroups first
|
||||
if (group.subGroups && group.subGroups.length > 0) {
|
||||
for (const subGroup of group.subGroups) {
|
||||
processGroup(subGroup, level + 1);
|
||||
}
|
||||
} else {
|
||||
// Add tasks
|
||||
for (const task of group.tasks) {
|
||||
const taskItem: VirtualItem = {
|
||||
index: index++,
|
||||
y: currentY,
|
||||
height: this.config.rowHeight,
|
||||
type: "task",
|
||||
data: task,
|
||||
groupId: group.id,
|
||||
level: level + 1,
|
||||
visible: true,
|
||||
};
|
||||
items.push(taskItem);
|
||||
currentY += this.config.rowHeight;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (const group of groups) {
|
||||
processGroup(group);
|
||||
}
|
||||
|
||||
this.virtualItems = items;
|
||||
this.viewportInfo.totalHeight = currentY;
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate visible items based on scroll position
|
||||
*/
|
||||
calculateVisibleItems(scrollTop: number): ViewportInfo {
|
||||
const startTime = performance.now();
|
||||
|
||||
this.viewportInfo.scrollTop = scrollTop;
|
||||
|
||||
// Find start and end indices
|
||||
const viewportStart = scrollTop;
|
||||
const viewportEnd = scrollTop + this.config.viewportHeight;
|
||||
|
||||
// Add overscan
|
||||
const overscanStart = Math.max(
|
||||
0,
|
||||
viewportStart - this.config.bufferSize
|
||||
);
|
||||
const overscanEnd = viewportEnd + this.config.bufferSize;
|
||||
|
||||
let startIndex = 0;
|
||||
let endIndex = this.virtualItems.length - 1;
|
||||
|
||||
// Binary search for start index
|
||||
let left = 0;
|
||||
let right = this.virtualItems.length - 1;
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const item = this.virtualItems[mid];
|
||||
if (item.y + item.height < overscanStart) {
|
||||
left = mid + 1;
|
||||
} else {
|
||||
right = mid - 1;
|
||||
startIndex = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Binary search for end index
|
||||
left = startIndex;
|
||||
right = this.virtualItems.length - 1;
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const item = this.virtualItems[mid];
|
||||
if (item.y > overscanEnd) {
|
||||
right = mid - 1;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
endIndex = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// Add overscan items
|
||||
startIndex = Math.max(0, startIndex - this.config.overscanCount);
|
||||
endIndex = Math.min(
|
||||
this.virtualItems.length - 1,
|
||||
endIndex + this.config.overscanCount
|
||||
);
|
||||
|
||||
// Get visible items
|
||||
const visibleItems = this.virtualItems.slice(startIndex, endIndex + 1);
|
||||
|
||||
// Update viewport info
|
||||
this.viewportInfo = {
|
||||
startIndex,
|
||||
endIndex,
|
||||
visibleItems,
|
||||
totalHeight: this.viewportInfo.totalHeight,
|
||||
scrollTop,
|
||||
};
|
||||
|
||||
// Performance tracking
|
||||
this.renderCount++;
|
||||
this.lastRenderTime = performance.now() - startTime;
|
||||
|
||||
return this.viewportInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached item or create new one
|
||||
*/
|
||||
getCachedItem(index: number): VirtualItem | null {
|
||||
if (this.cachedItems.has(index)) {
|
||||
return this.cachedItems.get(index)!;
|
||||
}
|
||||
|
||||
if (index >= 0 && index < this.virtualItems.length) {
|
||||
const item = this.virtualItems[index];
|
||||
this.cacheItem(index, item);
|
||||
return item;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache an item
|
||||
*/
|
||||
private cacheItem(index: number, item: VirtualItem): void {
|
||||
// Implement LRU cache
|
||||
if (this.cachedItems.size >= this.config.maxCachedItems) {
|
||||
// Remove oldest item
|
||||
const firstKey = this.cachedItems.keys().next().value;
|
||||
this.cachedItems.delete(firstKey);
|
||||
}
|
||||
|
||||
this.cachedItems.set(index, { ...item });
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate cache
|
||||
*/
|
||||
private invalidateCache(): void {
|
||||
this.cachedItems.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start cleanup timer
|
||||
*/
|
||||
private startCleanupTimer(): void {
|
||||
this.cleanupTimer = window.setInterval(() => {
|
||||
this.cleanup();
|
||||
}, this.config.cleanupInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop cleanup timer
|
||||
*/
|
||||
private stopCleanupTimer(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup unused cached items
|
||||
*/
|
||||
private cleanup(): void {
|
||||
const currentTime = Date.now();
|
||||
const maxAge = 30000; // 30 seconds
|
||||
|
||||
// Remove items that are far from current viewport
|
||||
const { startIndex, endIndex } = this.viewportInfo;
|
||||
const keepRange = this.config.overscanCount * 2;
|
||||
|
||||
for (const [index] of this.cachedItems) {
|
||||
if (
|
||||
index < startIndex - keepRange ||
|
||||
index > endIndex + keepRange
|
||||
) {
|
||||
this.cachedItems.delete(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get performance metrics
|
||||
*/
|
||||
getPerformanceMetrics(): {
|
||||
renderCount: number;
|
||||
lastRenderTime: number;
|
||||
cachedItemsCount: number;
|
||||
virtualItemsCount: number;
|
||||
averageRenderTime: number;
|
||||
} {
|
||||
return {
|
||||
renderCount: this.renderCount,
|
||||
lastRenderTime: this.lastRenderTime,
|
||||
cachedItemsCount: this.cachedItems.size,
|
||||
virtualItemsCount: this.virtualItems.length,
|
||||
averageRenderTime:
|
||||
this.renderCount > 0
|
||||
? this.lastRenderTime / this.renderCount
|
||||
: 0,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset performance counters
|
||||
*/
|
||||
resetPerformanceMetrics(): void {
|
||||
this.renderCount = 0;
|
||||
this.lastRenderTime = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current viewport info
|
||||
*/
|
||||
getViewportInfo(): ViewportInfo {
|
||||
return { ...this.viewportInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if lazy loading should be triggered
|
||||
*/
|
||||
shouldLazyLoad(itemIndex: number): boolean {
|
||||
const { startIndex, endIndex } = this.viewportInfo;
|
||||
const distance = Math.min(
|
||||
Math.abs(itemIndex - startIndex),
|
||||
Math.abs(itemIndex - endIndex)
|
||||
);
|
||||
|
||||
return distance <= this.config.lazyLoadThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate total height without building all items
|
||||
*/
|
||||
estimateTotalHeight(groups: TaskGroup[]): number {
|
||||
let totalHeight = 0;
|
||||
|
||||
const estimateGroup = (group: TaskGroup): number => {
|
||||
let height = this.config.groupHeaderHeight;
|
||||
|
||||
if (group.expanded) {
|
||||
if (group.subGroups && group.subGroups.length > 0) {
|
||||
height += group.subGroups.reduce(
|
||||
(sum, subGroup) => sum + estimateGroup(subGroup),
|
||||
0
|
||||
);
|
||||
} else {
|
||||
height += group.tasks.length * this.config.rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
};
|
||||
|
||||
for (const group of groups) {
|
||||
totalHeight += estimateGroup(group);
|
||||
}
|
||||
|
||||
return totalHeight;
|
||||
}
|
||||
}
|
||||
|
|
@ -616,4 +616,34 @@ export class ProjectConfigManager {
|
|||
// Clear cache when options change
|
||||
this.clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get worker configuration for project data computation
|
||||
*/
|
||||
getWorkerConfig(): {
|
||||
pathMappings: Array<{
|
||||
pathPattern: string;
|
||||
projectName: string;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
metadataMappings: MetadataMapping[];
|
||||
defaultProjectNaming: ProjectNamingStrategy;
|
||||
metadataKey: string;
|
||||
} {
|
||||
return {
|
||||
pathMappings: this.pathMappings,
|
||||
metadataMappings: this.metadataMappings,
|
||||
defaultProjectNaming: this.defaultProjectNaming,
|
||||
metadataKey: this.metadataKey,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project config data for a file (alias for getProjectConfig for compatibility)
|
||||
*/
|
||||
async getProjectConfigData(
|
||||
filePath: string
|
||||
): Promise<ProjectConfigData | null> {
|
||||
return await this.getProjectConfig(filePath);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,16 @@
|
|||
/**
|
||||
* Enhanced Project Data Cache Manager
|
||||
*
|
||||
*
|
||||
* Provides high-performance caching for project data with directory-level optimizations
|
||||
* and batch processing capabilities to reduce main thread blocking.
|
||||
*/
|
||||
|
||||
import { TFile, Vault, MetadataCache } from "obsidian";
|
||||
import { TgProject } from "../types/task";
|
||||
import { ProjectConfigData, ProjectConfigManager } from "./ProjectConfigManager";
|
||||
import {
|
||||
ProjectConfigData,
|
||||
ProjectConfigManager,
|
||||
} from "./ProjectConfigManager";
|
||||
|
||||
export interface CachedProjectData {
|
||||
tgProject?: TgProject;
|
||||
|
|
@ -35,25 +38,25 @@ export class ProjectDataCache {
|
|||
private vault: Vault;
|
||||
private metadataCache: MetadataCache;
|
||||
private projectConfigManager: ProjectConfigManager;
|
||||
|
||||
|
||||
// File-level cache for computed project data
|
||||
private fileCache = new Map<string, CachedProjectData>();
|
||||
|
||||
|
||||
// Directory-level cache for project config files
|
||||
private directoryCache = new Map<string, DirectoryCache>();
|
||||
|
||||
|
||||
// Batch processing optimization
|
||||
private pendingUpdates = new Set<string>();
|
||||
private batchUpdateTimer?: NodeJS.Timeout;
|
||||
private readonly BATCH_DELAY = 100; // ms
|
||||
|
||||
|
||||
// Performance tracking
|
||||
private stats: ProjectCacheStats = {
|
||||
totalFiles: 0,
|
||||
cachedFiles: 0,
|
||||
directoryCacheHits: 0,
|
||||
configCacheHits: 0,
|
||||
lastUpdateTime: 0
|
||||
lastUpdateTime: 0,
|
||||
};
|
||||
|
||||
constructor(
|
||||
|
|
@ -85,9 +88,11 @@ export class ProjectDataCache {
|
|||
/**
|
||||
* Batch get project data for multiple files with optimizations
|
||||
*/
|
||||
async getBatchProjectData(filePaths: string[]): Promise<Map<string, CachedProjectData>> {
|
||||
async getBatchProjectData(
|
||||
filePaths: string[]
|
||||
): Promise<Map<string, CachedProjectData>> {
|
||||
const result = new Map<string, CachedProjectData>();
|
||||
|
||||
|
||||
if (!this.projectConfigManager.isEnhancedProjectEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
|
@ -111,15 +116,18 @@ export class ProjectDataCache {
|
|||
// Process uncached files in batches by directory for efficiency
|
||||
if (uncachedPaths.length > 0) {
|
||||
const batchedByDirectory = this.groupByDirectory(uncachedPaths);
|
||||
|
||||
|
||||
for (const [directory, paths] of batchedByDirectory) {
|
||||
const directoryData = await this.getOrCreateDirectoryCache(directory);
|
||||
|
||||
const directoryData = await this.getOrCreateDirectoryCache(
|
||||
directory
|
||||
);
|
||||
|
||||
for (const filePath of paths) {
|
||||
const projectData = await this.computeProjectDataWithDirectoryCache(
|
||||
filePath,
|
||||
directoryData
|
||||
);
|
||||
const projectData =
|
||||
await this.computeProjectDataWithDirectoryCache(
|
||||
filePath,
|
||||
directoryData
|
||||
);
|
||||
if (projectData) {
|
||||
result.set(filePath, projectData);
|
||||
}
|
||||
|
|
@ -142,37 +150,45 @@ export class ProjectDataCache {
|
|||
directoryCache: DirectoryCache
|
||||
): Promise<CachedProjectData | null> {
|
||||
try {
|
||||
const tgProject = await this.projectConfigManager.determineTgProject(filePath);
|
||||
|
||||
const tgProject =
|
||||
await this.projectConfigManager.determineTgProject(filePath);
|
||||
|
||||
// Get enhanced metadata efficiently using cached config data
|
||||
let enhancedMetadata: Record<string, any> = {};
|
||||
|
||||
|
||||
// Get file metadata
|
||||
const fileMetadata = this.projectConfigManager.getFileMetadata(filePath) || {};
|
||||
|
||||
const fileMetadata =
|
||||
this.projectConfigManager.getFileMetadata(filePath) || {};
|
||||
|
||||
// Use cached config data if available
|
||||
const configData = directoryCache.configData || {};
|
||||
|
||||
|
||||
// Merge and apply mappings
|
||||
const mergedMetadata = { ...configData, ...fileMetadata };
|
||||
enhancedMetadata = this.projectConfigManager.applyMappingsToMetadata(mergedMetadata);
|
||||
enhancedMetadata =
|
||||
this.projectConfigManager.applyMappingsToMetadata(
|
||||
mergedMetadata
|
||||
);
|
||||
|
||||
const projectData: CachedProjectData = {
|
||||
tgProject,
|
||||
enhancedMetadata,
|
||||
timestamp: Date.now(),
|
||||
configSource: directoryCache.configFile?.path
|
||||
configSource: directoryCache.configFile?.path,
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
this.fileCache.set(filePath, projectData);
|
||||
|
||||
|
||||
// Update directory cache file tracking
|
||||
directoryCache.paths.add(filePath);
|
||||
|
||||
return projectData;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to compute project data for ${filePath}:`, error);
|
||||
console.warn(
|
||||
`Failed to compute project data for ${filePath}:`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -180,9 +196,11 @@ export class ProjectDataCache {
|
|||
/**
|
||||
* Get or create directory cache for project config files
|
||||
*/
|
||||
private async getOrCreateDirectoryCache(directory: string): Promise<DirectoryCache> {
|
||||
private async getOrCreateDirectoryCache(
|
||||
directory: string
|
||||
): Promise<DirectoryCache> {
|
||||
let cached = this.directoryCache.get(directory);
|
||||
|
||||
|
||||
if (cached) {
|
||||
// Check if cache is still valid
|
||||
if (cached.configFile) {
|
||||
|
|
@ -200,7 +218,7 @@ export class ProjectDataCache {
|
|||
// Create new directory cache
|
||||
cached = {
|
||||
configTimestamp: 0,
|
||||
paths: new Set()
|
||||
paths: new Set(),
|
||||
};
|
||||
|
||||
// Look for config file in this directory
|
||||
|
|
@ -208,24 +226,27 @@ export class ProjectDataCache {
|
|||
if (configFile) {
|
||||
cached.configFile = configFile;
|
||||
cached.configTimestamp = configFile.stat.mtime;
|
||||
|
||||
|
||||
// Read and cache config data
|
||||
try {
|
||||
const content = await this.vault.read(configFile);
|
||||
const metadata = this.metadataCache.getFileCache(configFile);
|
||||
|
||||
|
||||
let configData: ProjectConfigData = {};
|
||||
if (metadata?.frontmatter) {
|
||||
configData = { ...metadata.frontmatter };
|
||||
}
|
||||
|
||||
|
||||
// Parse additional config content
|
||||
const contentConfig = this.parseConfigContent(content);
|
||||
configData = { ...configData, ...contentConfig };
|
||||
|
||||
|
||||
cached.configData = configData;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read config file ${configFile.path}:`, error);
|
||||
console.warn(
|
||||
`Failed to read config file ${configFile.path}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -236,15 +257,18 @@ export class ProjectDataCache {
|
|||
/**
|
||||
* Find project config file in a specific directory (non-recursive)
|
||||
*/
|
||||
private async findConfigFileInDirectory(directory: string): Promise<TFile | null> {
|
||||
private async findConfigFileInDirectory(
|
||||
directory: string
|
||||
): Promise<TFile | null> {
|
||||
const abstractFile = this.vault.getAbstractFileByPath(directory);
|
||||
if (!abstractFile || !("children" in abstractFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const configFileName = "task-genius.config.md"; // TODO: Make configurable
|
||||
const configFile = abstractFile.children.find(
|
||||
(child: any) => child && child.name === configFileName && "stat" in child
|
||||
const configFile = (abstractFile as any).children.find(
|
||||
(child: any) =>
|
||||
child && child.name === configFileName && "stat" in child
|
||||
) as TFile | undefined;
|
||||
|
||||
return configFile || null;
|
||||
|
|
@ -255,14 +279,14 @@ export class ProjectDataCache {
|
|||
*/
|
||||
private groupByDirectory(filePaths: string[]): Map<string, string[]> {
|
||||
const groups = new Map<string, string[]>();
|
||||
|
||||
|
||||
for (const filePath of filePaths) {
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
const existing = groups.get(directory) || [];
|
||||
existing.push(filePath);
|
||||
groups.set(directory, existing);
|
||||
}
|
||||
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
|
|
@ -291,7 +315,9 @@ export class ProjectDataCache {
|
|||
|
||||
// Check if config file has been modified
|
||||
if (cached.configSource) {
|
||||
const configFile = this.vault.getAbstractFileByPath(cached.configSource);
|
||||
const configFile = this.vault.getAbstractFileByPath(
|
||||
cached.configSource
|
||||
);
|
||||
if (configFile && "stat" in configFile) {
|
||||
const configTimestamp = (configFile as TFile).stat.mtime;
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
|
|
@ -308,10 +334,15 @@ export class ProjectDataCache {
|
|||
/**
|
||||
* Compute and cache project data for a single file
|
||||
*/
|
||||
private async computeAndCacheProjectData(filePath: string): Promise<CachedProjectData | null> {
|
||||
private async computeAndCacheProjectData(
|
||||
filePath: string
|
||||
): Promise<CachedProjectData | null> {
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
const directoryCache = await this.getOrCreateDirectoryCache(directory);
|
||||
return await this.computeProjectDataWithDirectoryCache(filePath, directoryCache);
|
||||
return await this.computeProjectDataWithDirectoryCache(
|
||||
filePath,
|
||||
directoryCache
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -323,7 +354,11 @@ export class ProjectDataCache {
|
|||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("//")) {
|
||||
if (
|
||||
!trimmed ||
|
||||
trimmed.startsWith("#") ||
|
||||
trimmed.startsWith("//")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -348,7 +383,7 @@ export class ProjectDataCache {
|
|||
clearCache(filePath?: string): void {
|
||||
if (filePath) {
|
||||
this.fileCache.delete(filePath);
|
||||
|
||||
|
||||
// Clear from directory cache tracking
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
const dirCache = this.directoryCache.get(directory);
|
||||
|
|
@ -434,9 +469,12 @@ export class ProjectDataCache {
|
|||
async onFileModified(filePath: string): Promise<void> {
|
||||
// Clear cache for the modified file
|
||||
this.clearCache(filePath);
|
||||
|
||||
|
||||
// Check if it's a project config file
|
||||
if (filePath.endsWith('.config.md') || filePath.includes('task-genius')) {
|
||||
if (
|
||||
filePath.endsWith(".config.md") ||
|
||||
filePath.includes("task-genius")
|
||||
) {
|
||||
// Clear directory cache since config may have changed
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
this.clearDirectoryCache(directory);
|
||||
|
|
@ -451,9 +489,12 @@ export class ProjectDataCache {
|
|||
*/
|
||||
onFileDeleted(filePath: string): void {
|
||||
this.clearCache(filePath);
|
||||
|
||||
|
||||
// Update directory cache if it was a config file
|
||||
if (filePath.endsWith('.config.md') || filePath.includes('task-genius')) {
|
||||
if (
|
||||
filePath.endsWith(".config.md") ||
|
||||
filePath.includes("task-genius")
|
||||
) {
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
this.clearDirectoryCache(directory);
|
||||
}
|
||||
|
|
@ -464,7 +505,10 @@ export class ProjectDataCache {
|
|||
*/
|
||||
async onFileCreated(filePath: string): Promise<void> {
|
||||
// If it's a config file, clear directory cache to pick up new config
|
||||
if (filePath.endsWith('.config.md') || filePath.includes('task-genius')) {
|
||||
if (
|
||||
filePath.endsWith(".config.md") ||
|
||||
filePath.includes("task-genius")
|
||||
) {
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
this.clearDirectoryCache(directory);
|
||||
}
|
||||
|
|
@ -479,16 +523,16 @@ export class ProjectDataCache {
|
|||
async onFileRenamed(oldPath: string, newPath: string): Promise<void> {
|
||||
// Clear cache for old path
|
||||
this.clearCache(oldPath);
|
||||
|
||||
|
||||
// Update relevant directory caches
|
||||
const oldDirectory = this.getDirectoryPath(oldPath);
|
||||
const newDirectory = this.getDirectoryPath(newPath);
|
||||
|
||||
if (oldPath.endsWith('.config.md') || oldPath.includes('task-genius')) {
|
||||
|
||||
if (oldPath.endsWith(".config.md") || oldPath.includes("task-genius")) {
|
||||
this.clearDirectoryCache(oldDirectory);
|
||||
}
|
||||
|
||||
if (newPath.endsWith('.config.md') || newPath.includes('task-genius')) {
|
||||
|
||||
if (newPath.endsWith(".config.md") || newPath.includes("task-genius")) {
|
||||
this.clearDirectoryCache(newDirectory);
|
||||
}
|
||||
|
||||
|
|
@ -501,7 +545,7 @@ export class ProjectDataCache {
|
|||
*/
|
||||
async refreshStaleEntries(): Promise<void> {
|
||||
const staleFiles: string[] = [];
|
||||
|
||||
|
||||
for (const [filePath, cachedData] of this.fileCache.entries()) {
|
||||
if (!this.isCacheValid(filePath, cachedData)) {
|
||||
staleFiles.push(filePath);
|
||||
|
|
@ -509,7 +553,9 @@ export class ProjectDataCache {
|
|||
}
|
||||
|
||||
if (staleFiles.length > 0) {
|
||||
console.log(`Refreshing ${staleFiles.length} stale project data cache entries`);
|
||||
console.log(
|
||||
`Refreshing ${staleFiles.length} stale project data cache entries`
|
||||
);
|
||||
await this.getBatchProjectData(staleFiles);
|
||||
}
|
||||
}
|
||||
|
|
@ -518,11 +564,32 @@ export class ProjectDataCache {
|
|||
* Preload project data for recently accessed files
|
||||
*/
|
||||
async preloadRecentFiles(filePaths: string[]): Promise<void> {
|
||||
const uncachedFiles = filePaths.filter(path => !this.fileCache.has(path));
|
||||
|
||||
const uncachedFiles = filePaths.filter(
|
||||
(path) => !this.fileCache.has(path)
|
||||
);
|
||||
|
||||
if (uncachedFiles.length > 0) {
|
||||
console.log(`Preloading project data for ${uncachedFiles.length} recent files`);
|
||||
console.log(
|
||||
`Preloading project data for ${uncachedFiles.length} recent files`
|
||||
);
|
||||
await this.getBatchProjectData(uncachedFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set project data in cache (for external updates)
|
||||
*/
|
||||
async setProjectData(
|
||||
filePath: string,
|
||||
projectData: CachedProjectData
|
||||
): Promise<void> {
|
||||
this.fileCache.set(filePath, projectData);
|
||||
|
||||
// Update directory cache tracking
|
||||
const directory = this.getDirectoryPath(filePath);
|
||||
const dirCache = this.directoryCache.get(directory);
|
||||
if (dirCache) {
|
||||
dirCache.paths.add(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* Project Data Worker Manager
|
||||
*
|
||||
*
|
||||
* Manages project data computation workers to avoid blocking the main thread
|
||||
* during startup and project data operations.
|
||||
*/
|
||||
|
|
@ -8,19 +8,23 @@
|
|||
import { Vault, MetadataCache } from "obsidian";
|
||||
import { ProjectConfigManager } from "./ProjectConfigManager";
|
||||
import { ProjectDataCache, CachedProjectData } from "./ProjectDataCache";
|
||||
import {
|
||||
ProjectDataResponse,
|
||||
WorkerResponse
|
||||
import {
|
||||
ProjectDataResponse,
|
||||
WorkerResponse,
|
||||
UpdateConfigMessage,
|
||||
ProjectDataMessage,
|
||||
BatchProjectDataMessage,
|
||||
} from "./workers/TaskIndexWorkerMessage";
|
||||
|
||||
// Worker import temporarily disabled due to test compatibility issues
|
||||
// import ProjectWorker from "./workers/ProjectData.worker";
|
||||
// @ts-ignore Ignore type error for worker import
|
||||
import ProjectWorker from "./workers/ProjectData.worker";
|
||||
|
||||
export interface ProjectDataWorkerManagerOptions {
|
||||
vault: Vault;
|
||||
metadataCache: MetadataCache;
|
||||
projectConfigManager: ProjectConfigManager;
|
||||
maxWorkers?: number;
|
||||
enableWorkers?: boolean; // Add option to enable/disable workers
|
||||
}
|
||||
|
||||
export class ProjectDataWorkerManager {
|
||||
|
|
@ -28,21 +32,31 @@ export class ProjectDataWorkerManager {
|
|||
private metadataCache: MetadataCache;
|
||||
private projectConfigManager: ProjectConfigManager;
|
||||
private cache: ProjectDataCache;
|
||||
|
||||
|
||||
private workers: Worker[] = [];
|
||||
private maxWorkers: number;
|
||||
private enableWorkers: boolean;
|
||||
private requestId = 0;
|
||||
private pendingRequests = new Map<string, {
|
||||
resolve: (value: any) => void;
|
||||
reject: (error: any) => void;
|
||||
}>();
|
||||
private pendingRequests = new Map<
|
||||
string,
|
||||
{
|
||||
resolve: (value: any) => void;
|
||||
reject: (error: any) => void;
|
||||
}
|
||||
>();
|
||||
|
||||
// Worker round-robin index for load balancing
|
||||
private currentWorkerIndex = 0;
|
||||
|
||||
constructor(options: ProjectDataWorkerManagerOptions) {
|
||||
this.vault = options.vault;
|
||||
this.metadataCache = options.metadataCache;
|
||||
this.projectConfigManager = options.projectConfigManager;
|
||||
this.maxWorkers = options.maxWorkers || Math.max(1, Math.floor(navigator.hardwareConcurrency / 2));
|
||||
|
||||
this.maxWorkers =
|
||||
options.maxWorkers ||
|
||||
Math.max(1, Math.floor(navigator.hardwareConcurrency / 2));
|
||||
this.enableWorkers = options.enableWorkers ?? true;
|
||||
|
||||
this.cache = new ProjectDataCache(
|
||||
this.vault,
|
||||
this.metadataCache,
|
||||
|
|
@ -54,24 +68,76 @@ export class ProjectDataWorkerManager {
|
|||
|
||||
/**
|
||||
* Initialize worker pool
|
||||
*
|
||||
* TEMPORARY: Workers disabled to avoid build issues with tests.
|
||||
* The system will use cache-only optimization which still provides
|
||||
* significant performance improvements.
|
||||
*/
|
||||
private initializeWorkers(): void {
|
||||
console.log("ProjectDataWorkerManager: Workers temporarily disabled, using cache-only optimization");
|
||||
// Workers are temporarily disabled to avoid test failures
|
||||
// The caching system still provides significant performance improvements
|
||||
this.workers = [];
|
||||
if (!this.enableWorkers) {
|
||||
console.log(
|
||||
"ProjectDataWorkerManager: Workers disabled, using cache-only optimization"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(
|
||||
`ProjectDataWorkerManager: Initializing ${this.maxWorkers} workers`
|
||||
);
|
||||
|
||||
for (let i = 0; i < this.maxWorkers; i++) {
|
||||
const worker = new ProjectWorker();
|
||||
|
||||
worker.onmessage = (event: MessageEvent) => {
|
||||
this.handleWorkerMessage(event.data);
|
||||
};
|
||||
|
||||
worker.onerror = (error: ErrorEvent) => {
|
||||
console.error(`Worker ${i} error:`, error);
|
||||
};
|
||||
|
||||
this.workers.push(worker);
|
||||
}
|
||||
|
||||
// Send initial configuration to all workers
|
||||
this.updateWorkerConfig();
|
||||
|
||||
console.log(
|
||||
`ProjectDataWorkerManager: Successfully initialized ${this.workers.length} workers`
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"ProjectDataWorkerManager: Failed to initialize workers, falling back to synchronous processing",
|
||||
error
|
||||
);
|
||||
this.enableWorkers = false;
|
||||
this.workers = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update worker configuration when settings change (DISABLED)
|
||||
* Update worker configuration when settings change
|
||||
*/
|
||||
private updateWorkerConfig(): void {
|
||||
// Workers are disabled, no configuration needed
|
||||
console.log("ProjectDataWorkerManager: Worker config update skipped (workers disabled)");
|
||||
if (!this.enableWorkers || this.workers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = this.projectConfigManager.getWorkerConfig();
|
||||
|
||||
const configMessage: UpdateConfigMessage = {
|
||||
type: "updateConfig",
|
||||
requestId: this.generateRequestId(),
|
||||
config,
|
||||
};
|
||||
|
||||
// Send configuration to all workers
|
||||
for (const worker of this.workers) {
|
||||
try {
|
||||
worker.postMessage(configMessage);
|
||||
} catch (error) {
|
||||
console.warn("Failed to update worker config:", error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("ProjectDataWorkerManager: Updated worker configuration");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,28 +150,44 @@ export class ProjectDataWorkerManager {
|
|||
return cached;
|
||||
}
|
||||
|
||||
// Fallback to synchronous computation if not cached
|
||||
return await this.computeProjectDataSync(filePath);
|
||||
// Use worker if available, otherwise fallback to synchronous computation
|
||||
if (this.enableWorkers && this.workers.length > 0) {
|
||||
return await this.computeProjectDataWithWorker(filePath);
|
||||
} else {
|
||||
return await this.computeProjectDataSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project data for multiple files with batch optimization
|
||||
*/
|
||||
async getBatchProjectData(filePaths: string[]): Promise<Map<string, CachedProjectData>> {
|
||||
async getBatchProjectData(
|
||||
filePaths: string[]
|
||||
): Promise<Map<string, CachedProjectData>> {
|
||||
if (!this.projectConfigManager.isEnhancedProjectEnabled()) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
// Use cache first for batch operation
|
||||
const cacheResult = await this.cache.getBatchProjectData(filePaths);
|
||||
|
||||
|
||||
// Find files that weren't in cache
|
||||
const missingPaths = filePaths.filter(path => !cacheResult.has(path));
|
||||
|
||||
const missingPaths = filePaths.filter((path) => !cacheResult.has(path));
|
||||
|
||||
if (missingPaths.length > 0) {
|
||||
// Compute missing data using workers
|
||||
const workerResults = await this.computeBatchProjectDataWithWorkers(missingPaths);
|
||||
|
||||
// Compute missing data using workers or fallback
|
||||
let workerResults: Map<string, CachedProjectData>;
|
||||
|
||||
if (this.enableWorkers && this.workers.length > 0) {
|
||||
workerResults = await this.computeBatchProjectDataWithWorkers(
|
||||
missingPaths
|
||||
);
|
||||
} else {
|
||||
workerResults = await this.computeBatchProjectDataSync(
|
||||
missingPaths
|
||||
);
|
||||
}
|
||||
|
||||
// Merge results
|
||||
for (const [path, data] of workerResults) {
|
||||
cacheResult.set(path, data);
|
||||
|
|
@ -116,28 +198,260 @@ export class ProjectDataWorkerManager {
|
|||
}
|
||||
|
||||
/**
|
||||
* Compute project data for multiple files using fallback synchronous method
|
||||
*
|
||||
* TEMPORARY: Using synchronous fallback since workers are disabled
|
||||
* Compute project data using worker
|
||||
*/
|
||||
private async computeBatchProjectDataWithWorkers(filePaths: string[]): Promise<Map<string, CachedProjectData>> {
|
||||
private async computeProjectDataWithWorker(
|
||||
filePath: string
|
||||
): Promise<CachedProjectData | null> {
|
||||
try {
|
||||
const fileMetadata =
|
||||
await this.projectConfigManager.getFileMetadata(filePath);
|
||||
const configData =
|
||||
await this.projectConfigManager.getProjectConfigData(filePath);
|
||||
|
||||
const worker = this.getNextWorker();
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
const message: ProjectDataMessage = {
|
||||
type: "computeProjectData",
|
||||
requestId,
|
||||
filePath,
|
||||
fileMetadata: fileMetadata || {},
|
||||
configData: configData || {},
|
||||
};
|
||||
|
||||
const response = await this.sendWorkerMessage(worker, message);
|
||||
|
||||
if (response.success && response.data) {
|
||||
const projectData: CachedProjectData = {
|
||||
tgProject: response.data.tgProject,
|
||||
enhancedMetadata: response.data.enhancedMetadata,
|
||||
timestamp: response.data.timestamp,
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
await this.cache.setProjectData(filePath, projectData);
|
||||
|
||||
return projectData;
|
||||
} else {
|
||||
throw new Error(response.error || "Worker computation failed");
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Failed to compute project data with worker for ${filePath}:`,
|
||||
error
|
||||
);
|
||||
// Fallback to synchronous computation
|
||||
return await this.computeProjectDataSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute project data for multiple files using workers
|
||||
*/
|
||||
private async computeBatchProjectDataWithWorkers(
|
||||
filePaths: string[]
|
||||
): Promise<Map<string, CachedProjectData>> {
|
||||
const result = new Map<string, CachedProjectData>();
|
||||
|
||||
console.log(`ProjectDataWorkerManager: Computing project data for ${filePaths.length} files using fallback method`);
|
||||
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`ProjectDataWorkerManager: Computing project data for ${filePaths.length} files using ${this.workers.length} workers`
|
||||
);
|
||||
|
||||
try {
|
||||
// Prepare file data for workers
|
||||
const files = await Promise.all(
|
||||
filePaths.map(async (filePath) => {
|
||||
const fileMetadata =
|
||||
await this.projectConfigManager.getFileMetadata(
|
||||
filePath
|
||||
);
|
||||
const configData =
|
||||
await this.projectConfigManager.getProjectConfigData(
|
||||
filePath
|
||||
);
|
||||
|
||||
return {
|
||||
filePath,
|
||||
fileMetadata: fileMetadata || {},
|
||||
configData: configData || {},
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
// Distribute files across workers
|
||||
const filesPerWorker = Math.ceil(
|
||||
files.length / this.workers.length
|
||||
);
|
||||
const workerPromises: Promise<ProjectDataResponse[]>[] = [];
|
||||
|
||||
for (let i = 0; i < this.workers.length; i++) {
|
||||
const startIndex = i * filesPerWorker;
|
||||
const endIndex = Math.min(
|
||||
startIndex + filesPerWorker,
|
||||
files.length
|
||||
);
|
||||
const workerFiles = files.slice(startIndex, endIndex);
|
||||
|
||||
if (workerFiles.length > 0) {
|
||||
workerPromises.push(
|
||||
this.sendBatchRequestToWorker(i, workerFiles)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all workers to complete
|
||||
const workerResults = await Promise.all(workerPromises);
|
||||
|
||||
// Process results
|
||||
for (const batchResults of workerResults) {
|
||||
for (const response of batchResults) {
|
||||
if (!response.error) {
|
||||
const projectData: CachedProjectData = {
|
||||
tgProject: response.tgProject,
|
||||
enhancedMetadata: response.enhancedMetadata,
|
||||
timestamp: response.timestamp,
|
||||
};
|
||||
|
||||
result.set(response.filePath, projectData);
|
||||
|
||||
// Cache the result
|
||||
await this.cache.setProjectData(
|
||||
response.filePath,
|
||||
projectData
|
||||
);
|
||||
} else {
|
||||
console.warn(
|
||||
`Worker failed to process ${response.filePath}:`,
|
||||
response.error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(
|
||||
`ProjectDataWorkerManager: Successfully processed ${result.size}/${filePaths.length} files with workers`
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Failed to compute batch project data with workers:",
|
||||
error
|
||||
);
|
||||
// Fallback to synchronous computation
|
||||
return await this.computeBatchProjectDataSync(filePaths);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send batch request to a specific worker
|
||||
*/
|
||||
private async sendBatchRequestToWorker(
|
||||
workerIndex: number,
|
||||
files: any[]
|
||||
): Promise<ProjectDataResponse[]> {
|
||||
const worker = this.workers[workerIndex];
|
||||
const requestId = this.generateRequestId();
|
||||
|
||||
const message: BatchProjectDataMessage = {
|
||||
type: "computeBatchProjectData",
|
||||
requestId,
|
||||
files,
|
||||
};
|
||||
|
||||
const response = await this.sendWorkerMessage(worker, message);
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data;
|
||||
} else {
|
||||
throw new Error(
|
||||
response.error || "Batch worker computation failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to worker and wait for response
|
||||
*/
|
||||
private async sendWorkerMessage(
|
||||
worker: Worker,
|
||||
message: any
|
||||
): Promise<WorkerResponse> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.pendingRequests.delete(message.requestId);
|
||||
reject(new Error("Worker request timeout"));
|
||||
}, 30000); // 30 second timeout
|
||||
|
||||
this.pendingRequests.set(message.requestId, {
|
||||
resolve: (response) => {
|
||||
clearTimeout(timeout);
|
||||
resolve(response);
|
||||
},
|
||||
reject: (error) => {
|
||||
clearTimeout(timeout);
|
||||
reject(error);
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
worker.postMessage(message);
|
||||
} catch (error) {
|
||||
clearTimeout(timeout);
|
||||
this.pendingRequests.delete(message.requestId);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get next worker for round-robin load balancing
|
||||
*/
|
||||
private getNextWorker(): Worker {
|
||||
if (this.workers.length === 0) {
|
||||
throw new Error("No workers available");
|
||||
}
|
||||
|
||||
const worker = this.workers[this.currentWorkerIndex];
|
||||
this.currentWorkerIndex =
|
||||
(this.currentWorkerIndex + 1) % this.workers.length;
|
||||
|
||||
return worker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute project data for multiple files using synchronous fallback
|
||||
*/
|
||||
private async computeBatchProjectDataSync(
|
||||
filePaths: string[]
|
||||
): Promise<Map<string, CachedProjectData>> {
|
||||
const result = new Map<string, CachedProjectData>();
|
||||
|
||||
console.log(
|
||||
`ProjectDataWorkerManager: Computing project data for ${filePaths.length} files using fallback method`
|
||||
);
|
||||
|
||||
// Process files in parallel using Promise.all for better performance than sequential
|
||||
const dataPromises = filePaths.map(async (filePath) => {
|
||||
try {
|
||||
const data = await this.computeProjectDataSync(filePath);
|
||||
return { filePath, data };
|
||||
} catch (error) {
|
||||
console.warn(`Failed to compute project data for ${filePath}:`, error);
|
||||
console.warn(
|
||||
`Failed to compute project data for ${filePath}:`,
|
||||
error
|
||||
);
|
||||
return { filePath, data: null };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(dataPromises);
|
||||
|
||||
|
||||
for (const { filePath, data } of results) {
|
||||
if (data) {
|
||||
result.set(filePath, data);
|
||||
|
|
@ -147,30 +461,33 @@ export class ProjectDataWorkerManager {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send batch request to a specific worker (DISABLED)
|
||||
*/
|
||||
private async sendBatchRequestToWorker(workerIndex: number, files: any[]): Promise<ProjectDataResponse[]> {
|
||||
throw new Error("Workers are temporarily disabled");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute project data synchronously (fallback)
|
||||
*/
|
||||
private async computeProjectDataSync(filePath: string): Promise<CachedProjectData | null> {
|
||||
private async computeProjectDataSync(
|
||||
filePath: string
|
||||
): Promise<CachedProjectData | null> {
|
||||
try {
|
||||
const tgProject = await this.projectConfigManager.determineTgProject(filePath);
|
||||
const enhancedMetadata = await this.projectConfigManager.getEnhancedMetadata(filePath);
|
||||
const tgProject =
|
||||
await this.projectConfigManager.determineTgProject(filePath);
|
||||
const enhancedMetadata =
|
||||
await this.projectConfigManager.getEnhancedMetadata(filePath);
|
||||
|
||||
const data: CachedProjectData = {
|
||||
tgProject,
|
||||
enhancedMetadata,
|
||||
timestamp: Date.now()
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Cache the result
|
||||
await this.cache.setProjectData(filePath, data);
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to compute project data for ${filePath}:`, error);
|
||||
console.warn(
|
||||
`Failed to compute project data for ${filePath}:`,
|
||||
error
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -187,9 +504,11 @@ export class ProjectDataWorkerManager {
|
|||
this.pendingRequests.delete(message.requestId);
|
||||
|
||||
if (message.success) {
|
||||
pendingRequest.resolve(message.data);
|
||||
pendingRequest.resolve(message);
|
||||
} else {
|
||||
pendingRequest.reject(new Error(message.error || 'Unknown worker error'));
|
||||
pendingRequest.reject(
|
||||
new Error(message.error || "Unknown worker error")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +519,6 @@ export class ProjectDataWorkerManager {
|
|||
return `req_${++this.requestId}_${Date.now()}`;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clear cache
|
||||
*/
|
||||
|
|
@ -228,6 +546,35 @@ export class ProjectDataWorkerManager {
|
|||
*/
|
||||
onEnhancedProjectSettingChange(enabled: boolean): void {
|
||||
this.cache.onEnhancedProjectSettingChange(enabled);
|
||||
|
||||
// Reinitialize workers if needed
|
||||
if (enabled && this.enableWorkers && this.workers.length === 0) {
|
||||
this.initializeWorkers();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable workers
|
||||
*/
|
||||
setWorkersEnabled(enabled: boolean): void {
|
||||
if (this.enableWorkers === enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enableWorkers = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.initializeWorkers();
|
||||
} else {
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if workers are enabled and available
|
||||
*/
|
||||
isWorkersEnabled(): boolean {
|
||||
return this.enableWorkers && this.workers.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -292,12 +639,14 @@ export class ProjectDataWorkerManager {
|
|||
directoryCacheSize: number;
|
||||
pendingRequests: number;
|
||||
activeWorkers: number;
|
||||
workersEnabled: boolean;
|
||||
} {
|
||||
return {
|
||||
fileCacheSize: (this.cache as any).fileCache?.size || 0,
|
||||
directoryCacheSize: (this.cache as any).directoryCache?.size || 0,
|
||||
pendingRequests: this.pendingRequests.size,
|
||||
activeWorkers: this.workers.length
|
||||
activeWorkers: this.workers.length,
|
||||
workersEnabled: this.enableWorkers,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -307,14 +656,18 @@ export class ProjectDataWorkerManager {
|
|||
destroy(): void {
|
||||
// Terminate all workers
|
||||
for (const worker of this.workers) {
|
||||
worker.terminate();
|
||||
try {
|
||||
worker.terminate();
|
||||
} catch (error) {
|
||||
console.warn("Error terminating worker:", error);
|
||||
}
|
||||
}
|
||||
this.workers = [];
|
||||
|
||||
|
||||
// Clear pending requests
|
||||
for (const { reject } of this.pendingRequests.values()) {
|
||||
reject(new Error('Worker manager destroyed'));
|
||||
reject(new Error("Worker manager destroyed"));
|
||||
}
|
||||
this.pendingRequests.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
src/utils/types/worker.d.ts
vendored
6
src/utils/types/worker.d.ts
vendored
|
|
@ -3,3 +3,9 @@ declare module "*/TaskIndex.worker" {
|
|||
const WorkerFactory: new () => Worker;
|
||||
export default WorkerFactory;
|
||||
}
|
||||
|
||||
/** @hidden */
|
||||
declare module "*/ProjectData.worker" {
|
||||
const WorkerFactory: new () => Worker;
|
||||
export default WorkerFactory;
|
||||
}
|
||||
|
|
|
|||
1420
styles.css
1420
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue