# Rendimiento: diagnóstico y propuestas

**Estado:** diagnóstico · 24/10/2026
**Cómo se midió:**
- GET de cada pantalla desde local, con el usuario 14, pasando por `/login` del subdominio para cargar la sesión del cliente.
- Las conexiones a la base, con una transacción revertida.
- Herramienta: `perfil.php`, que registra cada consulta con `DB::listen`.
- La consulta principal de `/rpo` se comparó en variantes de solo `SELECT`.

> **Ojo con las cifras absolutas.** Desde local, cada consulta cuesta unos **160 ms de red** hasta `asercolgroup.com`: un `SELECT 1` tarda 160 ms. En el servidor nuevo, si la base está en la misma red, ese costo baja a casi nada. Por eso hay dos tipos de problema:
> - **Consultas repetidas (N+1):** en local pesan mucho, por la latencia. En producción pesan menos, pero se multiplican con cada fila.
> - **Consultas caras en sí mismas:** pesan igual en local y en producción. Son la prioridad.

## 1. Mediciones

| Pantalla | Cliente | Total | SQL | Consultas | HTML | Causa principal |
|---|---|---|---|---|---|---|
| `/rpo` | Tenaris | 26,9 s | 26,6 s | 118 | 714 KB | Consulta principal sin paginar (7,7 s) + facturación por fila (100 consultas) |
| `/rexpo` | Tenaris | 19,7 s | 19,5 s | 116 | 342 KB | Facturación por fila: 4 consultas propias + Websimex por cada fila |
| `/rhistorico` | Tenaris | 24,5 s | 24,3 s | 114 | 361 KB | Igual que `/rpo` |
| `/rtorrecontrol` | Tenaris | 42,6 s | 33,3 s | 38 | 247 KB | 15 conteos de estado, uno por consulta (16 s), y 9 s fuera de SQL |
| `reporte/globalbrenntagws` | Asercol | 31,2 s | 31,0 s | 189 | 1,4 MB | **182 consultas iguales a `dias_festivos`** (25,9 s) |
| `dashboard/impo` | Asercol | 5,9 s | 5,8 s | 38 | 201 KB | La misma consulta de compañías 6 veces; 25 a Websimex |
| `impo/pedidos` | Tenaris | 4,4 s | 3,8 s | 19 | **5,7 MB** | Estado del componente enorme (`etapasEh`), que viaja en cada clic |

## 2. Hallazgos y propuestas

Ordenadas por impacto sobre esfuerzo.

### R1. Paginar en la base, no en PHP (`/rpo`, `/rhistorico`, `/rexpo`, búsquedas)

`POController@index` y `search` traen **todas** las órdenes del cliente con `DB::select($SQL)` y después paginan el arreglo en PHP con `PaginarArray()`. Para Tenaris son 6.747 filas, y cada una calcula su estado y su color con subconsultas a `actividad`: unas 60 por fila.

| Variante de la misma consulta | Tiempo |
|---|---|
| Traer todas las filas (hoy) | 8.169 ms |
| `SELECT COUNT(*)` | 261 ms |
| `LIMIT 20` (una página) | 229 ms |

**Propuesta:**
- Pedir a la base solo la página (`LIMIT/OFFSET`) y el total con un `COUNT(*)`.
- Las rutas de "Actualizar fechas" (`actualizarDatos`) necesitan los ids de todas las filas: se sacan con una consulta liviana de `id` y `NUM_DO`, sin el cálculo de estado.
- **Ganancia:** unas 35 veces menos en la consulta principal.
- **Cuidado:** el filtro por estado (`WHERE STATUS = …`) sigue necesitando calcular el estado de todas. Solo ese caso queda como hoy.
- **Esfuerzo:** medio. Toca `SqlPOGeneral`, `Filtros` y `PaginarArray` (`JMetodos`), que usan también las gemelas de exportación.

### R2. Facturación por fila: cargar al abrir, no al pintar (`/rpo`, `/rexpo`, `/rhistorico`)

Cada fila incrusta un componente Livewire (`po.facturacion`, `ex-po.ex-po-facturacion`) que, solo por pintarse, hace:
- **en importación:** 5 consultas (la OC, facturas, conceptos, gastos e incoterms);
- **en exportación:** 4 consultas, más una a Websimex.

Con 20 filas son ~100 consultas, y casi siempre para un diálogo que nadie abre. Los conceptos son los mismos para todas las filas y se consultan 20 veces.

**Propuesta:**
- El componente pinta solo el botón y carga sus datos al abrir el diálogo.
- Los conceptos se consultan una vez por petición.
- **Ganancia:** −100 consultas por página, unos 16 s en local.
- **Esfuerzo:** bajo; los componentes ya se convirtieron en L3.

**Cuidado con `ex-po-facturacion`:** en `mount()`, para cada fila, crea en `po_facturacion_ex` las facturas que trae Websimex si aún no existen. Es una escritura al pintar la lista. Pasarla al momento de abrir el diálogo cambia cuándo se sincronizan esas facturas, así que hay que decidir si se mantiene.

**Hecho en importación (24/10/2026):**
- `po.facturacion` ya no consulta nada al pintarse; carga la OC, los incoterms, las facturas, los conceptos y los gastos al abrir el diálogo.
- `/rpo` de Tenaris pasó de **118 a 17 consultas** y de 26,9 s a 10,7 s en local.
- Lo que queda es la consulta principal (R1).
- En exportación (24/10/2026), por decisión del responsable, también la sincronización de facturas desde Websimex pasó al momento de abrir el diálogo. `/rexpo` de Tenaris pasó de **116 a 15 consultas** y de 19,7 s a 3,1 s en local.
- **Cambio de comportamiento:** las facturas de Websimex de una orden de cargue se registran en `po_facturacion_ex` cuando alguien abre su diálogo, no al listar.

### R3. Días festivos: una consulta por reporte, no una por fila (`reporte/globalbrenntagws` y probablemente los demás reportes globales)

Se consulta `dias_festivos` en Websimex **182 veces**, una por fila, con el mismo filtro y distintas fechas: 25,9 s de 31 s.

**Propuesta:** cargar los festivos del rango una sola vez, o cachearlos un día (cambian muy poco).
- **Ganancia:** el reporte pasaría de ~31 s a ~5 s en local.
- **Esfuerzo:** bajo.

### R4. Índice compuesto en `actividad(id_po, id_etapa)`

Casi todo el cálculo de estados es `SELECT COUNT(*) FROM actividad a WHERE PO.id = a.id_po AND a.id_etapa = N`. Hoy hay índices separados en `id_po` e `id_etapa`, y la tabla tiene ~553.000 filas. Un índice compuesto resuelve cada subconsulta solo con el índice. Beneficia a `/rpo`, `/rexpo`, Torre de Control, KPI y reportes.

```sql
ALTER TABLE actividad ADD INDEX idx_actividad_po_etapa (id_po, id_etapa);
```

- **Esfuerzo:** bajo.
- **Riesgo:** es un cambio en la base de producción. En InnoDB se crea en línea, sin bloquear la tabla, pero conviene una ventana de baja carga. Lo aplica el responsable (no hay migraciones).

### R5. Torre de Control: un conteo agrupado en vez de uno por estado (`/rtorrecontrol`)

Cada estado de `definicion_control_tower` ejecuta su propio `sql_nro`, que es la consulta completa de órdenes con un filtro distinto. Son 15 consultas de ~1 s cada una.

**Propuesta:**
- Calcular el estado una vez por orden y contar con `GROUP BY`.
- Mientras tanto, cachear los conteos unos minutos por cliente.
- Además, hay ~9 s fuera de SQL que conviene medir aparte: probablemente las conexiones a `asercolg_estadisticas` y el armado de gráficas.
- **Esfuerzo:** medio. Los SQL viven en la base, por cliente.

**Hecho en parte (24/10/2026):**
- Las ~9 s fuera de SQL eran los POST a Central (`registrarTablaDinamica`), uno por estado, para todos los clientes.
- Ahora se hacen solo para el grupo 27, que es el único que muestra esas tablas, y en paralelo.
- Quedan los conteos por estado, que son SQL configurable por cliente.

### R6. Estado de Livewire demasiado grande (`impo/pedidos`)

`Importaciones\OrdenesCompra` guarda en una propiedad pública `etapasEh`, la lista de etapas y hechos de todos los DO. Con Tenaris, el HTML llega a 5,7 MB. Como es pública, viaja en la página y **en cada petición de Livewire**: cada filtro y cada clic sube y baja esos megas.

**Propuesta:** no guardarla en el estado del componente. Se calcula en `render()` o con una propiedad computada, y solo se mantienen públicos los filtros.
- **Ganancia:** páginas y clics varias veces más livianos.
- **Esfuerzo:** bajo a medio, por componente.
- **Mismo problema en otros componentes:** el hallazgo H9 (propiedades públicas con datos del tenant) apunta a lo mismo.

**Hecho en Pedidos en Curso (24/10/2026):** `etapasEh` pasa a variable local, la descarga deja de subir la lista y las acciones de fila dejan de ser un componente por fila. Tenaris: estado de 5,7 MB a 0,23 MB; la lista se pagina (MigracionLayout.MD, L3 lote 6).

**Hecho en Productos creados en Websimex (24/10/2026):** resultados fuera del estado y tabla paginada en la base. Tenaris: de ~25 s a 2,2 s por búsqueda (MigracionLayout.MD, L3 lote 5).

### R7. Consultas repetidas sin necesidad

`dashboard/impo` consulta 6 veces las compañías del grupo. Hay más casos del mismo patrón: cada componente vuelve a calcular el contexto del tenant.

**Propuesta:** memorizar por petición (un `ContextoTenant` compartido, que ya existe para el Dashboard V2).
- **Esfuerzo:** bajo.

### R8. Servidor y despliegue

Se aplica una vez en el corte.

- **OPcache activo:** no está en las extensiones de PHP 8.3 del servidor nuevo. Sin él, PHP recompila todo en cada petición (2 a 3 veces más lento en tiempo de PHP).
- **Cachés de Laravel** en cada despliegue: `php artisan config:cache route:cache view:cache event:cache`. Hoy `routes/web.php` tiene 634 rutas; cachearlas evita leerlo en cada petición.
- **`APP_DEBUG=false`** y sin Debugbar en producción. Debugbar recoge cada consulta y vista, y añade su propio peso a cada página.
- **`QUEUE_CONNECTION=sync`:** los correos y los reportes corren dentro de la petición. Pasarlos a una cola con `database` y un `queue:work` en el cron quita esos segundos de la pantalla del usuario. Es un cambio de comportamiento, así que se decide aparte.

### R9. Librerías en el navegador

- Las pantallas heredadas cargan juntas Highcharts (5 módulos), select2, SweetAlert 1 y 2, toastr y jQuery, las usen o no. Al convertirlas en L3, cada una carga solo lo que usa.
- El Tailwind por CDN, que compila las clases en el navegador al abrir cada pantalla, ya se retiró de las 59 pantallas de L2a. Quedan las que siguen en iframe.

## 3. Orden sugerido

| # | Propuesta | Esfuerzo | Riesgo | Quién |
|---|---|---|---|---|
| 1 | R8 (OPcache, cachés, sin debug) | Bajo | Bajo | Corte (servidor) |
| 2 | R4 (índice en `actividad`) | Bajo | Bajo, DDL en producción | Responsable (SQL) |
| 3 | R2 (facturación al abrir) | Bajo | Bajo | Código, en L3 |
| 4 | R3 (festivos una vez) | Bajo | Bajo | Código |
| 5 | R1 (paginar en la base) | Medio | Medio: lógica compartida por muchas pantallas | Código, con prueba de humo y comparación de resultados |
| 6 | R6 y R7 | Bajo-medio | Bajo | Código, por componente |
| 7 | R5 (Torre de Control agrupada) | Medio | Medio: SQL configurable por cliente | Código + configuración |

**Probado y descartado como mejora principal:** reescribir las subconsultas `(SELECT COUNT(*) …) > 0` como `EXISTS`. Da exactamente los mismos resultados (las 6.747 filas idénticas), porque las condiciones solo usan `>` y `=` contra 0, pero gana solo un 13 % (7,9 s → 6,9 s). Sirve como complemento de R1, no en su lugar.
