Parallelize raw/ copy and document overall architecture

CopiaFileTrovati now copies with Parallel.ForEach (degree 6) instead of
sequentially: the bottleneck is network latency to the share, not CPU,
so overlapping copies gives a large speedup (~5x in a synthetic
benchmark with simulated SMB latency, documented in
docs/copia-raw-parallela.md).

Also adds docs/architettura.md, a full architecture overview of every
source file, the wizard's 5 phases, dialogs, persistence and standalone
scripts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
davideandClaude Sonnet 5 committed 2026-09-21 10:07:46 +02:00
1 parent 0966e66232
commit 6511d58b01
4 files changed
+201 -6

No files matched your search

+2 -1
View File
@@ -56,7 +56,8 @@ entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione inte
che contiene `CLAUDE.md`), indipendentemente da dove si trova il file `.xls`/`.xml`
di input scelto dall'utente.
3. **Crea cartella raw** — cerca nell'indice il percorso CAD di ogni componente del CSV
(`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`).
(`TrovaPercorsiCad`) e lo copia in `raw/` (`CopiaFileTrovati`, parallela — vedi
`docs/copia-raw-parallela.md`).
4. **Trova componenti referenziati** — legge euristicamente i metadati binari dei `.asm`
già in `raw/` (`AssemblyComponentScanner`) per risolvere ricorsivamente i
sotto-assiemi/parti che contengono (`RisolviAlberoComponenti`), copiandoli anch'essi
+7 -5
View File
@@ -954,23 +954,25 @@ public partial class Form1 : Form
}
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
/// Ritorna il numero di file copiati con successo.</summary>
/// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il
/// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie
/// concorrenti bastano a sovrapporre le attese senza saturare la share.</summary>
internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw)
{
Directory.CreateDirectory(cartellaRaw);
int copiati = 0;
foreach (var percorso in percorsi)
Parallel.ForEach(percorsi, new ParallelOptions { MaxDegreeOfParallelism = 6 }, percorso =>
{
try
{
Directory.CreateDirectory(cartellaRaw);
File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
copiati++;
Interlocked.Increment(ref copiati);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
}
}
});
return copiati;
}
+146
View File
@@ -0,0 +1,146 @@
# Architettura di CadSearchGui
Panoramica di tutti i file sorgente e di come collaborano. Per il "come si usa" vedi il
`README.md` in root; per il formato binario `.asm` vedi `docs/formato-asm.md`; per la
parallelizzazione della copia in `raw/` vedi `docs/copia-raw-parallela.md`.
## Vista d'insieme
Un solo eseguibile WinForms (nessuna CLI, nessun processo esterno): indicizza i file CAD di
una share di rete in un DB SQLite locale, poi guida l'utente in un wizard a 5 fasi che, a
partire da una distinta base esportata dal gestionale, raccoglie in locale (`raw/`) tutti i
file CAD dei componenti e dei loro sotto-assiemi, e verifica/raccoglie gli STEP corrispondenti
(`step_finali/`).
```
Distinta (.xls/.xml) → CSV (componente;descrizione) → raw/ (CAD copiati dall'indice)
│
scansione .asm ─────┘ (sotto-assiemi/parti mancanti)
│
step/ (esportati a mano da Creo) → step_finali/ (solo i codici distinta)
```
## Entry point
- **Program.cs** — `Main()` standard WinForms: `Application.Run(new Form1())`. Nessuna logica.
## Finestra principale (wizard)
- **Form1.cs** — tutta la UI è costruita in codice (`BuildUi`/`BuildFase*`), non con il
designer visuale. È un wizard: `_panelFasi[]` contiene un `Panel` per fase, uno solo
visibile alla volta (`VaiAllaFase`); si avanza con "Avanti" solo se `AvantiConsentito`
ritorna vero per la fase corrente. `Form1.Designer.cs` contiene solo lo scheletro minimo
generato dal template (`InitializeComponent`/`Dispose`), non è usato per costruire i
controlli.
Le 5 fasi e i metodi principali dietro ciascuna:
| # | Fase | Bottone → metodo | Cosa fa |
|---|---|---|---|
| 1 | Database | "Aggiorna indice" → `Indexer.ScanAsync` | Crea/aggiorna `cad_index.db` scansionando la share |
| 2 | Importa Distinta Base | selezione file → `ImportaDistinta` → `DistintaBaseImporter.ConvertiInCsv` | Xls/Xml gestionale → CSV componente;descrizione, scritto nella root del repo (`TrovaRadiceRepo`) |
| 3 | Crea cartella raw | "Crea cartella raw" → `CreaCartellaRaw` | `TrovaPercorsiCad` + `CopiaFileTrovati` (parallela) → `raw/` |
| 4 | Trova componenti referenziati | "Trova componenti referenziati" → `TrovaPrtReferenziati` → `RisolviAlberoComponenti` | Legge i `.asm` in `raw/` con `AssemblyComponentScanner`, risolve ricorsivamente sotto-assiemi/parti nell'indice, li copia anch'essi in `raw/` |
| 5 | Verifica completezza e STEP | "Verifica completezza assiemi" → `VerificaCompletezzaAssiemi`; "Crea cartella step_finali" → `GeneraReportStep` | Completezza ricorsiva sui percorsi in indice; confronto con `step/` → `step_finali/`. All'ingresso nella fase, `AggiornaAnteprimaAsm` mostra le icone trascinabili dei `.asm` della distinta già in `raw/` |
Stato condiviso tra le fasi (campi privati di `Form1`): `_dbPath`, `_percorsoInputCorrente`,
`_righeCorrenti` (righe del CSV), `_ultimaVerifica` (cache percorsi CAD), `_ultimaCartellaRaw`.
Altre responsabilità in Form1.cs:
- `TrovaPercorsiCad` / `CaricaCodiciNoti` — query sul DB (`files_ultima_versione`), la
seconda carica tutti i codici prt/asm noti in un colpo solo (usata sia per risolvere i
componenti sia da `AssemblyComponentScanner` per validare i token generici).
- `CopiaFileTrovati` — copia (in parallelo, grado 6) i file trovati in `raw/`, saltando
silenziosamente quelli irraggiungibili (share offline/permessi).
- `PulisciRawEStep` — svuota `raw/`, `step/`, `step_finali/` (manutenzione, richiede conferma).
- `InizializzaDatabase` / `ScegliDatabase` / `TrovaRadiceRepo` — localizzazione del DB e
della root del repo (quest'ultima risalendo da `AppContext.BaseDirectory` fino alla
cartella che contiene `CLAUDE.md`).
## Indicizzazione
- **Indexer.cs** — `ScanAsync` cammina tutte le cartelle di primo livello sotto la radice
(scoperte dinamicamente, non un elenco fisso) con un walk manuale a stack (`Walk`, più
veloce di `EnumerateFiles(..., AllDirectories)` su share con centinaia di migliaia di
file), esclude le cartelle `temp_*`, estrae da ogni nome file il pattern Pro-E
`<codice>.<tipo>.<versione>` (`ParseFilename`, regex), e fa un upsert incrementale riga per
riga confrontando `percorso_completo` + dimensione + data modifica. Progresso riportato via
`IProgress<ScanProgress>` (marshalato sul thread UI). Contiene anche lo schema SQL: tabella
`files` e vista `files_ultima_versione` (ultima versione per `codice`+`tipo_file` via
`ROW_NUMBER() OVER (...)`, non self-join+`GROUP BY` per evitare tempi quasi quadratici su
~200k righe).
## Scansione binaria dei `.asm`
- **AssemblyComponentScanner.cs** — estrae euristicamente i codici componente referenziati
dentro un `.asm` (formato per lo più binario, codici come stringhe ASCII leggibili al suo
interno). Due strategie: codici numerici Pro-E riconosciuti da soli via regex (come nei nomi
file), e nomi alfabetici di parti commerciali tokenizzati genericamente e validati contro
l'elenco di codici noti del DB (altrimenti il rumore binario sarebbe ingestibile). Dettagli
sul formato, l'euristica e i falsi positivi noti in `docs/formato-asm.md`.
## Import distinta base
- **DistintaBaseImporter.cs** — converte l'export del gestionale (SpreadsheetML XML con
estensione `.xls`/`.xml`, oppure `.xls`/`.xlsx` binario vero, distinti dai primi byte del
file) nelle sole colonne Componente/Descrizione, foglio `"Distinta Base"`. Se il nome del
file segue la convenzione `DBP_<codice>_<timestamp>`, aggiunge l'assieme generale come riga
in più (il foglio elenca solo i componenti sotto l'assieme). Scrive il CSV nella cartella
passata da `Form1` (root del repo), non necessariamente accanto al file di input.
## Finestre di dialogo (modali, mostrate a fine step)
- **ProgressForm.cs** — durante l'aggiornamento indice: barra di progresso, conteggi
nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima del tempo rimanente (dal ritmo
file/secondo rispetto al totale atteso, preso dal conteggio della scansione precedente
salvato nel DB).
- **ProgressoComponentiForm.cs** — dialog minimale (etichetta + barra) durante
`RisolviAlberoComponenti` in fase 4.
- **PrtMancantiForm.cs** — esito della risoluzione ricorsiva dell'albero componenti: PRT e
sotto-assiemi trovati e copiati in `raw/`, più i codici referenziati che non corrispondono a
nessun file indicizzato (possono essere disegni, componenti davvero mancanti, o falsi
positivi della scansione `.asm`).
- **ReportCompletezzaForm.cs** — per ogni assieme della distinta, se tutti i suoi componenti
(ricorsivamente fino ai PRT) sono nell'indice (✓/✗/? per "non trovato").
- **ReportStepForm.cs** — per ogni componente della distinta, `EsitoStep`: `Creato` (STEP
trovato e copiato), `StepMancante` (CAD in indice ma STEP non esportato),
`ComponenteNonTrovato` (nemmeno il CAD è in indice).
## Persistenza
- **DB** (`cad_index.db`, SQLite) — vedi tabella `files`/vista `files_ultima_versione` sopra.
Percorso: cercato accanto all'eseguibile o nella working directory all'avvio; altrimenti
selezionabile con "Apri DB...". Radice disco di scansione di default `X:\`
(`Form1.RadiceDiscoDefault`, hardcoded).
- **File su disco**, tutti nella root del repo (non nella cartella dell'eseguibile): il CSV
della distinta, `raw/` (CAD dei componenti e sotto-componenti), `step/` (STEP esportati a
mano da Creo, input), `step_finali/` (STEP dei soli codici della distinta, output).
## Script standalone (`scripts/`)
C# file-based apps (.NET 10, `dotnet run scripts/Nome.cs -- arg1 arg2`, nessun `.csproj`,
dipendenze extra con `#:package Nome@versione` in testa al file):
- **ExtractCreoStrings.cs** — BOM di un `.asm` risolvendo ricorsivamente i sotto-assiemi
contro `cad_index.db` (stessa logica di `AssiemeCompleto` in Form1, solo codici numerici
Pro-E).
- **BomViaDb.cs** — come sopra ma con la tokenizzazione generica validata contro il DB, per
intercettare anche i nomi alfabetici delle parti commerciali.
- **Analyze.cs** — dump del contesto testo/hex attorno a un pattern in un file binario, per
investigare a mano nuovi falsi positivi prima di toccare la regex di
`AssemblyComponentScanner`.
- **Query.cs** — query SQL ad-hoc contro `cad_index.db` da riga di comando.
> ⚠️ Nota importante di sicurezza: se `dotnet run` viene lanciato dentro `CadSearchGui/`
> (che contiene un `.csproj` di tipo `WinExe`), esegue l'app GUI vera invece dello script
> standalone e resta bloccato in attesa che una finestra si chiuda. Lanciare gli script da
> una cartella senza `.csproj` (es. la root del repo o `scripts/` stessa).
## Test
- **CadSearchGui.Tests/** (xUnit) — riferimento al progetto principale via `ProjectReference`
+ `InternalsVisibleTo` (in `CadSearchGui.csproj`), così i test chiamano direttamente
`Indexer.ParseFilename` e `AssemblyComponentScanner.EstraiCodiciReferenziati` senza esporli
come API pubblica. File: `IndexerParseFilenameTests.cs`,
`AssemblyComponentScannerTests.cs`, `DistintaBaseImporterTests.cs`,
`CaseInsensitiveLookupIntegrationTests.cs`.
+46
View File
@@ -0,0 +1,46 @@
# Copia parallela dei file in `raw/`
`Form1.CopiaFileTrovati` (usata in fase 3 "Crea cartella raw" e in fase 4 "Trova componenti
referenziati" per copiare i file trovati nell'indice dentro `raw/`) copia i file in parallelo
invece che uno alla volta.
## Perché
I file vengono letti da una share di rete (`X:\`, vedi CLAUDE.md). Il tempo per ogni copia è
dominato dalla latenza di rete (round-trip SMB per aprire/leggere/chiudere il file), non dalla
CPU: un thread passa la maggior parte del tempo in attesa della risposta del server. Copiare
più file in parallelo sovrappone queste attese invece di sommarle, con un guadagno quasi
proporzionale al grado di parallelismo.
## Implementazione
`Parallel.ForEach` con `MaxDegreeOfParallelism = 6` (non un thread per file: troppa
concorrenza sulla stessa share rischia di saturare le connessioni SMB o subire throttling dal
server, risultando più lento invece che più veloce). Il contatore dei file copiati usa
`Interlocked.Increment` per essere thread-safe. La logica di skip per file irraggiungibili
(share offline, permessi — `catch (IOException or UnauthorizedAccessException)`) resta
invariata: essendo dentro il corpo della lambda, ogni iterazione ha il proprio try/catch
indipendente.
Il valore 6 è una stima di partenza, non misurata sulla share reale: se in pratica risulta
troppo aggressivo o troppo conservativo per la vostra rete, va aggiustato empiricamente.
## Benchmark (sintetico)
Non essendoci accesso diretto alla share `X:\` da questo ambiente, il confronto prima/dopo è
stato fatto con uno script standalone (`dotnet run <file>.cs`, pattern già usato in `scripts/`)
che copia file locali fittizi aggiungendo un ritardo artificiale di 80 ms per file, per simulare
la latenza di un round-trip SMB (il disco locale da solo è troppo veloce per mostrare qualunque
differenza).
50 file, ritardo simulato 80 ms/file:
| Versione | Tempo totale |
|---|---|
| Prima (sequenziale) | 4751 ms |
| Ora (parallela, grado 6) | 961 ms |
~5x più veloce, coerente con l'aspettativa teorica (limite di ~6x dato il grado di
parallelismo, scontato l'overhead di scheduling). Essendo una simulazione, il guadagno reale
sulla share va verificato sul campo: la latenza effettiva, la banda disponibile e il
comportamento del server sotto più connessioni concorrenti possono cambiare il risultato.