From a49d338741891354318e9047a725e45dea2b82b1 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 16 Mar 2026 15:16:27 +0100 Subject: [PATCH] perf(scanner): ottimizza scansione destinazione con pass unico e barra determinata Sostituisce le due scansioni sequenziali (cartelle + file CAD) con un unico passaggio parallelo in services/destinationScanner.js. La lettura di primo livello fornisce il totale delle sezioni, rendendo la barra di progresso determinata (N / totale) durante l'analisi della destinazione. Il label mostra contemporaneamente sezioni completate e file CAD trovati --- renderer/renderer.js | 14 ++-- services/destinationScanner.js | 114 +++++++++++++++++++++++++++++++++ services/folderProcessor.js | 6 +- services/zipProcessor.js | 6 +- 4 files changed, 128 insertions(+), 12 deletions(-) create mode 100644 services/destinationScanner.js diff --git a/renderer/renderer.js b/renderer/renderer.js index 81cab9b..6041106 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -32,16 +32,22 @@ function hideProgress() { progressContainer.style.display = 'none'; } +let _indexFilesFound = 0; + function updateProgress({ phase, current, total, file, scanned, folder }) { if (phase === 'scan') { + _indexFilesFound = 0; progressLabel.textContent = 'Scansione file sorgente...'; progressBar.removeAttribute('value'); } else if (phase === 'index-dest') { - progressLabel.textContent = `Analisi destinazione: ${scanned} cartelle scansionate${folder ? ` — ${folder}` : ''}`; - progressBar.removeAttribute('value'); + progressBar.max = total; + progressBar.value = current; + const filesInfo = _indexFilesFound > 0 ? ` — ${_indexFilesFound} file CAD trovati` : ''; + progressLabel.textContent = `Analisi destinazione: ${current} / ${total}${folder ? ` — ${folder}` : ''}${filesInfo}`; } else if (phase === 'index-dup') { - progressLabel.textContent = `Ricerca duplicati: ${scanned} file CAD trovati${file ? ` — ${file}` : ''}`; - progressBar.removeAttribute('value'); + _indexFilesFound = scanned; + const dirInfo = progressBar.max > 0 ? `${progressBar.value} / ${progressBar.max} — ` : ''; + progressLabel.textContent = `Analisi destinazione: ${dirInfo}${scanned} file CAD trovati`; } else if (phase === 'copy') { progressBar.max = total; progressBar.value = current; diff --git a/services/destinationScanner.js b/services/destinationScanner.js new file mode 100644 index 0000000..0cea3de --- /dev/null +++ b/services/destinationScanner.js @@ -0,0 +1,114 @@ +const fs = require('fs-extra'); +const path = require('path'); +const { getCadInfo } = require('./router'); + +const CODE_FOLDER_REGEX = /^\d{3}$/; +const MAX_SCAN_DEPTH = 6; + +function toCadKey(cadInfo) { + return String(cadInfo?.key || '').toLowerCase(); +} + +/** + * Scansiona la cartella di destinazione in un unico passaggio parallelo e + * costruisce contemporaneamente: + * - folderIndex: Map (per il routing) + * - cadKeyIndex: Set (per il rilevamento duplicati) + * + * Legge prima le sottocartelle di primo livello (1 sola readdir) per ottenere + * il totale e mostrare una barra determinata durante la scansione. + */ +async function buildDestinationIndexes(destinationRoot, onProgress) { + const folderIndex = new Map(); + const cadKeyIndex = new Set(); + + if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { + return { folderIndex, cadKeyIndex }; + } + + // Lettura di primo livello: 1 sola chiamata per avere il totale + let topEntries; + try { + topEntries = await fs.readdir(destinationRoot, { withFileTypes: true }); + } catch { + return { folderIndex, cadKeyIndex }; + } + + const topDirs = topEntries + .filter((e) => e.isDirectory()) + .map((e) => ({ name: e.name, fullPath: path.join(destinationRoot, e.name) })); + + const total = topDirs.length; + let completed = 0; + let scannedFiles = 0; + + // Registra i file CAD al primo livello (non cartelle) + for (const entry of topEntries) { + if (!entry.isFile()) continue; + const cadInfo = getCadInfo(entry.name); + if (cadInfo) { + scannedFiles += 1; + cadKeyIndex.add(toCadKey(cadInfo)); + } + } + + // Walk ricorsivo del sottoalbero di una cartella di primo livello + async function walkSubtree(dirPath, depth) { + if (depth > MAX_SCAN_DEPTH) { + return; + } + + let entries; + try { + entries = await fs.readdir(dirPath, { withFileTypes: true }); + } catch { + return; + } + + const subdirPromises = []; + + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + + if (entry.isDirectory()) { + if (CODE_FOLDER_REGEX.test(entry.name)) { + const rows = folderIndex.get(entry.name) || []; + rows.push(fullPath); + folderIndex.set(entry.name, rows); + } + subdirPromises.push(walkSubtree(fullPath, depth + 1)); + } else if (entry.isFile()) { + const cadInfo = getCadInfo(entry.name); + if (cadInfo) { + scannedFiles += 1; + onProgress?.({ phase: 'index-dup', scanned: scannedFiles, file: entry.name }); + cadKeyIndex.add(toCadKey(cadInfo)); + } + } + } + + await Promise.all(subdirPromises); + } + + // Processa tutte le cartelle di primo livello in parallelo + await Promise.all( + topDirs.map(async ({ name, fullPath }) => { + onProgress?.({ phase: 'index-dest', current: completed, total, folder: name }); + + if (CODE_FOLDER_REGEX.test(name)) { + const rows = folderIndex.get(name) || []; + rows.push(fullPath); + folderIndex.set(name, rows); + } + + await walkSubtree(fullPath, 1); + + completed += 1; + onProgress?.({ phase: 'index-dest', current: completed, total, folder: name }); + }) + ); + + return { folderIndex, cadKeyIndex }; +} + +module.exports = { buildDestinationIndexes, toCadKey }; diff --git a/services/folderProcessor.js b/services/folderProcessor.js index 5d7cc56..fb97d57 100644 --- a/services/folderProcessor.js +++ b/services/folderProcessor.js @@ -1,8 +1,7 @@ const fs = require('fs-extra'); const path = require('path'); const { getDestinationDecision, getCadInfo } = require('./router'); -const { buildDestinationIndex } = require('./destinationIndex'); -const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex'); +const { buildDestinationIndexes, toCadKey } = require('./destinationScanner'); const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted'); function parseNumericVersion(version) { @@ -70,8 +69,7 @@ async function processFolder(folder, config, onProgress) { onProgress?.({ phase: 'scan' }); const files = await collectFilesRecursively(folder); - const destinationIndex = await buildDestinationIndex(config?.destination, onProgress); - const existingCadKeys = await buildExistingCadKeyIndex(config?.destination, onProgress); + const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress); const sourceMaxVersions = buildHighestNumericVersionByCadKey(files); const result = { scanned: 0, diff --git a/services/zipProcessor.js b/services/zipProcessor.js index 217ea55..9ea0c6c 100644 --- a/services/zipProcessor.js +++ b/services/zipProcessor.js @@ -3,8 +3,7 @@ const fs = require('fs-extra'); const path = require('path'); const { pipeline } = require('stream/promises'); const { getDestinationDecision, getCadInfo } = require('./router'); -const { buildDestinationIndex } = require('./destinationIndex'); -const { buildExistingCadKeyIndex, toCadKey } = require('./duplicateIndex'); +const { buildDestinationIndexes, toCadKey } = require('./destinationScanner'); const { prepareUnroutedTarget, prepareDuplicateTarget, getSkippedTarget } = require('./unrouted'); function parseNumericVersion(version) { @@ -60,8 +59,7 @@ async function processZip(zipPath, config, onProgress) { onProgress?.({ phase: 'scan' }); const { index: sourceMaxVersions, total } = await buildZipHighestNumericVersionByCadKey(zipPath); - const destinationIndex = await buildDestinationIndex(config?.destination, onProgress); - const existingCadKeys = await buildExistingCadKeyIndex(config?.destination, onProgress); + const { folderIndex: destinationIndex, cadKeyIndex: existingCadKeys } = await buildDestinationIndexes(config?.destination, onProgress); const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true })); const result = {