docling-studio/docs/audit/reports/release-0.5.0/12-performance.md
Pier-Jean Malandrino efc27932dd refactor(audit): remediate 0.5.0 audit findings — clean architecture, security, DRY, SOLID, perf
Closes the 12 MAJ raised by the release/0.5.0 audit pipeline (cf.
docs/audit/reports/release-0.5.0/summary.md → summary-reaudit.md).

Volet 1 — Reasoning architecture (audits 01/02/06/07 strengthening)
  * Domain ports: LLMProvider, ReasoningRunner, ReasoningParseError
  * Domain DTOs: LLMProviderType, ReasoningResult, ReasoningIteration
  * infra/llm/ollama_provider.py — OllamaProvider with health_check
  * infra/docling_agent_reasoning.py — runner adapter, encapsulates the
    private _rag_loop call (tracked at docling-project/docling-agent#26),
    commits OLLAMA_HOST once at boot (eliminates the per-request env race),
    translates upstream IndexError into ReasoningParseError
  * api/reasoning.py — zero coupling to docling-agent / mellea / docling-core,
    consumes app.state.reasoning_runner via the port
  * main.py — DI wires OllamaProvider + DoclingAgentReasoningRunner at boot
    when REASONING_ENABLED=true and deps are importable
  * Rename RAG_* env vars → REASONING_*, endpoint /rag → /reasoning,
    type RAGResult → ReasoningResult, frontend feature flag wiring,
    i18n strings, tests, docs (BREAKING — pre-1.0 surface, no external
    consumers in production)
  * 17 new tests: adapter unit tests with sys.modules stubs, OllamaProvider
    httpx tests, R3 concurrent-host isolation, R6 multi-iteration trace
    serialization, R13 Protocol conformance via isinstance
  * E2E Karate scenario: nav-reasoning hidden when REASONING_ENABLED=false
  * README — Live Reasoning section (env vars, archi, link to issue #26)

Bloc B — Security (audit 08, dev-only context)
  * docker-compose.yml — DEV DEFAULTS header, OpenSearch DISABLE_SECURITY_PLUGIN
    flagged as dev-only with link to OpenSearch security docs
  * main.py — boot warning if NEO4J_URI is set with the default 'changeme'
    password, so prod operators can't silently inherit it

Bloc C — DRY frontend (audit 05)
  * shared/storage/keys.ts — STORAGE_KEYS centralised (theme, locale)
  * features/settings/store.ts — dead apiUrl ref + orphan i18n keys removed
  * api/schemas.py — DOCUMENT_STATUS_UPLOADED constant

Bloc D — Quality (audits 02/06/07/09/10/12)
  * domain/ports.py — DocumentConverter.supports_page_batching property
    (LSP fix, replaces isinstance(ServeConverter) check)
  * domain/ports.py — VectorStore.ping() (encapsulation, replaces
    _vector_store._client.info() reach-around)
  * api/analyses.py + api/ingestion.py — path params {job_id} → {analysis_id}
    aligned with the user-facing terminology (URLs unchanged)
  * api/documents.py — Path.read_bytes() + generate_preview() wrapped in
    asyncio.to_thread, unblocks the FastAPI event loop on /preview
  * infra/docling_tree.py — PEP 604 union for isinstance (Ruff UP038)
  * src/__tests__/integration/ — cross-feature integration test relocated
    out of features/history/ so feature folders stay self-contained
  * Tightened terminal `assert X is not None` checks (isinstance(.., datetime),
    exact value comparisons)

Validation
  * 446 backend pytest, 202 frontend vitest — all green
  * ruff + ruff format + ESLint + Prettier + vue-tsc clean
  * Re-audit verdict: 0 CRIT / 0 MAJ, score ~94/100, GO

Closes #200
2026-04-29 14:00:00 +02:00

4.2 KiB

Rapport d'audit : Performance & Ressources

Release : 0.5.0 Date : 2026-04-28 Auditeur : claude-code


Score de compliance

Metrique Valeur
Items conformes 13 / 15
Score 86.67 / 100
Ecarts CRITICAL 0
Ecarts MAJOR 1
Ecarts MINOR 1
Ecarts INFO 1

Ecarts constates

[MAJ] Blocage I/O synchrone dans endpoint async de prévisualisation

  • Localisation : document-parser/api/documents.py:115-116
  • Constat : L'endpoint /preview lit le fichier PDF entier en synchrone (with open(...); f.read()) dans une fonction async, bloquant l'event loop FastAPI. Cela pénalise la performance pour les gros PDFs et empêche les autres requêtes d'être traitées pendant la lecture.
  • Regle violee : 12.1.4 (poids 3 → revu comme MAJ car contexte limité : endpoint unique, rarement appelé en rafale, mais violation du principle async)
  • Remediation : Utiliser asyncio.to_thread() ou une lecture asynchrone pour charger le fichier sans bloquer l'event loop.

[MIN] Watchers Vue sans cleanup explicite en Pinia store

  • Localisation : frontend/src/features/settings/store.ts:27-39
  • Constat : Les watchers watch() et watchEffect() définis au niveau du store (lignes 27-39) ne disposent pas explicitement de leur cleanup. En théorie, Pinia décharge les stores mais les watchers restent actifs si le store persiste en mémoire.
  • Regle violee : 12.2.1 (poids 2 → revu comme MIN car watchers non critiques et scope limité au store)
  • Remediation : Retourner une fonction stop() depuis le composable ou encapsuler dans onUnmounted().

[INFO] Absence de pagination explicite sur /api/documents et /api/graph

  • Localisation : document-parser/api/documents.py:72-76 et document-parser/api/graph.py (find_all implicit)
  • Constat : Les endpoints GET /api/documents et GET /api/graph retournent la liste entière sans pagination explicite (frontend assume la limite implicite). Si le nombre de documents/nœuds du graphe croît (>10k), la payload API et le render frontend deviennent coûteux.
  • Regle violee : 12.3.2 (poids 1, information)
  • Remediation : Ajouter des paramètres limit et offset optionnels pour paginer les listes (déjà implémentés au niveau repository avec limit=200, offset=0).

Points positifs

  • Semaphore bien configuré (12.1.3) — MAX_CONCURRENT_ANALYSES défini à 3, implémenté avec asyncio.Semaphore(), appliqué dans la méthode _run_analysis().
  • Fichiers temporaires supprimés (12.1.2) — L'upload et la suppression de documents gèrent le disque correctement. Les fichiers sont supprimés lors de delete() après vérification du chemin.
  • Conversion asynchrone (12.1.4) — Docling (conversion CPU-intensive) est délégué via asyncio.to_thread() dans LocalConverter, libérant l'event loop.
  • Lecture en chunks (12.1.5) — L'endpoint /upload lit les uploads par chunks de 64 KB au lieu de charger le fichier entier en mémoire en une seule allocation.
  • Watchers avec cleanup (12.2.1, 12.2.2) — Dans les composants StudioPage.vue, GraphView.vue, BboxOverlay.vue, les event listeners sont correctement supprimés dans onBeforeUnmount() ou onMounted().
  • Pas de re-render excessif (12.2.4) — Utilisation cohérente de computed() pour les états dérivés, pas de fonctions au template.
  • Pas de requête N+1 évidente (12.1.1) — Les repositories utilisent des JOIN explicites (ex: _SELECT_WITH_DOC) et pas de boucles avec requêtes unitaires.

Verdict partiel : GO CONDITIONNEL

Condition : Appliquer la remediation [MAJ] pour l'endpoint /preview avant la release ou planifier la correction dans le sprint suivant avec une priorité haute. L'absence de fix n'est pas bloquante pour cette release (endpoint non critique, charge potentielle limitée), mais la dette technique doit être remboursée rapidement.

Justification :

  • Score 86.67 >= 80 → GO
  • 0 écart CRITICAL → aucun blocage de sécurité/intégrité
  • 1 écart MAJOR (preview I/O) → acceptable en GO CONDITIONNEL avec plan de remediation
  • 1 écart MINOR + 1 INFO → non bloquants