UNPKG

file-mover

Version:

Script to move files and update imports automatically

508 lines 19.8 kB
import { promises as fs } from "fs"; import path from "path"; import { updateImportsInFile } from "../fileOps.js"; import { findDependencyImports } from "../importUtils.js"; // No-op timer for when performance tracking is disabled class NoOpTimer { end() { return 0; } } class Performance { metrics; verbose; timerStack; runId; logDir; constructor(verbose = false) { this.verbose = verbose; this.metrics = this.getInitialMetrics(); this.timerStack = new Map(); this.runId = this.generateRunId(); this.logDir = path.join(process.cwd(), "performance"); this.ensureLogDirectory(); } generateRunId() { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}_${String(now.getHours()).padStart(2, "0")}-${String(now.getMinutes()).padStart(2, "0")}-${String(now.getSeconds()).padStart(2, "0")}`; } async ensureLogDirectory() { try { await fs.mkdir(this.logDir, { recursive: true }); } catch { // Directory might already exist, ignore error } } getInitialMetrics() { return { totalTime: 0, fileDiscovery: { time: 0, fileCount: 0 }, validation: { time: 0, moveCount: 0 }, importAnalysis: { time: 0, totalFiles: 0, filesWithImports: 0, fileReadTime: 0, astParseTime: 0, importMatchingTime: 0, individualFileTimes: [], }, fileOperations: { time: 0, moves: 0, updates: 0, writeTime: 0, moveTime: 0, }, cachePerformance: { astCacheHits: 0, fileCacheHits: 0, totalCacheLookups: 0 }, individualMoves: [], }; } calculateSummary() { const totalFiles = this.metrics.importAnalysis.totalFiles; const totalMoves = this.metrics.individualMoves.length; const totalUpdates = this.metrics.fileOperations.updates; const avgAnalysisTime = totalMoves > 0 ? this.metrics.individualMoves.reduce((sum, move) => sum + move.analysisTime, 0) / totalMoves : 0; const avgMoveTime = totalMoves > 0 ? this.metrics.individualMoves.reduce((sum, move) => sum + move.moveTime, 0) / totalMoves : 0; const avgUpdateTime = totalMoves > 0 ? this.metrics.individualMoves.reduce((sum, move) => sum + move.updateTime, 0) / totalMoves : 0; const avgFileAnalysisTime = this.metrics.importAnalysis.individualFileTimes.length > 0 ? this.metrics.importAnalysis.individualFileTimes.reduce((sum, file) => sum + file.totalTime, 0) / this.metrics.importAnalysis.individualFileTimes.length : 0; const cacheHitRate = this.metrics.cachePerformance.totalCacheLookups > 0 ? (this.metrics.cachePerformance.astCacheHits + this.metrics.cachePerformance.fileCacheHits) / this.metrics.cachePerformance.totalCacheLookups : 0; return { totalFiles, totalMoves, totalUpdates, avgAnalysisTime, avgMoveTime, avgUpdateTime, avgFileAnalysisTime, cacheHitRate, }; } async savePerformanceLog() { if (!this.verbose) return; const logEntry = { timestamp: new Date().toISOString(), runId: this.runId, metrics: this.metrics, summary: this.calculateSummary(), }; const logFile = path.join(this.logDir, `performance_${this.runId}.json`); await fs.writeFile(logFile, JSON.stringify(logEntry, null, 2)); } async updatePerformanceHistory() { if (!this.verbose) return; const historyFile = path.join(this.logDir, "performance_history.json"); let history = []; try { const existingData = await fs.readFile(historyFile, "utf8"); history = JSON.parse(existingData); } catch { // File doesn't exist or is invalid, start with empty array } const logEntry = { timestamp: new Date().toISOString(), runId: this.runId, metrics: this.metrics, summary: this.calculateSummary(), }; // Add new entry and keep only the last 50 runs history.push(logEntry); if (history.length > 50) { history = history.slice(-50); } await fs.writeFile(historyFile, JSON.stringify(history, null, 2)); } generatePerformanceTable() { if (!this.verbose) return ""; const summary = this.calculateSummary(); const table = ` ╔══════════════════════════════════════════════════════════════════════════════╗ ║ PERFORMANCE SUMMARY ║ ╠══════════════════════════════════════════════════════════════════════════════╣ ║ Total Execution Time: ${this.metrics.totalTime.toFixed(2)}ms ║ ║ Files Discovered: ${this.metrics.fileDiscovery.fileCount} (${this.metrics.fileDiscovery.time.toFixed(2)}ms) ║ ║ Moves Validated: ${this.metrics.validation.moveCount} (${this.metrics.validation.time.toFixed(2)}ms) ║ ║ Files Analyzed: ${this.metrics.importAnalysis.totalFiles} (${this.metrics.importAnalysis.time.toFixed(2)}ms) ║ ║ Files with Imports: ${this.metrics.importAnalysis.filesWithImports} ║ ║ Total Moves: ${this.metrics.individualMoves.length} ║ ║ Total Updates: ${this.metrics.fileOperations.updates} ║ ╠══════════════════════════════════════════════════════════════════════════════╣ ║ AVERAGE TIMES (per move) ║ ║ Analysis: ${summary.avgAnalysisTime.toFixed(2)}ms | Move: ${summary.avgMoveTime.toFixed(2)}ms | Update: ${summary.avgUpdateTime.toFixed(2)}ms ║ ║ File Analysis: ${summary.avgFileAnalysisTime.toFixed(2)}ms | Cache Hit Rate: ${(summary.cacheHitRate * 100).toFixed(1)}% ║ ╚══════════════════════════════════════════════════════════════════════════════╝ `; return table; } startTimer(label) { if (!this.verbose) { return new NoOpTimer(); } const startTime = globalThis.performance.now(); this.timerStack.set(label, startTime); return { end: () => { const endTime = globalThis.performance.now(); const duration = endTime - startTime; this.timerStack.delete(label); return duration; }, }; } trackFileAnalysis(file, readTime, parseTime, matchTime, importCount) { if (!this.verbose) return; this.metrics.importAnalysis.individualFileTimes.push({ file, readTime, parseTime, matchTime, totalTime: readTime + parseTime + matchTime, importCount, }); } addFileOpTime(type, time) { if (!this.verbose) return; if (type === "move") { this.metrics.fileOperations.moveTime += time; } else { this.metrics.fileOperations.writeTime += time; } } addFileOpUpdates(count) { if (!this.verbose) return; this.metrics.fileOperations.updates += count; } addFileOpMoves(count) { if (!this.verbose) return; this.metrics.fileOperations.moves += count; } setFileOpTotalTime(time) { if (!this.verbose) return; this.metrics.fileOperations.time = time; } addValidationTime(time, moveCount) { if (!this.verbose) return; this.metrics.validation.time += time; this.metrics.validation.moveCount += moveCount; } addDiscoveryTime(time, fileCount) { if (!this.verbose) return; this.metrics.fileDiscovery.time += time; this.metrics.fileDiscovery.fileCount = fileCount; } setImportAnalysisTime(time, totalFiles, filesWithImports) { if (!this.verbose) return; this.metrics.importAnalysis.time = time; this.metrics.importAnalysis.totalFiles = totalFiles; this.metrics.importAnalysis.filesWithImports = filesWithImports; } setImportAnalysisBreakdown(read, parse, match) { if (!this.verbose) return; this.metrics.importAnalysis.fileReadTime = read; this.metrics.importAnalysis.astParseTime = parse; this.metrics.importAnalysis.importMatchingTime = match; } clearFileAnalysisTimes() { if (!this.verbose) return; this.metrics.importAnalysis.individualFileTimes = []; } addMoveMetrics(move) { if (!this.verbose) return; this.metrics.individualMoves.push(move); } setTotalTime(time) { if (!this.verbose) return; this.metrics.totalTime = time; } async printSummary() { if (!this.verbose) return; console.log(this.generatePerformanceTable()); // Save performance data await this.savePerformanceLog(); await this.updatePerformanceHistory(); } clearCaches() { if (!this.verbose) return; this.metrics.cachePerformance = { astCacheHits: 0, fileCacheHits: 0, totalCacheLookups: 0 }; } reset() { if (!this.verbose) return; this.metrics = this.getInitialMetrics(); } getMetrics() { return this.metrics; } trackCacheHit(type) { if (!this.verbose) return; if (type === "ast") { this.metrics.cachePerformance.astCacheHits++; } else { this.metrics.cachePerformance.fileCacheHits++; } } trackCacheLookup() { if (!this.verbose) return; this.metrics.cachePerformance.totalCacheLookups++; } } export function getPerformance(verbose = false) { return new Performance(verbose); } export class MoveTracker { perf; totalTimer = null; validationTimer = null; fileDiscoveryTimer = null; precomputeTimer = null; fileOpsTimer = null; analysisTimer = null; moveTimer = null; updateMovedFileTimer = null; updateTimer = null; constructor(verbose) { this.perf = getPerformance(verbose); } startTotalTimer() { this.totalTimer = this.perf.startTimer("Total execution"); } startValidationTimer() { this.validationTimer = this.perf.startTimer("Validation"); } endValidationTimer(moveCount) { if (this.validationTimer) { this.perf.addValidationTime(this.validationTimer.end(), moveCount); this.validationTimer = null; } } startFileDiscoveryTimer() { this.fileDiscoveryTimer = this.perf.startTimer("File discovery"); } endFileDiscoveryTimer(fileCount) { if (this.fileDiscoveryTimer) { this.perf.addDiscoveryTime(this.fileDiscoveryTimer.end(), fileCount); this.fileDiscoveryTimer = null; } } startPrecomputeTimer() { this.precomputeTimer = this.perf.startTimer("Pre-computing import paths"); } endPrecomputeTimer() { if (this.precomputeTimer) { this.precomputeTimer.end(); this.precomputeTimer = null; } } startFileOpsTimer() { this.fileOpsTimer = this.perf.startTimer("File operations"); } endFileOpsTimer() { if (this.fileOpsTimer) { this.perf.addFileOpTime("move", this.fileOpsTimer.end()); this.fileOpsTimer = null; } } startAnalysisTimer(moveIndex) { this.analysisTimer = this.perf.startTimer(`Import analysis for move ${moveIndex + 1}`); } endAnalysisTimer() { if (this.analysisTimer) { const time = this.analysisTimer.end(); this.analysisTimer = null; return time; } return 0; } clearFileAnalysisTimes() { this.perf.clearFileAnalysisTimes(); } getFileAnalysisTimes() { return this.perf.getMetrics().importAnalysis.individualFileTimes; } startMoveTimer(moveIndex) { this.moveTimer = this.perf.startTimer(`Physical file move ${moveIndex + 1}`); } endMoveTimer() { if (this.moveTimer) { const time = this.moveTimer.end(); this.moveTimer = null; return time; } return 0; } startUpdateMovedFileTimer(moveIndex) { this.updateMovedFileTimer = this.perf.startTimer(`Moved file import updates ${moveIndex + 1}`); } endUpdateMovedFileTimer() { if (this.updateMovedFileTimer) { const time = this.updateMovedFileTimer.end(); this.updateMovedFileTimer = null; return time; } return 0; } startUpdateTimer(moveIndex) { this.updateTimer = this.perf.startTimer(`Import updates for move ${moveIndex + 1}`); } endUpdateTimer() { if (this.updateTimer) { const time = this.updateTimer.end(); this.updateTimer = null; return time; } return 0; } addMoveMetrics(metrics) { this.perf.addMoveMetrics(metrics); } setTotalTime() { if (this.totalTimer) { this.perf.setTotalTime(this.totalTimer.end()); this.totalTimer = null; } } async printSummary() { await this.perf.printSummary(); } addFileOpUpdates(totalUpdates) { this.perf.addFileOpUpdates(totalUpdates); } setImportAnalysisTime(time, totalFiles, filesWithImports) { this.perf.setImportAnalysisTime(time, totalFiles, filesWithImports); } setImportAnalysisBreakdown(read, parse, match) { this.perf.setImportAnalysisBreakdown(read, parse, match); } startOverallAnalysisTimer() { return this.perf.startTimer("Overall import analysis"); } } /** * Batch update imports in multiple files to reduce I/O overhead */ export async function batchUpdateImports({ importAnalysis, tracker, }) { // OPTIMIZATION: Use concurrency limiting to prevent "too many open files" error const CONCURRENCY_LIMIT = 200; // Process max 50 files at once let totalUpdates = 0; for (let i = 0; i < importAnalysis.length; i += CONCURRENCY_LIMIT) { const batch = importAnalysis.slice(i, i + CONCURRENCY_LIMIT); const updatePromises = batch.map(async ({ file, imports }) => { const updated = await updateImportsInFile({ currentFilePath: file, imports, }); return updated ? 1 : 0; }); const batchResults = await Promise.all(updatePromises); const batchUpdates = batchResults.reduce((sum, count) => sum + count, 0); totalUpdates += batchUpdates; // Optional: Add a small delay between batches to reduce system load if (i + CONCURRENCY_LIMIT < importAnalysis.length) { await new Promise((resolve) => setTimeout(resolve, 10)); } } // Update performance metrics tracker.addFileOpUpdates(totalUpdates); return totalUpdates; } /** * Analyze which files import the target file with performance tracking */ export async function analyzeImportsWithTracking(sourceFiles, targetImportPaths, tracker) { if (globalThis.appState.verbose) { // console.log(`🔍 Analyzing imports for target: ${targetPath}`); // console.log(`🎯 Target import paths to match:`, targetImportPaths); } // Track overall analysis timing const overallAnalysisTimer = tracker.startOverallAnalysisTimer(); let totalFileReadTime = 0; let totalAstParseTime = 0; let totalImportMatchingTime = 0; let filesWithImports = 0; // OPTIMIZATION: Use concurrency limiting to prevent "too many open files" error const CONCURRENCY_LIMIT = 50; // Process max 50 files at once const results = []; for (let i = 0; i < sourceFiles.length; i += CONCURRENCY_LIMIT) { const batch = sourceFiles.slice(i, i + CONCURRENCY_LIMIT); const analysisPromises = batch.map(async (file) => { try { if (globalThis.appState.verbose) { // console.log(`📂 Analyzing file: ${file}`); } const content = await fs.readFile(file, "utf8"); const imports = findDependencyImports({ content, targetImportPaths, currentFile: file, }); if (globalThis.appState.verbose) { // console.log(`📂 Analyzing ${file}: ${imports.length} import(s) found`); } if (imports.length > 0) { filesWithImports++; return { file, imports }; } return null; } catch (error) { const normalizedFile = path.normalize(file); const scanningSelf = globalThis.appState.fileMoves.some(([fromPath]) => normalizedFile === fromPath); if (!scanningSelf) { console.warn(`⚠️ Could not read ${file}: ${error instanceof Error ? error.message : String(error)}`); } return null; } }); const batchResults = await Promise.all(analysisPromises); // Filter out null results and add to results array for (const result of batchResults) { if (result) { results.push(result); } } // Optional: Add a small delay between batches to reduce system load if (i + CONCURRENCY_LIMIT < sourceFiles.length) { await new Promise((resolve) => setTimeout(resolve, 10)); } } // Update overall metrics const overallAnalysisTime = overallAnalysisTimer.end(); tracker.setImportAnalysisTime(overallAnalysisTime, sourceFiles.length, filesWithImports); // Aggregate individual file times const individualFileTimes = tracker.getFileAnalysisTimes(); totalFileReadTime = individualFileTimes.reduce((sum, file) => sum + file.readTime, 0); totalAstParseTime = individualFileTimes.reduce((sum, file) => sum + file.parseTime, 0); totalImportMatchingTime = individualFileTimes.reduce((sum, file) => sum + file.matchTime, 0); tracker.setImportAnalysisBreakdown(totalFileReadTime, totalAstParseTime, totalImportMatchingTime); return results; } //# sourceMappingURL=moveTracker.js.map