Skip to content

Commit fab3cf3

Browse files
committed
perf: auf asynchrone fs‑Operationen umgestellt; Streaming & Batch‑Lesen für große Dateien ⚡️
Kurz: - fs.promises verwendet und synchrone Dateioperationen ersetzt - neue async‑Hilfen: pathExistsAsync, getCachedFileContentAsync, readLargeFileAsync, getLogItemsAsync, handleOpenFileWithoutPathAsync - Batch‑Verarbeitung beim Verzeichnislesen und streambasiertes Lesen für >50MB Dateien - registerCommands und LogViewer auf async‑Flow angepasst; doppelte pathExists‑Implementierung bereinigt
1 parent 797f061 commit fab3cf3

File tree

3 files changed

+326
-26
lines changed

3 files changed

+326
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,18 @@ All notable changes to the "magento-log-viewer" extension will be documented in
99
- perf: Implemented dynamic cache configuration based on available system memory
1010
- perf: Added intelligent cache size management with automatic optimization under memory pressure
1111
- perf: Enhanced cache statistics and monitoring capabilities for better performance insights
12+
- perf: Replaced synchronous file operations with asynchronous alternatives to prevent UI blocking
13+
- perf: Added stream-based reading for large files (>50MB) to improve memory efficiency
14+
- perf: Implemented batch processing for directory reads to prevent system overload
1215
- feat: Added user-configurable cache settings: `cacheMaxFiles`, `cacheMaxFileSize`, `enableCacheStatistics`
1316
- feat: Added "Show Cache Statistics" command for real-time cache monitoring
1417
- feat: Cache now automatically scales from 20-100 files and 1-10MB based on available memory
18+
- feat: Added asynchronous file content reading with automatic fallback to synchronous for compatibility
1519
- fix: Cache management now removes multiple old entries efficiently instead of one-by-one cleanup
1620
- fix: Added automatic cache optimization when system memory usage exceeds 80%
1721
- fix: Improved memory usage estimation and monitoring for cached file contents
22+
- fix: Eliminated redundant `pathExists` function implementations across modules
23+
- fix: Consolidated all path existence checks to use centralized helpers functions
1824

1925
---
2026

src/helpers.ts

Lines changed: 241 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as vscode from 'vscode';
22
import * as path from 'path';
33
import * as fs from 'fs';
4+
import { promises as fsPromises } from 'fs';
45
import { LogViewerProvider, ReportViewerProvider, LogItem } from './logViewer';
56

67
// Prompts the user to confirm if the current project is a Magento project.
@@ -108,18 +109,18 @@ export function registerCommands(context: vscode.ExtensionContext, logViewerProv
108109
const message = `Cache: ${stats.size}/${stats.maxSize} files | Memory: ${stats.memoryUsage} | Max file size: ${Math.round(stats.maxFileSize / 1024 / 1024)} MB`;
109110
vscode.window.showInformationMessage(message);
110111
}); // Improved command registration for openFile
111-
vscode.commands.registerCommand('magento-log-viewer.openFile', (filePath: string | unknown, lineNumber?: number) => {
112+
vscode.commands.registerCommand('magento-log-viewer.openFile', async (filePath: string | unknown, lineNumber?: number) => {
112113
// If filePath is not a string, show a selection box with available log files
113114
if (typeof filePath !== 'string') {
114-
handleOpenFileWithoutPath(magentoRoot);
115+
await handleOpenFileWithoutPathAsync(magentoRoot);
115116
return;
116117
}
117118

118119
// If it's just a line number (e.g. "/20")
119120
if (filePath.startsWith('/') && !filePath.includes('/')) {
120121
const possibleLineNumber = parseInt(filePath.substring(1));
121122
if (!isNaN(possibleLineNumber)) {
122-
handleOpenFileWithoutPath(magentoRoot, possibleLineNumber);
123+
await handleOpenFileWithoutPathAsync(magentoRoot, possibleLineNumber);
123124
return;
124125
}
125126
}
@@ -176,7 +177,7 @@ export function clearAllLogFiles(logViewerProvider: LogViewerProvider, magentoRo
176177
vscode.window.showWarningMessage('Are you sure you want to delete all log files?', 'Yes', 'No').then(selection => {
177178
if (selection === 'Yes') {
178179
const logPath = path.join(magentoRoot, 'var', 'log');
179-
if (logViewerProvider.pathExists(logPath)) {
180+
if (pathExists(logPath)) {
180181
const files = fs.readdirSync(logPath);
181182
files.forEach(file => fs.unlinkSync(path.join(logPath, file)));
182183
logViewerProvider.refresh();
@@ -317,7 +318,25 @@ export function isValidPath(filePath: string): boolean {
317318
}
318319
}
319320

320-
// Checks if the given path exists.
321+
/**
322+
* Checks if the given path exists (asynchronous version)
323+
* @param p Path to check
324+
* @returns Promise<boolean> - true if path exists, false otherwise
325+
*/
326+
export async function pathExistsAsync(p: string): Promise<boolean> {
327+
try {
328+
await fsPromises.access(p);
329+
return true;
330+
} catch (err) {
331+
return false;
332+
}
333+
}
334+
335+
/**
336+
* Checks if the given path exists (synchronous fallback for compatibility)
337+
* @param p Path to check
338+
* @returns boolean - true if path exists, false otherwise
339+
*/
321340
export function pathExists(p: string): boolean {
322341
try {
323342
fs.accessSync(p);
@@ -401,6 +420,63 @@ export function getIconForLogLevel(level: string): vscode.ThemeIcon {
401420
}
402421
}
403422

423+
// Asynchronous version of getLogItems for better performance
424+
export async function getLogItemsAsync(dir: string, parseTitle: (filePath: string) => string, getIcon: (filePath: string) => vscode.ThemeIcon): Promise<LogItem[]> {
425+
if (!(await pathExistsAsync(dir))) {
426+
return [];
427+
}
428+
429+
const items: LogItem[] = [];
430+
431+
try {
432+
const files = await fsPromises.readdir(dir);
433+
434+
// Process files in batches to avoid overwhelming the system
435+
const batchSize = 10;
436+
for (let i = 0; i < files.length; i += batchSize) {
437+
const batch = files.slice(i, i + batchSize);
438+
439+
const batchPromises = batch.map(async (file) => {
440+
const filePath = path.join(dir, file);
441+
442+
try {
443+
const stats = await fsPromises.stat(filePath);
444+
445+
if (stats.isDirectory()) {
446+
const subItems = await getLogItemsAsync(filePath, parseTitle, getIcon);
447+
return subItems.length > 0 ? subItems : [];
448+
} else if (stats.isFile()) {
449+
const title = parseTitle(filePath);
450+
const logFile = new LogItem(title, vscode.TreeItemCollapsibleState.None, {
451+
command: 'magento-log-viewer.openFile',
452+
title: 'Open Log File',
453+
arguments: [filePath]
454+
});
455+
logFile.iconPath = getIcon(filePath);
456+
return [logFile];
457+
}
458+
} catch (error) {
459+
console.error(`Error processing file ${filePath}:`, error);
460+
}
461+
462+
return [];
463+
});
464+
465+
const batchResults = await Promise.all(batchPromises);
466+
items.push(...batchResults.flat());
467+
468+
// Small delay between batches to prevent blocking
469+
if (i + batchSize < files.length) {
470+
await new Promise(resolve => setTimeout(resolve, 1));
471+
}
472+
}
473+
} catch (error) {
474+
console.error(`Error reading directory ${dir}:`, error);
475+
}
476+
477+
return items;
478+
}
479+
404480
export function getLogItems(dir: string, parseTitle: (filePath: string) => string, getIcon: (filePath: string) => vscode.ThemeIcon): LogItem[] {
405481
if (!pathExists(dir)) {
406482
return [];
@@ -498,7 +574,82 @@ function getReportContent(filePath: string): unknown | null {
498574
}
499575
}
500576

501-
// Enhanced file content caching function
577+
// Enhanced file content caching function (asynchronous)
578+
export async function getCachedFileContentAsync(filePath: string): Promise<string | null> {
579+
try {
580+
// Check if file exists first
581+
if (!(await pathExistsAsync(filePath))) {
582+
return null;
583+
}
584+
585+
const stats = await fsPromises.stat(filePath);
586+
587+
// For very large files (>50MB), use streaming
588+
if (stats.size > 50 * 1024 * 1024) {
589+
return readLargeFileAsync(filePath);
590+
}
591+
592+
// Don't cache files larger than configured limit to prevent memory issues
593+
if (stats.size > CACHE_CONFIG.maxFileSize) {
594+
return await fsPromises.readFile(filePath, 'utf-8');
595+
}
596+
597+
const cachedContent = fileContentCache.get(filePath);
598+
599+
// Return cached content if it's still valid
600+
if (cachedContent && cachedContent.timestamp >= stats.mtime.getTime()) {
601+
return cachedContent.content;
602+
}
603+
604+
// Read file content asynchronously
605+
const content = await fsPromises.readFile(filePath, 'utf-8');
606+
607+
// Manage cache size - remove oldest entries if cache is full
608+
if (fileContentCache.size >= CACHE_CONFIG.maxSize) {
609+
// Remove multiple old entries if we're significantly over the limit
610+
const entriesToRemove = Math.max(1, Math.floor(CACHE_CONFIG.maxSize * 0.1));
611+
const keys = Array.from(fileContentCache.keys());
612+
613+
for (let i = 0; i < entriesToRemove && keys.length > 0; i++) {
614+
fileContentCache.delete(keys[i]);
615+
}
616+
}
617+
618+
// Cache the content
619+
fileContentCache.set(filePath, {
620+
content,
621+
timestamp: stats.mtime.getTime()
622+
});
623+
624+
return content;
625+
} catch (error) {
626+
console.error(`Error reading file ${filePath}:`, error);
627+
return null;
628+
}
629+
}
630+
631+
// Stream-based reading for very large files
632+
async function readLargeFileAsync(filePath: string): Promise<string> {
633+
return new Promise((resolve, reject) => {
634+
const chunks: Buffer[] = [];
635+
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
636+
637+
stream.on('data', (chunk: Buffer) => {
638+
chunks.push(chunk);
639+
});
640+
641+
stream.on('end', () => {
642+
resolve(Buffer.concat(chunks).toString('utf8'));
643+
});
644+
645+
stream.on('error', (error) => {
646+
console.error(`Error reading large file ${filePath}:`, error);
647+
reject(error);
648+
});
649+
});
650+
}
651+
652+
// Enhanced file content caching function (synchronous - for compatibility)
502653
export function getCachedFileContent(filePath: string): string | null {
503654
try {
504655
// Check if file exists first
@@ -731,7 +882,90 @@ export function formatTimestamp(timestamp: string): string {
731882
}
732883
}
733884

734-
// Shows a dialog to select a log file when no path is provided
885+
// Shows a dialog to select a log file when no path is provided (async version)
886+
export async function handleOpenFileWithoutPathAsync(magentoRoot: string, lineNumber?: number): Promise<void> {
887+
try {
888+
// Collect log and report files asynchronously
889+
const logPath = path.join(magentoRoot, 'var', 'log');
890+
const reportPath = path.join(magentoRoot, 'var', 'report');
891+
const logFiles: string[] = [];
892+
const reportFiles: string[] = [];
893+
894+
// Check directories and read files in parallel
895+
const [logExists, reportExists] = await Promise.all([
896+
pathExistsAsync(logPath),
897+
pathExistsAsync(reportPath)
898+
]);
899+
900+
const fileReadPromises: Promise<void>[] = [];
901+
902+
if (logExists) {
903+
fileReadPromises.push(
904+
fsPromises.readdir(logPath).then(files => {
905+
return Promise.all(files.map(async file => {
906+
const filePath = path.join(logPath, file);
907+
const stats = await fsPromises.stat(filePath);
908+
if (stats.isFile()) {
909+
logFiles.push(filePath);
910+
}
911+
}));
912+
}).then(() => {})
913+
);
914+
}
915+
916+
if (reportExists) {
917+
fileReadPromises.push(
918+
fsPromises.readdir(reportPath).then(files => {
919+
return Promise.all(files.map(async file => {
920+
const filePath = path.join(reportPath, file);
921+
const stats = await fsPromises.stat(filePath);
922+
if (stats.isFile()) {
923+
reportFiles.push(filePath);
924+
}
925+
}));
926+
}).then(() => {})
927+
);
928+
}
929+
930+
await Promise.all(fileReadPromises);
931+
932+
// Create a list of options for the quick pick
933+
const options: { label: string; description: string; filePath: string }[] = [
934+
...logFiles.map(filePath => ({
935+
label: path.basename(filePath),
936+
description: 'Log File',
937+
filePath
938+
})),
939+
...reportFiles.map(filePath => ({
940+
label: path.basename(filePath),
941+
description: 'Report File',
942+
filePath
943+
}))
944+
];
945+
946+
// If no files were found
947+
if (options.length === 0) {
948+
showErrorMessage('No log or report files found.');
949+
return;
950+
}
951+
952+
// Show a quick pick dialog
953+
const selection = await vscode.window.showQuickPick(options, {
954+
placeHolder: lineNumber !== undefined ?
955+
`Select a file to navigate to line ${lineNumber}` :
956+
'Select a log or report file'
957+
});
958+
959+
if (selection) {
960+
openFile(selection.filePath, lineNumber);
961+
}
962+
} catch (error) {
963+
showErrorMessage(`Error fetching log files: ${error instanceof Error ? error.message : String(error)}`);
964+
console.error('Error fetching log files:', error);
965+
}
966+
}
967+
968+
// Shows a dialog to select a log file when no path is provided (sync fallback)
735969
export function handleOpenFileWithoutPath(magentoRoot: string, lineNumber?: number): void {
736970
try {
737971
// Collect log and report files

0 commit comments

Comments
 (0)