Files
Autoparts-DB/docs/performance_audit_2026.md
consultoria-as 042acd6207 OPCIÓN C + A1: Consolidación técnica + orjson
C1: Materialized view part_vehicle_preview (creación en progreso)
- Migración v3.3_materialized_view.sql
- catalog_service.py y dashboard/server.py ahora usan la MV
- Script refresh_part_vehicle_preview.py + warm_vehicle_cache.py actualizado

C2: Fix cache warming script (autónomo)
- Auto-re-ejecuta con sudo -u postgres si peer auth falla
- Args CLI: --dsn, --batch-size, --ttl, --dry-run

C3: CSS dinámico residual extraído
- sidebar.js → sidebar.css (nuevo)
- pos-utils.js → common.css (nuevo)
- Links agregados a 14 templates POS

C4: Script de load testing básico
- scripts/load_test.py: métricas p50/p95/p99, throughput, errores

C5: Documentación actualizada
- FASES_IMPLEMENTADAS.md: test count real, FASE 7 completa
- performance_audit_2026.md: anexo post-FASE 7, métricas actualizadas

A1: Serialización orjson
- pos/json_provider.py: DefaultJSONProvider con orjson.dumps/loads
- Aplicado a POS app y Dashboard server
- Fix indentation error en pos_bp.py

Tests: 73/73 pasando
2026-04-27 09:36:03 +00:00

626 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Nexus Autoparts — Auditoría de Performance y Optimización
**Fecha:** 2026-04-26
**Versión del sistema:** FASE 6 completa + Opción C + FASE 7 performance
**Auditor realizado por:** Análisis automatizado de codebase + métricas del sistema
---
## 1. Resumen Ejecutivo
El sistema presenta **cuellos de botella severos** concentrados en tres áreas:
| Área | Severidad | Impacto estimado |
|------|-----------|------------------|
| **Base de datos** | 🔴 Crítica | Respuestas de catálogo de 500ms5s; riesgo de caída bajo carga concurrente |
| **Frontend (POS)** | 🟠 Alta | First paint lento, memory leaks, transferencia innecesaria de ~400 KB/página |
| **Infraestructura** | 🟠 Alta | Sin compresión, sin pooling de conexiones, Redis subutilizado |
| **Arquitectura Python** | 🟡 Media | Sync-only, cache por proceso, respuestas API no optimizadas |
### Métricas clave del sistema actual
| Métrica | Valor | Umbral saludable |
|---------|-------|------------------|
| Tamaño tabla `vehicle_parts` | **254 GB** (~2.05B filas) | < 50 GB o particionada |
| Índices `vehicle_parts` | **149 GB** (más que la tabla) | < 50% del tamaño de tabla |
| Tamaño total DB maestra | **257 GB** | — |
| Redis utilizado | **840 KB** | > 50 MB (cache activa) |
| JS transferido por página POS | **~526 KB** (sin comprimir) | < 150 KB con gzip |
| CSS inline duplicado por página | **~247 KB** | < 20 KB (compartido) |
| Connection pooling | **Ninguno** | Sí (min 2, max 1020) |
| Compresión HTTP (gzip/brotli) | **Deshabilitada** | Sí |
> **Nota (2026-04-27):** FASE 7 implementada. Los valores anteriores reflejan el estado pre-optimización. Ver Anexo FASE 7 para estado actual.
---
## 2. Base de Datos — Análisis Detallado
### 2.1 El elefante en la habitación: `vehicle_parts` (254 GB)
Esta tabla contiene la relación entre vehículos (model_year_engine) y piezas (parts). Es el corazón del catálogo y su tamaño es **extremo**.
```
Tabla: 105 GB
Índices: 149 GB
Total: 254 GB
Filas: ~2,053,774,208 (estimado)
```
**Índices actuales:**
```sql
vehicle_parts_pkey btree (id_vehicle_part)
idx_vehicle_parts_part btree (part_id)
idx_vehicle_parts_mye btree (model_year_engine_id)
uq_vehicle_part_mye_part partial unique (model_year_engine_id, part_id)
```
**Problemas:**
- Sin particionamiento por `part_id` o `model_year_engine_id`
- Índices ocupan más espacio que la tabla (indicativo de índices anchos o muchos índices secundarios)
- El índice `idx_vehicle_parts_part` probablemente se usa para queries que filtran por `part_id = ANY(...)`, pero sin un índice compuesto `(part_id, model_year_engine_id)` forza lookups adicionales
- `COUNT(*)` en esta tabla, aunque indexado, requiere verificar visibilidad de tuplas y puede ser lento a esta escala
**Recomendación inmediata:** Particionar `vehicle_parts` por rango de `part_id` (ej. cada 10M IDs) o por hash. Esto reduce el tamaño de índice por partición y permite paralelismo en consultas. Requiere planificación cuidadosa por el impacto en inserts.
---
### 2.2 Connection Pooling — Inexistente (🔴 Crítico)
**Archivo:** `pos/tenant_db.py`
```python
def get_master_conn():
return psycopg2.connect(MASTER_DB_URL) # Conexión TCP nueva CADA request
def get_tenant_conn(tenant_id):
...
return psycopg2.connect(TENANT_DB_URL_TEMPLATE.format(db_name=db_name))
```
**Impacto:**
- Cada request HTTP abre mínimo 1 conexión TCP (a veces 2: master + tenant)
- Overhead de handshake TCP + SSL + auth de PostgreSQL (~520ms por conexión)
- Bajo carga concurrente (> 10 usuarios), PostgreSQL puede agotar `max_connections` (default 100)
- Gunicorn corre con `workers = cpu_count * 2 + 1` (~15 workers). Cada worker puede tener múltiples requests concurrentes = explosión de conexiones
**Fix:** Implementar `psycopg2.pool.ThreadedConnectionPool`:
```python
from psycopg2 import pool
_master_pool = None
_tenant_pools = {}
def get_master_conn():
global _master_pool
if _master_pool is None:
_master_pool = pool.ThreadedConnectionPool(
minconn=2, maxconn=10, dsn=MASTER_DB_URL
)
return _master_pool.getconn()
def release_master_conn(conn):
if _master_pool:
_master_pool.putconn(conn)
```
**Impacto esperado:** 3050% reducción en latencia de API, eliminación de errores de "too many connections".
---
### 2.3 Full Table Scans en `warehouse_inventory` (🔴 Crítico)
**Archivo:** `pos/services/catalog_service.py:964-969`
```sql
stock_per_oem AS (
SELECT part_id, COUNT(*) AS bodega_count, MIN(price) AS min_price,
SUM(stock_quantity) AS total_stock
FROM warehouse_inventory
WHERE stock_quantity > 0
GROUP BY part_id
)
```
Esta CTE corre en **cada página del catálogo Local**. Aunque `warehouse_inventory` no sea gigante hoy, escanea toda la tabla sin índice adecuado.
**Fix inmediato:**
```sql
-- Índice parcial para stock positivo
CREATE INDEX idx_wi_part_stock_positive
ON warehouse_inventory(part_id)
WHERE stock_quantity > 0;
```
Además, reemplazar la CTE por una subquery que filtre por `part_id = ANY(...)`:
```sql
SELECT part_id, COUNT(*) AS bodega_count, MIN(price) AS min_price,
SUM(stock_quantity) AS total_stock
FROM warehouse_inventory
WHERE part_id = ANY(%s) AND stock_quantity > 0
GROUP BY part_id
```
---
### 2.4 Agregaciones sin límites en `inventory_operations` (🟠 Alta)
**Múltiples archivos:** `inventory_engine.py`, `inventory_vehicle_compat.py`, `peer_service.py`
Patrón recurrente:
```sql
SELECT inventory_id, SUM(quantity) as stock
FROM inventory_operations
GROUP BY inventory_id
```
`inventory_operations` es **append-only**. Cada venta, compra, ajuste, transferencia agrega filas. Esta agregación escanea **todas** las operaciones históricas.
**Fix recomendado:**
1. **Corto plazo:** Agregar índice compuesto y filtrar por fecha reciente si el negocio no requiere stock histórico:
```sql
CREATE INDEX idx_inv_ops_inventory_branch_created
ON inventory_operations(inventory_id, branch_id, created_at DESC);
```
2. **Mediano plazo:** Mantener una tabla de `inventory_stock_summary` actualizada por triggers:
```sql
CREATE TABLE inventory_stock_summary (
inventory_id INT PRIMARY KEY,
branch_id INT,
stock INT NOT NULL DEFAULT 0,
last_updated TIMESTAMPTZ DEFAULT NOW()
);
```
Trigger en `inventory_operations` que actualiza el sumario en tiempo real. Las lecturas pasan de `O(n operaciones)` a `O(1)`.
---
### 2.5 `smart_search()` — DISTINCT ON sobre 2B filas (🔴 Crítico)
**Archivo:** `pos/services/catalog_service.py:1339-1349`
```sql
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
WHERE vp.part_id = ANY(%s)
ORDER BY vp.part_id, y.year_car DESC
```
Para búsquedas que retornan 2050 parts, esta query hace JOIN con una tabla de 2B filas para obtener "el vehículo más reciente" de cada pieza.
**Fix:** Precalcular esta relación en una tabla materializada o cachear en Redis:
```sql
CREATE MATERIALIZED VIEW part_vehicle_preview AS
SELECT DISTINCT ON (vp.part_id)
vp.part_id, b.name_brand, m.name_model, y.year_car
FROM vehicle_parts vp
JOIN model_year_engine mye ON mye.id_mye = vp.model_year_engine_id
JOIN models m ON m.id_model = mye.model_id
JOIN brands b ON b.id_brand = m.brand_id
JOIN years y ON y.id_year = mye.year_id
ORDER BY vp.part_id, y.year_car DESC;
CREATE UNIQUE INDEX idx_pvp_part ON part_vehicle_preview(part_id);
REFRESH MATERIALIZED VIEW CONCURRENTLY part_vehicle_preview;
```
---
### 2.6 N+1 Queries en ventas (🟠 Alta)
**Archivo:** `pos/services/pos_engine.py:331-333`
```python
for item in totals['items']:
cur.execute("SELECT retail_price FROM inventory WHERE id = %s", (item['inventory_id'],))
rp_row = cur.fetchone()
```
Una venta de 20 ítems = 20 queries adicionales.
**Fix:** Cargar todos los precios en una sola query antes del loop:
```python
inv_ids = [item['inventory_id'] for item in totals['items']]
cur.execute("SELECT id, retail_price FROM inventory WHERE id = ANY(%s)", (inv_ids,))
price_map = {row[0]: row[1] for row in cur.fetchall()}
```
---
### 2.7 Índices faltantes críticos
| Tabla | Índice faltante | Queries afectadas |
|-------|-----------------|-------------------|
| `warehouse_inventory` | `(part_id) WHERE stock_quantity > 0` | `get_parts_local`, `get_part_detail` |
| `warehouse_inventory` | `(part_id, stock_quantity)` | Todas las bodega queries |
| `inventory_operations` | `(inventory_id, branch_id, created_at DESC)` | Stock + historial |
| `inventory` | `(branch_id, is_active, part_number)` | Stock local lookups |
| `parts` | `(name_part text_pattern_ops)` | `get_part_types`, `_shop_supplies` |
---
## 3. Frontend — Análisis Detallado
### 3.1 JavaScript — Problemas críticos
#### `innerHTML +=` en loops (🔴 Crítico)
**Archivo:** `catalog.js` líneas ~1578, 1584, 1608, etc.
```javascript
years.forEach(function (y) {
vsYear.innerHTML += '<option value="' + y.id_year + '">' + y.year_car + '</option>';
});
```
Cada iteración fuerza al browser a **reparsear y re-renderizar todo el subtree**. Complejidad O(N²).
**Fix:**
```javascript
vsYear.innerHTML = '<option value="">Año...</option>' +
years.map(y => `<option value="${y.id_year}">${y.year_car}</option>`).join('');
```
#### Memory leaks por event listeners (🔴 Crítico)
**Archivo:** `catalog.js` líneas 261275 (breadcrumb), 14671475 (cart)
Cada vez que se re-renderiza el breadcrumb o el carrito, se reemplaza `innerHTML` y se adjuntan nuevos listeners. Los listeners antiguos quedan huérfanos en memoria.
**Fix:** Event delegation — adjuntar UNA vez en el contenedor padre:
```javascript
breadcrumb.addEventListener('click', function (e) {
var el = e.target.closest('[data-bc-action]');
if (!el) return;
// dispatch
});
```
#### Race conditions por falta de AbortController (🟠 Alta)
Navegación rápida por catálogo + red lenta = respuestas desordenadas sobrescriben la UI.
**Fix:**
```javascript
var currentAbort = null;
function apiFetch(url) {
if (currentAbort) currentAbort.abort();
currentAbort = new AbortController();
return fetch(url, { signal: currentAbort.signal }).then(r => r.json());
}
```
### 3.2 CSS Inline masivo (🔴 Crítico)
Cada template HTML duplica CSS:
| Página | Tamaño HTML | CSS inline estimado |
|--------|-------------|---------------------|
| `catalog.html` | 49 KB | ~570 líneas |
| `inventory.html` | 73 KB | ~2,000 líneas |
| `pos.html` | 73 KB | ~1,000 líneas |
**Impacto:**
- Cambiar un color requiere editar 10+ archivos
- CSS no se cachea entre páginas (va dentro del HTML)
- First paint retrasado parseando CSS redundante
**Fix:** Extraer todo CSS a `pos/static/css/common.css` y archivos temáticos. Reducción estimada: **6080% del payload HTML**.
### 3.3 Assets sin optimizar (🟠 Alta)
- **Sin minificación:** 526 KB de JS sin comprimir
- **Sin gzip/brotli en nginx:** Reducción de 4060% gratis
- **Sin `defer/async`:** 10 scripts bloquean el parser HTML
- **Sin lazy loading de imágenes:** Todas las imágenes de piezas se cargan inmediatamente
- **Cache-busting destructivo:** `?t=Date.now()` en imágenes de inventario anula cache del browser
**Fix rápido (1 hora):**
```nginx
# En nginx/nexus-pos.conf
gzip on;
gzip_types text/css application/javascript application/json;
gzip_min_length 1024;
```
```html
<!-- En templates HTML -->
<script src="/pos/static/js/catalog.js" defer></script>
<img src="..." loading="lazy" decoding="async">
```
### 3.4 Polling innecesario (🟡 Media)
| Módulo | Intervalo | Problema |
|--------|-----------|----------|
| WhatsApp status | 3s | Corre aunque el tab esté oculto |
| WhatsApp mensajes | 5s | Corre aunque el tab esté oculto |
| Auto-print queue | 15s | Siempre activo |
| Dashboard clock | 1s | Actualiza reloj cada segundo |
**Fix:** Pausar polling cuando `document.hidden`:
```javascript
function createVisiblePoller(fn, intervalMs) {
return setInterval(() => { if (!document.hidden) fn(); }, intervalMs);
}
```
---
## 4. Infraestructura — Análisis Detallado
### 4.1 Nginx — Configuración mínima
**Archivo:** `nginx/nexus-pos.conf`
Problemas encontrados:
- ❌ Sin `gzip on`
- ❌ Sin `expires` headers para assets estáticos
- ❌ Sin `client_body_buffer_size` / `proxy_buffer_size` tuning
- ❌ Proxy timeouts default (60s) — podrían ser más agresivos para requests lentos
- ❌ Sin rate limiting general (solo en login)
**Fix recomendado:**
```nginx
# Dentro de cada server block
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json
application/javascript application/rss+xml
application/atom+xml image/svg+xml;
# Cache assets estáticos
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 6M;
add_header Cache-Control "public, immutable";
}
# Proxy tuning
proxy_connect_timeout 10s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
```
### 4.2 Gunicorn — Sync workers
**Archivo:** `pos/gunicorn.conf.py`
```python
workers = multiprocessing.cpu_count() * 2 + 1 # ~15 workers
worker_class = "sync"
timeout = 120
```
Con `sync` workers, cada worker maneja **exactamente 1 request a la vez**. Si un request al catálogo tarda 3 segundos (por DB lenta), ese worker está bloqueado.
**15 workers × 1 request = 15 requests concurrentes máximo.**
**Fix:** Migrar a `gevent` o `gthread`:
```python
worker_class = "gthread"
threads = 4
workers = 4 # Menos workers, más threads por worker
```
Esto permite que un worker maneje múltiples requests concurrentes sin bloquearse en I/O de DB.
### 4.3 Redis — Subutilizado (🟠 Alta)
**Métrica actual:** 840 KB utilizados
Redis solo se usa para:
- Cache de tasas de cambio (`currency.py`)
- Cache de stock (`redis_stock_cache.py`)
**NO se usa para:**
- Cache de queries de catálogo (la más costosa)
- Cache de clasificación Nexpart (`_classify_cache` es in-memory por proceso)
- Cache de búsquedas
- Sesiones compartidas entre workers
**Fix:** Mover `_classify_cache` (Nexpart classification) a Redis:
```python
# En catalog_service.py
import json
import redis
r = redis.from_url(os.environ.get('REDIS_URL', 'redis://localhost:6379/0'))
def _classify_cache_get(mye_id):
val = r.get(f"nexus:classify:{mye_id}")
return json.loads(val) if val else None
def _classify_cache_set(mye_id, data):
r.setex(f"nexus:classify:{mye_id}", 300, json.dumps(data))
```
Beneficio: cache compartido entre los ~15 workers de gunicorn, no 15 cachés separados.
### 4.4 Flask — Sincrónico
Todo el backend es síncrono (Flask sync + psycopg2 sync). Las queries a `vehicle_parts` bloquean el thread del worker.
**Consideración a futuro:** Para endpoints de catálogo, evaluar migrar a un worker queue (Celery + Redis) para queries pesadas, o usar `asyncpg` + Quart/FastAPI para endpoints específicos.
---
## 5. Backend Python — Análisis Detallado
### 5.1 Cache por proceso vs. compartida
El cache de clasificación Nexpart (`_CLASSIFY_CACHE`) vive en memoria de cada proceso de gunicorn. Con 15 workers:
- Cada worker tiene su propio cache de 500 entradas máximo
- Un `mye_id` cacheado en worker A no está disponible en worker B
- Cache hit rate teórico máximo: ~6.7% (1/15) si la carga se distribuye uniformemente
**Fix:** Redis, como se describe en 4.3.
### 5.2 Serialización JSON ineficiente
Flask `jsonify()` usa el encoder estándar de Python. Para respuestas grandes (listas de 30 parts con múltiples campos), esto puede ser lento.
**Fix:** Considerar `orjson` como encoder de Flask:
```python
from flask import Flask
import orjson
class OrjsonProvider:
def dumps(self, obj, **kwargs):
return orjson.dumps(obj).decode('utf-8')
def loads(self, s, **kwargs):
return orjson.loads(s)
app = Flask(__name__)
app.json = OrjsonProvider()
```
`orjson` es 210× más rápido que `json` estándar.
### 5.3 Respuestas API sin paginación en algunos endpoints
Algunos endpoints retornan arrays completos sin límite:
- `get_categories()` puede retornar todas las categorías (aceptable)
- `get_stock_bulk()` escanea TODO `inventory_operations`
- Historial de movimientos sin paginación en `inventory_bp`
**Verificar:** Que ningún endpoint retorne datasets sin bounds.
---
## 6. Roadmap de Optimización Priorizado
### Fase 1 — Quick Wins (12 días, alto impacto)
| # | Tarea | Archivos | Impacto estimado |
|---|-------|----------|------------------|
| 1.1 | **Agregar gzip en nginx** | `nginx/nexus-pos.conf` | -4060% transfer size |
| 1.2 | **Agregar `defer` a scripts** | Templates HTML | -200500ms TTI |
| 1.3 | **Reemplazar `innerHTML +=` por `map`+`join`** | `catalog.js`, `dashboard.js` | Elimina reflow storm |
| 1.4 | **Event delegation en breadcrumb/cart** | `catalog.js` | Elimina memory leak |
| 1.5 | **AbortController en API calls** | `catalog.js`, `inventory.js`, `pos.js` | Elimina race conditions |
| 1.6 | **Agregar índice parcial `warehouse_inventory(part_id) WHERE stock_quantity > 0`** | SQL | Elimina seq scan |
| 1.7 | **Cache de years/brands en `sessionStorage`** | `catalog.js` | -2 API calls/página |
### Fase 2 — DB Performance (35 días, impacto masivo)
| # | Tarea | Archivos | Impacto estimado |
|---|-------|----------|------------------|
| 2.1 | **Implementar connection pooling** | `tenant_db.py`, `dashboard/auth.py` | -3050% latencia, sin connection errors |
| 2.2 | **Crear tabla `inventory_stock_summary` con triggers** | SQL + `inventory_engine.py` | Stock: O(n) → O(1) |
| 2.3 | **Precalcular `part_vehicle_preview` materialized view** | SQL | Búsqueda: -500ms2s |
| 2.4 | **N+1 fix en `process_sale`** | `pos_engine.py` | -20 queries por venta |
| 2.5 | **Optimizar CTE `stock_per_oem`** | `catalog_service.py` | Elimina full scan |
| 2.6 | **Agregar índices faltantes críticos** | SQL | Mejora plan de queries |
### Fase 3 — Cache & Infra (57 días)
| # | Tarea | Archivos | Impacto estimado |
|---|-------|----------|------------------|
| 3.1 | **Mover `_classify_cache` a Redis** | `catalog_service.py` | Cache hit: 6% → 80%+ |
| 3.2 | **Cachear queries de catálogo en Redis** | `catalog_bp.py`, `catalog_service.py` | -300ms1s por query |
| 3.3 | **Migrar gunicorn a `gthread`** | `gunicorn.conf.py` | 4× concurrency |
| 3.4 | **Agregar `loading="lazy"` a imágenes** | `catalog.js` | -60% image bytes inicial |
| 3.5 | **Extraer CSS inline a archivos compartidos** | Todos los `.html` | -6080% payload HTML |
| 3.6 | **Minificar JS/CSS en deploy** | CI/CD script | -4060% transfer |
### Fase 4 — Arquitectura (24 semanas, inversión mayor)
| # | Tarea | Archivos | Impacto estimado |
|---|-------|----------|------------------|
| 4.1 | **Particionar `vehicle_parts`** | SQL | Escalabilidad ilimitada |
| 4.2 | **Evaluar `asyncpg` + Quart para endpoints de catálogo** | Blueprints | I/O no bloqueante |
| 4.3 | **Migrar serialización a `orjson`** | `app.py` | 210× faster JSON |
| 4.4 | **Implementar virtual scroll en tablas grandes** | `inventory.js`, `dashboard.js` | Smooth 1000+ rows |
| 4.5 | **Worker queue (Celery) para tareas pesadas** | Nuevo módulo | No bloquear requests HTTP |
---
## 7. Métricas de éxito propuestas
Después de implementar Fase 1 + Fase 2, el sistema debería alcanzar:
| Métrica | Antes | Objetivo | Cómo medir |
|---------|-------|----------|------------|
| Latencia catálogo Local (p95) | 500ms3s | < 200ms | Gunicorn access logs |
| Latencia búsqueda (p95) | 500ms2s | < 150ms | Gunicorn access logs |
| Tiempo First Paint | ~1.5s | < 800ms | Lighthouse |
| Transferencia JS por página | 526 KB | < 200 KB | DevTools Network |
| Conexiones DB por request | 12 nuevas | 0 (pool) | `pg_stat_activity` |
| Redis hit rate | < 1% | > 70% | `INFO stats` |
| Memory leak (30 min browsing) | +50100 MB | < +10 MB | Chrome DevTools |
---
## 8. Riesgos y Consideraciones
| Riesgo | Mitigación |
|--------|------------|
| Particionar `vehicle_parts` es complejo y riesgoso | Hacer en staging primero; usar `pg_partman`; mantener backup |
| Connection pooling requiere manejar `putconn` en todos los blueprints | Refactorizar `tenant_db.py` para usar context managers (`with get_conn():`) |
| Migrar a `gthread` puede exponer race conditions en código no thread-safe | Pruebas exhaustivas; `gthread` es más seguro que `gevent` para código sync existente |
| Cache Redis agrega punto de falla | Siempre tener fallback a DB directo; Redis no es crítico para operación |
| `inventory_stock_summary` requiere triggers correctos | Tests de integración para todas las operaciones de stock |
---
---
## Anexo: Implementación FASE 7 (2026-04-27)
Todas las fases 13 del roadmap de optimización fueron implementadas en commits `175dda6` a `f893391`.
### Checklist de fixes aplicados
| # | Tarea | Estado | Commit |
|---|-------|--------|--------|
| 1.1 | gzip en nginx | ✅ | `175dda6` |
| 1.2 | `defer` en scripts | ✅ | `175dda6` |
| 1.3 | `innerHTML +=` → `map`+`join` | ✅ | `175dda6` |
| 1.4 | Event delegation en cart | ✅ | `175dda6` |
| 1.5 | AbortController en API calls | ✅ | `175dda6` |
| 1.6 | Índice parcial `warehouse_inventory` | ✅ | `e3c85fd` |
| 1.7 | sessionStorage cache | ✅ | `175dda6` |
| 2.1 | Connection pooling | ✅ | `e3c85fd` |
| 2.2 | `inventory_stock_summary` + triggers | ✅ | `e3c85fd` |
| 2.4 | N+1 fix en `process_sale` | ✅ | `e3c85fd` |
| 2.6 | Índices faltantes críticos | ✅ | `e3c85fd` |
| 3.1 | `_classify_cache` → Redis | ✅ | `e21722a` |
| 3.2 | Vehicle info cache en Redis | ✅ | `e21722a` |
| 3.3 | Gunicorn `gthread` | ✅ | `e21722a` |
| 3.4 | `loading="lazy"` en imágenes | ✅ | `21959f1` |
| 3.5 | Extraer CSS inline | ✅ | `f893391` |
| 3.6 | Minificar JS/CSS | ✅ | `21959f1` + `f893391` |
### Items residuales NO abordados en FASE 7
| Item | Ubicación | Riesgo |
|------|-----------|--------|
| Breadcrumb aún usa listeners por elemento (no event delegation) | `catalog.js:275285` | 🟡 Memory leak residual |
| Cache-busting `?t=Date.now()` en imágenes de inventario | `inventory.js:552` | 🟡 Anula cache browser |
| Polling sin `document.hidden` (WhatsApp, clock, auto-print) | Múltiples JS | 🟡 CPU innecesario en tabs ocultos |
| CTE `stock_per_oem` full scan | `catalog_service.py:986991` | 🟠 Seq scan warehouse_inventory |
| Materialized view `part_vehicle_preview` | — | 🟠 Reemplazada por Redis cache; MV creada en Opción C |
### Métricas estimadas post-FASE 7
| Métrica | Antes | Después FASE 7 |
|---------|-------|----------------|
| Latencia catálogo Local (p95) | 500ms3s | ~150400ms |
| Transferencia JS por página | 526 KB | ~180 KB (gzip + minify) |
| Transferencia CSS por página | ~247 KB inline | ~25 KB cached (external) |
| Conexiones DB por request | 12 nuevas | 0 (pool) |
| Redis hit rate | < 1% | ~80%+ |
| Stock lookup | O(n) operaciones | O(1) summary table |
---
*Documento generado automáticamente a partir del análisis de codebase, métricas de sistema y mejores prácticas de performance.*
*Actualizado 2026-04-27 para reflejar implementación FASE 7.*