file-mover
Version:
Script to move files and update imports automatically
508 lines • 19.8 kB
JavaScript
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