feat: Update docker-compose configuration to use environment variables for sensitive data, enhance testing documentation with new LaTeX files for capacity planning, and refine README for clarity on compilation and testing processes.
This commit is contained in:
+29
-19
@@ -1,26 +1,36 @@
|
||||
# Plan / Informe de pruebas IPSExtractor
|
||||
# Plan / Informe / Capacidad — IPSExtractor
|
||||
|
||||
## Entregable principal
|
||||
Tres documentos LaTeX **para compartir**:
|
||||
|
||||
**Informe de Pruebas (resultados):** [`InformePruebas_IPSExtractor.pdf`](InformePruebas_IPSExtractor.pdf)
|
||||
Fuentes: [`informe/`](informe/)
|
||||
| PDF | Fuentes | Contenido |
|
||||
|-----|---------|-----------|
|
||||
| [`PlanPruebas_IPSExtractor.pdf`](PlanPruebas_IPSExtractor.pdf) | [`main.tex`](main.tex) | Estrategia, arquitectura, catálogo UT/IT/E2E/ST |
|
||||
| [`InformePruebas_IPSExtractor.pdf`](InformePruebas_IPSExtractor.pdf) | [`informe/`](informe/) | Resultados, métricas, interpretación |
|
||||
| [`Capacidad100k_IPSExtractor.pdf`](Capacidad100k_IPSExtractor.pdf) | [`capacidad_100k/`](capacidad_100k/) | Modelo 100k/mes, coste LLM, veredicto |
|
||||
|
||||
Incluye métricas, gráficas, scorecard y análisis de viabilidad de **100.000 facturas/mes**.
|
||||
|
||||
## Plan histórico (referencia)
|
||||
|
||||
El documento de plan de pruebas sigue en esta carpeta (`main.tex` → `PlanPruebas_IPSExtractor.pdf`) como referencia de metodología/IDs. El foco de QA es el **informe**.
|
||||
|
||||
## Campañas
|
||||
|
||||
- [`informes/2026-07-13-shared8g.md`](informes/2026-07-13-shared8g.md)
|
||||
- Scripts k6: [`scripts/`](scripts/)
|
||||
|
||||
## Compilar informe
|
||||
## Compilar los tres
|
||||
|
||||
```powershell
|
||||
cd docs\plan_pruebas\informe
|
||||
pdflatex -interaction=nonstopmode main.tex
|
||||
pdflatex -interaction=nonstopmode main.tex
|
||||
# Plan
|
||||
cd docs\plan_pruebas
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
Copy-Item main.pdf PlanPruebas_IPSExtractor.pdf -Force
|
||||
|
||||
# Informe
|
||||
cd informe
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
Copy-Item main.pdf ..\InformePruebas_IPSExtractor.pdf -Force
|
||||
|
||||
# Capacidad 100k
|
||||
cd ..\capacidad_100k
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
Copy-Item main.pdf ..\Capacidad100k_IPSExtractor.pdf -Force
|
||||
```
|
||||
|
||||
## Campañas y scripts
|
||||
|
||||
- [`informes/2026-07-13-shared8g.md`](informes/2026-07-13-shared8g.md)
|
||||
- Scripts: [`scripts/`](scripts/)
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Capacidad 100 000 facturas/mes
|
||||
|
||||
Documento LaTeX compartible: modelo por etapa, evidencias (bot/LLM/OCR), coste LLM y veredicto.
|
||||
|
||||
## Compilar
|
||||
|
||||
```powershell
|
||||
cd docs\plan_pruebas\capacidad_100k
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
pdflatex -interaction=nonstopmode -file-line-error main.tex
|
||||
Copy-Item main.pdf ..\Capacidad100k_IPSExtractor.pdf -Force
|
||||
```
|
||||
|
||||
Salida: [`../Capacidad100k_IPSExtractor.pdf`](../Capacidad100k_IPSExtractor.pdf)
|
||||
|
||||
Complementa el [Informe de Pruebas](../informe/) y el [Plan](../).
|
||||
@@ -0,0 +1,78 @@
|
||||
\relax
|
||||
\providecommand \babel@aux [2]{\global \let \babel@toc \@gobbletwo }
|
||||
\@nameuse{bbl@beforestart}
|
||||
\catcode `"\active
|
||||
\catcode `<\active
|
||||
\catcode `>\active
|
||||
\@nameuse{es@quoting}
|
||||
\providecommand\hyper@newdestlabel[2]{}
|
||||
\providecommand\HyField@AuxAddToFields[1]{}
|
||||
\providecommand\HyField@AuxAddToCoFields[2]{}
|
||||
\babel@aux{spanish}{}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {1}Objetivo: 100\,000 facturas al mes}{2}{chapter.1}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:objetivo}{{1}{2}{Objetivo: 100\,000 facturas al mes}{chapter.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.1}Qu\'e pregunta responde este documento}{2}{section.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.2}Traducci\'on a ritmo (facturas/hora)}{2}{section.1.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Ritmo m\'inimo para cumplir 100k/mes.}}{2}{table.1.1}\protected@file@percent }
|
||||
\newlabel{tab:objetivos-h}{{1.1}{2}{Ritmo m\'inimo para cumplir 100k/mes}{table.1.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.3}C\'omo usar este documento}{2}{section.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura del pipeline}{4}{chapter.2}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:arq-cap}{{2}{4}{Arquitectura del pipeline}{chapter.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.1}Patrones de dise\~no relevantes}{4}{section.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.2}Flujo de negocio (punta a punta)}{4}{section.2.2}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{5}{figure.2.1}\protected@file@percent }
|
||||
\newlabel{fig:pipeline-negocio}{{2.1}{5}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.3}Vista de componentes}{5}{section.2.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} hace OCR y \textbf {devuelve JSON} (no escribe en PG); el Desktop/extraction persiste esos resultados con \texttt {POST /count-jobs} en Postgres.}}{5}{figure.2.2}\protected@file@percent }
|
||||
\newlabel{fig:arquitectura}{{2.2}{5}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} hace OCR y \textbf {devuelve JSON} (no escribe en PG); el Desktop/extraction persiste esos resultados con \texttt {POST /count-jobs} en Postgres}{figure.2.2}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {3}Modelo de capacidad por etapa}{6}{chapter.3}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:modelo}{{3}{6}{Modelo de capacidad por etapa}{chapter.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.1}C\'omo leer los n\'umeros}{6}{section.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.2}Tabla de capacidad (datos julio 2026)}{6}{section.3.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Ritmo por etapa. Pipeline $\approx \mathop {\mathgroup \symoperators m\acute {\dotlessi }n}$ de etapas (cuello OCR). $^{\mathrm {P}}$Conservador $\sim $100 $<$ 139; techo observado OCR $\sim $325 puede cubrir 139 si el host no degrada m\'as.}}{6}{table.3.1}\protected@file@percent }
|
||||
\newlabel{tab:capacidad}{{3.1}{6}{Ritmo por etapa. Pipeline $\approx \min $ de etapas (cuello OCR). $^{\mathrm {P}}$Conservador $\sim $100 $<$ 139; techo observado OCR $\sim $325 puede cubrir 139 si el host no degrada m\'as}{table.3.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.3}C\'omo se obtiene el ritmo del pipeline}{7}{section.3.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.4}Gr\'afico 1 --- etapas vs objetivos}{8}{section.3.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Azul = host compartido (medido/estimado). Verde = host dedicado (estimado). L\'ineas = m\'inimos para 100k/mes.}}{8}{figure.3.1}\protected@file@percent }
|
||||
\newlabel{fig:capacidad}{{3.1}{8}{Azul = host compartido (medido/estimado). Verde = host dedicado (estimado). L\'ineas = m\'inimos para 100k/mes}{figure.3.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.5}Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?}{8}{section.3.5}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3.2}{\ignorespaces Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k.}}{9}{figure.3.2}\protected@file@percent }
|
||||
\newlabel{fig:pipeline-metas}{{3.2}{9}{Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k}{figure.3.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.6}Qu\'e concluir aqu\'i}{9}{section.3.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {4}Evidencias que alimentan el modelo}{10}{chapter.4}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:evidencias}{{4}{10}{Evidencias que alimentan el modelo}{chapter.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.1}Resumen}{10}{section.4.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.1}{\ignorespaces Medido vs estimado (campa\~na julio 2026).}}{10}{table.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.2}Detalle por prueba}{10}{section.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.3}A\'un no medido a escala}{11}{section.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {5}Coste LLM (100k que pasan conteo)}{12}{chapter.5}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:coste}{{5}{12}{Coste LLM (100k que pasan conteo)}{chapter.5}{}}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD. No incluye agent Selenium, orquestaci\'on \texttt {bot-descarga}, OCR, infra ni reintentos.}}{12}{table.5.1}\protected@file@percent }
|
||||
\newlabel{tab:coste-llm-100k}{{5.1}{12}{Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD. No incluye agent Selenium, orquestaci\'on \texttt {bot-descarga}, OCR, infra ni reintentos}{table.5.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {6}Veredicto}{13}{chapter.6}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:veredicto}{{6}{13}{Veredicto}{chapter.6}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.1}La respuesta corta}{13}{section.6.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {6.1}{\ignorespaces Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570.}}{13}{figure.6.1}\protected@file@percent }
|
||||
\newlabel{fig:veredicto-brecha}{{6.1}{13}{Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570}{figure.6.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.2}Si hoy no est\'a garantizado: qu\'e hace falta}{14}{section.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.3}Por meta operativa}{14}{section.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {7}Requisitos sugeridos y pr\'oximos pasos}{15}{chapter.7}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:requisitos}{{7}{15}{Requisitos sugeridos y pr\'oximos pasos}{chapter.7}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.1}Para lograr 100k/mes (lista simple)}{15}{section.7.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.2}Pr\'oximas mediciones}{15}{section.7.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.3}Documentos relacionados}{15}{section.7.3}\protected@file@percent }
|
||||
\gdef \@abspage@last{16}
|
||||
@@ -0,0 +1,28 @@
|
||||
\BOOKMARK [0][-]{chapter.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000\040\000a\000l\000\040\000m\000e\000s}{}% 1
|
||||
\BOOKMARK [1][-]{section.1.1}{\376\377\000Q\000u\000\351\000\040\000p\000r\000e\000g\000u\000n\000t\000a\000\040\000r\000e\000s\000p\000o\000n\000d\000e\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.1}% 2
|
||||
\BOOKMARK [1][-]{section.1.2}{\376\377\000T\000r\000a\000d\000u\000c\000c\000i\000\363\000n\000\040\000a\000\040\000r\000i\000t\000m\000o\000\040\000\050\000f\000a\000c\000t\000u\000r\000a\000s\000/\000h\000o\000r\000a\000\051}{chapter.1}% 3
|
||||
\BOOKMARK [1][-]{section.1.3}{\376\377\000C\000\363\000m\000o\000\040\000u\000s\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.1}% 4
|
||||
\BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000d\000e\000l\000\040\000p\000i\000p\000e\000l\000i\000n\000e}{}% 5
|
||||
\BOOKMARK [1][-]{section.2.1}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.2}% 6
|
||||
\BOOKMARK [1][-]{section.2.2}{\376\377\000F\000l\000u\000j\000o\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o\000\040\000\050\000p\000u\000n\000t\000a\000\040\000a\000\040\000p\000u\000n\000t\000a\000\051}{chapter.2}% 7
|
||||
\BOOKMARK [1][-]{section.2.3}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 8
|
||||
\BOOKMARK [0][-]{chapter.3}{\376\377\000M\000o\000d\000e\000l\000o\000\040\000d\000e\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{}% 9
|
||||
\BOOKMARK [1][-]{section.3.1}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000l\000o\000s\000\040\000n\000\372\000m\000e\000r\000o\000s}{chapter.3}% 10
|
||||
\BOOKMARK [1][-]{section.3.2}{\376\377\000T\000a\000b\000l\000a\000\040\000d\000e\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d\000\040\000\050\000d\000a\000t\000o\000s\000\040\000j\000u\000l\000i\000o\000\040\0002\0000\0002\0006\000\051}{chapter.3}% 11
|
||||
\BOOKMARK [1][-]{section.3.3}{\376\377\000C\000\363\000m\000o\000\040\000s\000e\000\040\000o\000b\000t\000i\000e\000n\000e\000\040\000e\000l\000\040\000r\000i\000t\000m\000o\000\040\000d\000e\000l\000\040\000p\000i\000p\000e\000l\000i\000n\000e}{chapter.3}% 12
|
||||
\BOOKMARK [1][-]{section.3.4}{\376\377\000G\000r\000\341\000f\000i\000c\000o\000\040\0001\000\040\040\024\000\040\000e\000t\000a\000p\000a\000s\000\040\000v\000s\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000s}{chapter.3}% 13
|
||||
\BOOKMARK [1][-]{section.3.5}{\376\377\000G\000r\000\341\000f\000i\000c\000o\000\040\0002\000\040\040\024\000\040\000\277\000l\000l\000e\000g\000a\000m\000o\000s\000\040\000a\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000?}{chapter.3}% 14
|
||||
\BOOKMARK [1][-]{section.3.6}{\376\377\000Q\000u\000\351\000\040\000c\000o\000n\000c\000l\000u\000i\000r\000\040\000a\000q\000u\000\355}{chapter.3}% 15
|
||||
\BOOKMARK [0][-]{chapter.4}{\376\377\000E\000v\000i\000d\000e\000n\000c\000i\000a\000s\000\040\000q\000u\000e\000\040\000a\000l\000i\000m\000e\000n\000t\000a\000n\000\040\000e\000l\000\040\000m\000o\000d\000e\000l\000o}{}% 16
|
||||
\BOOKMARK [1][-]{section.4.1}{\376\377\000R\000e\000s\000u\000m\000e\000n}{chapter.4}% 17
|
||||
\BOOKMARK [1][-]{section.4.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000p\000o\000r\000\040\000p\000r\000u\000e\000b\000a}{chapter.4}% 18
|
||||
\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000\372\000n\000\040\000n\000o\000\040\000m\000e\000d\000i\000d\000o\000\040\000a\000\040\000e\000s\000c\000a\000l\000a}{chapter.4}% 19
|
||||
\BOOKMARK [0][-]{chapter.5}{\376\377\000C\000o\000s\000t\000e\000\040\000L\000L\000M\000\040\000\050\0001\0000\0000\000k\000\040\000q\000u\000e\000\040\000p\000a\000s\000a\000n\000\040\000c\000o\000n\000t\000e\000o\000\051}{}% 20
|
||||
\BOOKMARK [0][-]{chapter.6}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o}{}% 21
|
||||
\BOOKMARK [1][-]{section.6.1}{\376\377\000L\000a\000\040\000r\000e\000s\000p\000u\000e\000s\000t\000a\000\040\000c\000o\000r\000t\000a}{chapter.6}% 22
|
||||
\BOOKMARK [1][-]{section.6.2}{\376\377\000S\000i\000\040\000h\000o\000y\000\040\000n\000o\000\040\000e\000s\000t\000\341\000\040\000g\000a\000r\000a\000n\000t\000i\000z\000a\000d\000o\000:\000\040\000q\000u\000\351\000\040\000h\000a\000c\000e\000\040\000f\000a\000l\000t\000a}{chapter.6}% 23
|
||||
\BOOKMARK [1][-]{section.6.3}{\376\377\000P\000o\000r\000\040\000m\000e\000t\000a\000\040\000o\000p\000e\000r\000a\000t\000i\000v\000a}{chapter.6}% 24
|
||||
\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000o\000s\000\040\000y\000\040\000p\000r\000\363\000x\000i\000m\000o\000s\000\040\000p\000a\000s\000o\000s}{}% 25
|
||||
\BOOKMARK [1][-]{section.7.1}{\376\377\000P\000a\000r\000a\000\040\000l\000o\000g\000r\000a\000r\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000\040\000\050\000l\000i\000s\000t\000a\000\040\000s\000i\000m\000p\000l\000e\000\051}{chapter.7}% 26
|
||||
\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000r\000\363\000x\000i\000m\000a\000s\000\040\000m\000e\000d\000i\000c\000i\000o\000n\000e\000s}{chapter.7}% 27
|
||||
\BOOKMARK [1][-]{section.7.3}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000r\000e\000l\000a\000c\000i\000o\000n\000a\000d\000o\000s}{chapter.7}% 28
|
||||
@@ -0,0 +1,20 @@
|
||||
\documentclass[11pt,a4paper,oneside]{report}
|
||||
|
||||
\input{preamble}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\makecover
|
||||
|
||||
\tableofcontents
|
||||
\clearpage
|
||||
|
||||
\input{sections/01_objetivo}
|
||||
\input{sections/02_arquitectura_pipeline}
|
||||
\input{sections/03_modelo_etapas}
|
||||
\input{sections/04_evidencias}
|
||||
\input{sections/05_coste_llm}
|
||||
\input{sections/06_veredicto}
|
||||
\input{sections/07_requisitos}
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,29 @@
|
||||
\babel@toc {spanish}{}\relax
|
||||
\contentsline {chapter}{\numberline {1}Objetivo: 100\,000 facturas al mes}{2}{chapter.1}%
|
||||
\contentsline {section}{\numberline {1.1}Qu\'e pregunta responde este documento}{2}{section.1.1}%
|
||||
\contentsline {section}{\numberline {1.2}Traducci\'on a ritmo (facturas/hora)}{2}{section.1.2}%
|
||||
\contentsline {section}{\numberline {1.3}C\'omo usar este documento}{2}{section.1.3}%
|
||||
\contentsline {chapter}{\numberline {2}Arquitectura del pipeline}{4}{chapter.2}%
|
||||
\contentsline {section}{\numberline {2.1}Patrones de dise\~no relevantes}{4}{section.2.1}%
|
||||
\contentsline {section}{\numberline {2.2}Flujo de negocio (punta a punta)}{4}{section.2.2}%
|
||||
\contentsline {section}{\numberline {2.3}Vista de componentes}{5}{section.2.3}%
|
||||
\contentsline {chapter}{\numberline {3}Modelo de capacidad por etapa}{6}{chapter.3}%
|
||||
\contentsline {section}{\numberline {3.1}C\'omo leer los n\'umeros}{6}{section.3.1}%
|
||||
\contentsline {section}{\numberline {3.2}Tabla de capacidad (datos julio 2026)}{6}{section.3.2}%
|
||||
\contentsline {section}{\numberline {3.3}C\'omo se obtiene el ritmo del pipeline}{7}{section.3.3}%
|
||||
\contentsline {section}{\numberline {3.4}Gr\'afico 1 --- etapas vs objetivos}{8}{section.3.4}%
|
||||
\contentsline {section}{\numberline {3.5}Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?}{8}{section.3.5}%
|
||||
\contentsline {section}{\numberline {3.6}Qu\'e concluir aqu\'i}{9}{section.3.6}%
|
||||
\contentsline {chapter}{\numberline {4}Evidencias que alimentan el modelo}{10}{chapter.4}%
|
||||
\contentsline {section}{\numberline {4.1}Resumen}{10}{section.4.1}%
|
||||
\contentsline {section}{\numberline {4.2}Detalle por prueba}{10}{section.4.2}%
|
||||
\contentsline {section}{\numberline {4.3}A\'un no medido a escala}{11}{section.4.3}%
|
||||
\contentsline {chapter}{\numberline {5}Coste LLM (100k que pasan conteo)}{12}{chapter.5}%
|
||||
\contentsline {chapter}{\numberline {6}Veredicto}{13}{chapter.6}%
|
||||
\contentsline {section}{\numberline {6.1}La respuesta corta}{13}{section.6.1}%
|
||||
\contentsline {section}{\numberline {6.2}Si hoy no est\'a garantizado: qu\'e hace falta}{14}{section.6.2}%
|
||||
\contentsline {section}{\numberline {6.3}Por meta operativa}{14}{section.6.3}%
|
||||
\contentsline {chapter}{\numberline {7}Requisitos sugeridos y pr\'oximos pasos}{15}{chapter.7}%
|
||||
\contentsline {section}{\numberline {7.1}Para lograr 100k/mes (lista simple)}{15}{section.7.1}%
|
||||
\contentsline {section}{\numberline {7.2}Pr\'oximas mediciones}{15}{section.7.2}%
|
||||
\contentsline {section}{\numberline {7.3}Documentos relacionados}{15}{section.7.3}%
|
||||
@@ -0,0 +1,106 @@
|
||||
\usepackage[T1]{fontenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage[spanish,es-tabla]{babel}
|
||||
\usepackage{lmodern}
|
||||
\usepackage{microtype}
|
||||
\usepackage[
|
||||
a4paper,
|
||||
margin=2.4cm,
|
||||
headheight=14pt
|
||||
]{geometry}
|
||||
\usepackage{setspace}
|
||||
\onehalfspacing
|
||||
|
||||
\usepackage{fancyhdr}
|
||||
\pagestyle{fancy}
|
||||
\fancyhf{}
|
||||
\fancyhead[L]{\small IPSExtractor --- Capacidad 100k/mes}
|
||||
\fancyhead[R]{\small\thepage}
|
||||
\fancyfoot[C]{\footnotesize Documento para compartir}
|
||||
\renewcommand{\headrulewidth}{0.4pt}
|
||||
|
||||
\usepackage{titlesec}
|
||||
\titleformat{\chapter}[display]
|
||||
{\normalfont\huge\bfseries}{\chaptertitlename\ \thechapter}{14pt}{\Huge}
|
||||
\titleformat{\section}{\normalfont\Large\bfseries}{\thesection}{1em}{}
|
||||
|
||||
\usepackage{booktabs}
|
||||
\usepackage{longtable}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{multirow}
|
||||
\usepackage{array}
|
||||
\usepackage{enumitem}
|
||||
\setlist[itemize]{nosep,leftmargin=1.4em}
|
||||
\setlist[enumerate]{nosep,leftmargin=1.4em}
|
||||
|
||||
\usepackage{siunitx}
|
||||
\usepackage{xcolor}
|
||||
\definecolor{ipsblue}{HTML}{1B4F72}
|
||||
\definecolor{ipsgrey}{HTML}{566573}
|
||||
\definecolor{codebg}{HTML}{F4F6F7}
|
||||
\definecolor{passgreen}{HTML}{1E8449}
|
||||
\definecolor{failred}{HTML}{C0392B}
|
||||
\definecolor{warnorange}{HTML}{B9770E}
|
||||
|
||||
\usepackage{listings}
|
||||
\lstset{
|
||||
basicstyle=\ttfamily\footnotesize,
|
||||
backgroundcolor=\color{codebg},
|
||||
frame=single,
|
||||
framerule=0.3pt,
|
||||
breaklines=true,
|
||||
columns=fullflexible,
|
||||
showstringspaces=false
|
||||
}
|
||||
|
||||
\usepackage{pgfplots}
|
||||
\pgfplotsset{compat=1.18}
|
||||
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric}
|
||||
|
||||
\usepackage{graphicx}
|
||||
\usepackage{float}
|
||||
\usepackage{hyperref}
|
||||
\hypersetup{
|
||||
colorlinks=true,
|
||||
linkcolor=ipsblue,
|
||||
urlcolor=ipsblue,
|
||||
pdftitle={Capacidad 100000 facturas/mes IPSExtractor},
|
||||
pdfauthor={IPSExtractor QA}
|
||||
}
|
||||
|
||||
\newcommand{\docdate}{17 de julio de 2026}
|
||||
\newcommand{\pass}{\textcolor{passgreen}{\textbf{PASS}}}
|
||||
\newcommand{\fail}{\textcolor{failred}{\textbf{FAIL}}}
|
||||
\newcommand{\parcial}{\textcolor{warnorange}{\textbf{PARCIAL}}}
|
||||
|
||||
\newcommand{\callout}[2]{%
|
||||
\begin{center}
|
||||
\fbox{\parbox{0.92\textwidth}{\centering #1\\[0.3em]\normalsize #2}}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\newcommand{\makecover}{%
|
||||
\begin{titlepage}
|
||||
\centering
|
||||
\vspace*{1.2cm}
|
||||
{\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[0.35cm]
|
||||
{\Huge\bfseries IPSExtractor}\\[0.25cm]
|
||||
{\Large\color{ipsgrey} Extracci\'on y an\'alisis de facturas m\'edicas}\\[0.35cm]
|
||||
{\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[1.5cm]
|
||||
{\LARGE\bfseries Capacidad: 100\,000 facturas/mes}\\[0.25cm]
|
||||
{\large Modelo por etapa $\cdot$ Evidencias $\cdot$ Coste LLM}\\[1.8cm]
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\textbf{Fecha} & \docdate \\
|
||||
\textbf{Evidencias} & Campa\~na 13--17 jul 2026 (k6, LLM, ARQ, OCR) \\
|
||||
\textbf{Host de referencia} & ubuntu --- ASUS X441UAK, 8\,GiB (shared8g) \\
|
||||
\textbf{Audiencia} & Documento para compartir \\
|
||||
\end{tabular}
|
||||
\vfill
|
||||
{\small Complementa el \emph{Informe de Pruebas} y el \emph{Plan de pruebas}.}\\[0.15cm]
|
||||
{\small\textbf{Veredicto breve:} host actual \textcolor{warnorange}{no garantizado} (cuello OCR);
|
||||
dedicado 24$\times$7 \textcolor{passgreen}{S\'I condicional}.}\\[0.25cm]
|
||||
{\color{ipsblue}\rule{0.45\textwidth}{0.6pt}}
|
||||
\end{titlepage}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
\chapter{Objetivo: 100\,000 facturas al mes}
|
||||
\label{ch:objetivo}
|
||||
|
||||
\section{Qu\'e pregunta responde este documento}
|
||||
\textquestiondown Puede IPSExtractor procesar del orden de \textbf{100\,000 facturas m\'edicas al mes}?
|
||||
|
||||
\callout{\textbf{Anticipo del veredicto (detalle en cap.~\ref{ch:veredicto})}}{%
|
||||
\textbf{En el host compartido actual: no garantizado} (pipeline $\approx$ OCR $\sim$100--325/h; piso $\sim$100 $<$ 139).\\
|
||||
\textbf{Con host dedicado y OCR dimensionado: s\'i plausible para 24$\times$7;} jornada 8\,h pide m\'as escalado.
|
||||
}
|
||||
|
||||
La respuesta depende de dos cosas:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Horario:} 24$\times$7 vs solo jornada laboral.
|
||||
\item \textbf{Infra:} host compartido (hoy) vs host dedicado + OCR, agent Selenium y LLM dimensionados (orquestador \texttt{bot-descarga} en Docker).
|
||||
\end{enumerate}
|
||||
|
||||
\section{Traducci\'on a ritmo (facturas/hora)}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lrr@{}}
|
||||
\toprule
|
||||
\textbf{Escenario operativo} & \textbf{Facturas/d\'ia} & \textbf{Facturas/hora} \\
|
||||
\midrule
|
||||
100\,000 / mes, 24$\times$7 & $\approx$3\,333 & $\approx$\textbf{139} \\
|
||||
100\,000 / mes, 8\,h $\times$ 22\,d & $\approx$4\,545 & $\approx$\textbf{570} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Ritmo m\'inimo para cumplir 100k/mes.}
|
||||
\label{tab:objetivos-h}
|
||||
\end{table}
|
||||
|
||||
\callout{\large Lectura r\'apida}{%
|
||||
24$\times$7 $\Rightarrow$ $\approx$\textbf{139 fact/h} punta a punta.\\
|
||||
Jornada 8\,h $\Rightarrow$ $\approx$\textbf{570 fact/h}.\\
|
||||
Gana la etapa m\'as lenta; el resto sobra si esa falla.
|
||||
}
|
||||
|
||||
\section{C\'omo usar este documento}
|
||||
\begin{enumerate}
|
||||
\item Mire el veredicto (cap.~\ref{ch:veredicto}): s\'i / no / qu\'e falta.
|
||||
\item Compare etapas vs 139 y 570 (tabla~\ref{tab:capacidad}).
|
||||
\item Revise evidencias medidas (cap.~\ref{ch:evidencias}) y coste LLM (cap.~\ref{ch:coste}).
|
||||
\item Use el checklist de requisitos (cap.~\ref{ch:requisitos}) como plan de acci\'on.
|
||||
\end{enumerate}
|
||||
@@ -0,0 +1,44 @@
|
||||
\chapter{Arquitectura del pipeline}
|
||||
\label{ch:arq-cap}
|
||||
|
||||
\callout{\textbf{Nombres (importante)}}{%
|
||||
\textbf{bot-descarga} = servicio de orquestaci\'on (API Docker): jobs, claim, heartbeats.\\
|
||||
\textbf{Agent Selenium} = programa en el PC que descarga PDFs (Imaging $\rightarrow$ MinIO).\\
|
||||
Las pruebas \textbf{ST-BOT-*} miden el orquestador. La capacidad $\sim$422 fact/h es del agent.
|
||||
}
|
||||
|
||||
\section{Patrones de dise\~no relevantes}
|
||||
IPSExtractor no es un monolito: el volumen mensualmente viable depende de c\'omo
|
||||
escalan \textbf{por separado} cada bounded context.
|
||||
|
||||
\begin{description}[style=nextline,leftmargin=0.5cm]
|
||||
\item[API Gateway]
|
||||
\'Unica puerta HTTP en el host (\texttt{:8189}). Orquesta, no procesa OCR ni LLM.
|
||||
Escala de control; no es el cuello de 100k.
|
||||
\item[Microservicios por dominio]
|
||||
\texttt{count-api} (OCR), \texttt{extraction}+workers (LLM), \texttt{bot-descarga} (orquestador)
|
||||
+ agent Selenium (PC), \texttt{cleaning} (CSV). Cada uno tiene su propio l\'imite de capacidad.
|
||||
\item[Cola as\'incrona (ARQ/Redis)]
|
||||
La extracci\'on puede encolar PDFs y drenar con $N$ workers. Throughput LLM
|
||||
$\approx N \times (3600/\mathrm{p50})$ si no hay cuota del proveedor.
|
||||
\item[Claim/lease del orquestador (\texttt{bot-descarga})]
|
||||
Varios agents Selenium en PCs distintos reclaman jobs v\'ia la API; la capacidad de descarga
|
||||
crece con $N$ agents, no con un solo proceso Selenium.
|
||||
\end{description}
|
||||
|
||||
\section{Flujo de negocio (punta a punta)}
|
||||
\input{../diagrams/pipeline_negocio}
|
||||
|
||||
Orden t\'ipico:
|
||||
\begin{enumerate}
|
||||
\item \textbf{Descarga:} agent Selenium $\rightarrow$ PDFs en MinIO.
|
||||
\item \textbf{Conteo:} OCR/ONNX valida p\'aginas/palabras (filtro).
|
||||
\item \textbf{Extracci\'on:} LLM rellena campos $\rightarrow$ filas en PostgreSQL.
|
||||
\item \textbf{Limpieza:} reglas/CSV sobre el lote extra\'ido.
|
||||
\end{enumerate}
|
||||
|
||||
El pipeline completo solo es tan r\'apido como la etapa m\'as lenta bajo la misma
|
||||
contenci\'on de CPU/RAM.
|
||||
|
||||
\section{Vista de componentes}
|
||||
\input{../diagrams/arquitectura}
|
||||
@@ -0,0 +1,237 @@
|
||||
\chapter{Modelo de capacidad por etapa}
|
||||
\label{ch:modelo}
|
||||
|
||||
\callout{\textbf{Idea en una frase}}{%
|
||||
Cada etapa tiene un ritmo (facturas/hora).\\
|
||||
El \textbf{pipeline completo} solo va tan r\'apido como la etapa m\'as lenta
|
||||
(hoy: OCR en host compartido).\\
|
||||
El agent Selenium y el LLM pueden ir bien por separado y el sistema entero igual no llega a 100k/mes.
|
||||
}
|
||||
|
||||
\section{C\'omo leer los n\'umeros}
|
||||
\begin{itemize}
|
||||
\item \textbf{Host actual:} servidor compartido de las pruebas (8\,GiB + MLflow y otros).
|
||||
\item \textbf{Host dedicado:} estimaci\'on si IPSExtractor va solo (sin competir por CPU/RAM).
|
||||
\item \textbf{139/h:} m\'inimo para 100k/mes en 24$\times$7.
|
||||
\item \textbf{570/h:} m\'inimo para 100k/mes en jornada 8\,h $\times$ 22\,d.
|
||||
\end{itemize}
|
||||
|
||||
\noindent Sem\'aforo:
|
||||
\textcolor{passgreen}{\textbf{S\'I}} = llega;
|
||||
\textcolor{warnorange}{\textbf{JUSTO}} = cerca / hace falta margen;
|
||||
\textcolor{failred}{\textbf{NO}} = no llega.
|
||||
|
||||
\section{Tabla de capacidad (datos julio 2026)}
|
||||
\begin{itemize}
|
||||
\item LLM ST-EXT-02: p50 7{,}1\,s $\Rightarrow$ $\approx$506 fact/h (1 worker).
|
||||
\item OCR ST-COUNT-01: p50 35{,}8\,s @3 concurrentes $\Rightarrow$ $\approx$100--325 fact/h
|
||||
(en serie era $\sim$2\,s/PDF).
|
||||
\item Agent Selenium: 633 PDFs / 90\,min $\Rightarrow$ $\approx$422 fact/h (1 agent PC).
|
||||
\item Gateway ST-GW-01: p95 41\,ms, 0\,\% fallos --- no limita.
|
||||
\end{itemize}
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{@{}lrrcc@{}}
|
||||
\toprule
|
||||
\textbf{Etapa} & \textbf{Host actual} & \textbf{Dedicado (est.)} & \textbf{\textquestiondown 139/h?} & \textbf{\textquestiondown 570/h?} \\
|
||||
& fact/h & fact/h & & \\
|
||||
\midrule
|
||||
Gateway & $>$10\,000 & $>$20\,000 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{passgreen}{\textbf{S\'I}} \\
|
||||
OCR (conteo) & $\sim$100--325 & $\sim$400 & \textcolor{warnorange}{\textbf{JUSTO}} & \textcolor{failred}{\textbf{NO}} \\
|
||||
LLM (extracci\'on) & $\sim$506 & $\sim$1\,000 (2\,w) & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{warnorange}{\textbf{JUSTO}} \\
|
||||
Descarga (agent) & $\sim$422 & $\sim$500+ & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{failred}{\textbf{NO}} \\
|
||||
Limpieza CSV & $\sim$800 & $\sim$3\,000 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{warnorange}{\textbf{JUSTO}} \\
|
||||
\midrule
|
||||
\textbf{Pipeline entero} & $\mathbf{\sim}$\textbf{100--325} & $\mathbf{\sim}$\textbf{400} & \textcolor{warnorange}{\textbf{JUSTO}}$^{\mathrm{P}}$ & \textcolor{failred}{\textbf{NO}} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Ritmo por etapa. Pipeline $\approx\min$ de etapas (cuello OCR). $^{\mathrm{P}}$Conservador
|
||||
$\sim$100 $<$ 139; techo observado OCR $\sim$325 puede cubrir 139 si el host no degrada m\'as.}
|
||||
\label{tab:capacidad}
|
||||
\end{table}
|
||||
|
||||
\section{C\'omo se obtiene el ritmo del pipeline}
|
||||
\subsection*{Primero: qu\'e \emph{no} es ese n\'umero}
|
||||
No corrimos 100\,000 PDFs de punta a punta midiendo el reloj.
|
||||
El ritmo del ``pipeline entero'' es un \textbf{modelo}: se calcula a partir de las
|
||||
etapas que s\'i medimos (agent, OCR, LLM, limpieza).
|
||||
|
||||
\subsection*{Una factura es secuencial; muchas facturas no}
|
||||
Para \textbf{una} factura el orden es obligatorio:
|
||||
\begin{center}
|
||||
descarga $\rightarrow$ conteo OCR $\rightarrow$ extracci\'on LLM $\rightarrow$ limpieza.
|
||||
\end{center}
|
||||
No puede empezar el LLM de esa factura si a\'un no termin\'o el OCR.
|
||||
|
||||
Pero en el d\'ia a d\'ia no hay una sola factura: hay un \textbf{lote}.
|
||||
Mientras la factura A est\'a en el LLM, la B puede estar en OCR y la C en descarga.
|
||||
Es como una cadena de montaje: cada puesto trabaja en paralelo sobre piezas distintas.
|
||||
|
||||
\subsection*{Analog\'ia del embudo}
|
||||
El ritmo del sistema completo lo marca el \textbf{puesto m\'as lento}
|
||||
(el cuello / embudo), no el m\'as r\'apido.
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}lrl@{}}
|
||||
\toprule
|
||||
\textbf{Puesto (etapa)} & \textbf{Ritmo medido} & \textbf{Lectura} \\
|
||||
\midrule
|
||||
Descarga (agent Selenium) & $\approx$422 fact/h & r\'apido \\
|
||||
Conteo (OCR) & $\approx$100--325 fact/h & \textbf{el m\'as lento} \\
|
||||
Extracci\'on (LLM) & $\approx$506 fact/h & r\'apido \\
|
||||
Limpieza & $\approx$800 fact/h & r\'apido \\
|
||||
\midrule
|
||||
\textbf{Pipeline entero} & $\approx$\textbf{100--325 fact/h} & $=\min$ = OCR \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
Por eso da igual que el agent haga 422/h: si el OCR solo deja pasar 100--325/h,
|
||||
el lote completo no puede ir m\'as r\'apido que eso.
|
||||
|
||||
\subsection*{F\'ormula (r\'egimen normal / cadena de montaje)}
|
||||
\[
|
||||
\text{ritmo del pipeline} \;\approx\; \min(\text{descarga},\,\text{OCR},\,\text{LLM},\,\text{limpieza}).
|
||||
\]
|
||||
Con los n\'umeros de arriba: $\min(422,\,100\text{--}325,\,506,\,800) = \mathbf{100\text{--}325}$.
|
||||
|
||||
\subsection*{Y si proces\'aramos de una en una, sin solapar?}
|
||||
Entonces s\'i se \textbf{suman} los tiempos de cada etapa por factura:
|
||||
\[
|
||||
\text{fact/h} \approx 3600\big/\big(t_{\mathrm{descarga}}+t_{\mathrm{OCR}}+t_{\mathrm{LLM}}+t_{\mathrm{limpieza}}\big).
|
||||
\]
|
||||
Ejemplo con OCR lento ($\sim$36\,s): $\sim$70 fact/h.
|
||||
Con OCR r\'apido en serie ($\sim$2\,s): $\sim$200 fact/h.
|
||||
|
||||
Eso ser\'ia un laboratorio artificial (una factura y la siguiente solo cuando acaba).
|
||||
En producci\'on real usamos el modelo del embudo ($\min$), que es el que importa.
|
||||
|
||||
\subsection*{Qu\'e ponemos en tablas y gr\'aficos}
|
||||
\begin{itemize}
|
||||
\item Host actual: pipeline = $\sim$100--325 (cuello OCR).
|
||||
\item Para el veredicto 24$\times$7 usamos el \textbf{piso} $\sim$100 (conservador; queda bajo 139).
|
||||
\item Host dedicado (est.): $\sim$400 (OCR estimado $\sim$400).
|
||||
\end{itemize}
|
||||
|
||||
\section{Gr\'afico 1 --- etapas vs objetivos}
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=\textwidth,
|
||||
height=6.8cm,
|
||||
ylabel={Facturas / hora},
|
||||
symbolic x coords={OCR,LLM,Agent,Limpieza,Pipeline},
|
||||
xtick=data,
|
||||
xticklabels={OCR,LLM,{Agent Selenium},Limpieza,Pipeline},
|
||||
x tick label style={font=\small},
|
||||
legend style={at={(0.02,0.98)},anchor=north west,font=\small,draw=ipsgrey!40},
|
||||
bar width=11pt,
|
||||
ymin=0, ymax=1100,
|
||||
enlarge x limits=0.12,
|
||||
grid=major,
|
||||
major grid style={ipsgrey!20},
|
||||
]
|
||||
\addplot[fill=ipsblue!75, draw=ipsblue] coordinates {
|
||||
(OCR,200) (LLM,506) (Agent,422) (Limpieza,800) (Pipeline,200)
|
||||
};
|
||||
\addlegendentry{Host actual}
|
||||
\addplot[fill=passgreen!60, draw=passgreen] coordinates {
|
||||
(OCR,400) (LLM,1012) (Agent,500) (Limpieza,1000) (Pipeline,400)
|
||||
};
|
||||
\addlegendentry{Host dedicado (est.)}
|
||||
\draw[failred, ultra thick] (axis cs:OCR,139) -- (axis cs:Pipeline,139)
|
||||
node[pos=0, left, font=\scriptsize\bfseries, text=failred] {139};
|
||||
\draw[warnorange, ultra thick] (axis cs:OCR,570) -- (axis cs:Pipeline,570)
|
||||
node[pos=0, left, font=\scriptsize\bfseries, text=warnorange] {570};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Azul = host compartido (medido/estimado). Verde = host dedicado (estimado).
|
||||
L\'ineas = m\'inimos para 100k/mes.}
|
||||
\label{fig:capacidad}
|
||||
\end{figure}
|
||||
|
||||
\section{Gr\'afico 2 --- \textquestiondown llegamos a 100k/mes?}
|
||||
\subsection*{C\'omo leer este gr\'afico}
|
||||
Solo hay \textbf{dos barras} = cu\'anto puede el pipeline (modelo):
|
||||
\begin{itemize}
|
||||
\item Rojo = host \textbf{actual} (piso $\sim$100 fact/h).
|
||||
\item Verde = host \textbf{dedicado} estimado ($\sim$400 fact/h).
|
||||
\end{itemize}
|
||||
Las \textbf{l\'ineas verticales} no son barras: son las metas que hay que superar:
|
||||
\begin{itemize}
|
||||
\item Azul punteada = 139/h (100k/mes trabajando 24$\times$7).
|
||||
\item Naranja punteada = 570/h (100k/mes en jornada 8\,h).
|
||||
\end{itemize}
|
||||
Si la barra \textbf{pasa} la l\'inea $\Rightarrow$ esa meta es alcanzable. Si se queda corta $\Rightarrow$ no.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
xbar,
|
||||
width=0.78\textwidth,
|
||||
height=4.8cm,
|
||||
xlabel={Facturas / hora que aguanta el pipeline},
|
||||
symbolic y coords={dedicado,actual},
|
||||
ytick=data,
|
||||
yticklabels={\empty,\empty},
|
||||
xmin=0, xmax=650,
|
||||
enlarge y limits=0.45,
|
||||
bar width=22pt,
|
||||
bar shift=0pt,
|
||||
grid=major,
|
||||
major grid style={ipsgrey!25},
|
||||
axis y line*=left,
|
||||
axis x line*=bottom,
|
||||
clip=false,
|
||||
]
|
||||
\addplot[fill=failred!75, draw=failred] coordinates {(100,actual)};
|
||||
\addplot[fill=passgreen!75, draw=passgreen] coordinates {(400,dedicado)};
|
||||
% Etiquetas izquierda
|
||||
\node[anchor=east, font=\small\bfseries, align=right]
|
||||
at (axis cs:-5,actual) {Host actual\\(hoy)};
|
||||
\node[anchor=east, font=\small\bfseries, align=right]
|
||||
at (axis cs:-5,dedicado) {Host dedicado\\(estimado)};
|
||||
% Valores
|
||||
\node[anchor=west, font=\normalsize\bfseries] at (axis cs:110,actual) {$\sim$100 fact/h};
|
||||
\node[anchor=west, font=\normalsize\bfseries] at (axis cs:410,dedicado) {$\sim$400 fact/h};
|
||||
% Metas como l\'ineas (no barras)
|
||||
\draw[ipsblue, ultra thick, dashed]
|
||||
(axis cs:139,actual) -- (axis cs:139,dedicado);
|
||||
\node[font=\scriptsize\bfseries, text=ipsblue, anchor=south, align=center]
|
||||
at (axis cs:139,dedicado) {meta 139\\(24$\times$7)};
|
||||
\draw[warnorange, ultra thick, dashed]
|
||||
(axis cs:570,actual) -- (axis cs:570,dedicado);
|
||||
\node[font=\scriptsize\bfseries, text=warnorange, anchor=south, align=center]
|
||||
at (axis cs:570,dedicado) {meta 570\\(jornada)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Solo capacidad (barras) vs metas (l\'ineas). Modelo por cuello OCR; no es E2E de 100k.}
|
||||
\label{fig:pipeline-metas}
|
||||
\end{figure}
|
||||
|
||||
\begin{center}
|
||||
\renewcommand{\arraystretch}{1.25}
|
||||
\begin{tabular}{@{}lcc@{}}
|
||||
\toprule
|
||||
& \textbf{\textquestiondown Llega a 139/h?} & \textbf{\textquestiondown Llega a 570/h?} \\
|
||||
& (100k en 24$\times$7) & (100k en 8\,h) \\
|
||||
\midrule
|
||||
Host actual $\sim$100 & \textcolor{failred}{\textbf{NO}} (falta margen) & \textcolor{failred}{\textbf{NO}} \\
|
||||
Host dedicado $\sim$400 & \textcolor{passgreen}{\textbf{S\'I}} & \textcolor{failred}{\textbf{NO}} (falta escalar) \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
|
||||
\section{Qu\'e concluir aqu\'i}
|
||||
\begin{enumerate}
|
||||
\item Gateway, agent Selenium y LLM \textbf{no} son el cuello para 139/h (por etapa).
|
||||
\item El pipeline sigue al OCR: $\sim$100--325/h $\Rightarrow$ 139/h \textbf{no garantizado}
|
||||
(el piso conservador $\sim$100 queda bajo).
|
||||
\item Host dedicado ($\sim$400/h): \textbf{s\'i} a 139/h; \textbf{no} a 570/h sin m\'as OCR/agents.
|
||||
\end{enumerate}
|
||||
@@ -0,0 +1,40 @@
|
||||
\chapter{Evidencias que alimentan el modelo}
|
||||
\label{ch:evidencias}
|
||||
|
||||
\section{Resumen}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{@{}llp{5.4cm}@{}}
|
||||
\toprule
|
||||
\textbf{Etapa} & \textbf{Cifra} & \textbf{Origen} \\
|
||||
\midrule
|
||||
Gateway & no es cuello (p95 41\,ms) & ST-GW-01 (13-jul), 0\,\% fallos \\
|
||||
Agent Selenium (1 agent PC) & $\approx$422 fact/h & 633 PDFs / 90\,min \\
|
||||
LLM (1 worker) & $\approx$506 fact/h & ST-EXT-02 (17-jul): p50 7{,}114\,s \\
|
||||
OCR count-api & $\sim$100--325 fact/h & ST-COUNT-01 (17-jul): p50 35{,}8\,s @3 \\
|
||||
Cola ARQ (mock) & $\approx$4\,500 docs/h cola & ST-EXT-01: 30/30 en $\approx$24\,s \\
|
||||
Pipeline host actual & $\sim$100--325 fact/h & $=\min$ etapas; cuello OCR (no E2E) \\
|
||||
Pipeline dedicado (est.) & $\sim$400 fact/h & $\min$ con OCR $\sim$400 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Medido vs estimado (campa\~na julio 2026).}
|
||||
\end{table}
|
||||
|
||||
\section{Detalle por prueba}
|
||||
\begin{description}[style=nextline,leftmargin=0.4cm]
|
||||
\item[ST-EXT-02 (LLM)]
|
||||
gpt-5.4-mini; 23/23 ok; p50 7{,}114\,s $\Rightarrow$ 506 fact/h (1\,w); 0$\times$429.
|
||||
\item[ST-EXT-01 (ARQ mock)]
|
||||
30/30 drenados en 22{,}5\,s. Valida cola async; \textbf{no} es capacidad LLM real.
|
||||
\item[ST-COUNT-01 (OCR)]
|
||||
9/9 ok; p50 35{,}8\,s con 3 concurrentes (vs $\sim$2\,s en serie). Degrada fuerte en shared8g.
|
||||
\item[Agent Selenium]
|
||||
422 fact/h con 1 agent PC: supera 139/h; no alcanza 570/h sin m\'as agents.
|
||||
\item[ST-GW-01 / ST-BOT-01]
|
||||
Gateway OK. ST-BOT-01 mide el orquestador \texttt{bot-descarga} (heartbeats): host contendido malo (p95 2{,}28\,s); host m\'as libre OK (86\,ms).
|
||||
\end{description}
|
||||
|
||||
\section{A\'un no medido a escala}
|
||||
ST-COUNT-01 lab, ST-EXT-01 lab (100--500), E2E punta a punta a cientos/miles de PDFs,
|
||||
ni 100k reales.
|
||||
@@ -0,0 +1,32 @@
|
||||
\chapter{Coste LLM (100k que pasan conteo)}
|
||||
\label{ch:coste}
|
||||
|
||||
Hip\'otesis: el filtro de conteo deja pasar \textbf{100\,000} PDFs a extracci\'on
|
||||
con modelo \textbf{gpt-5.4 mini} a USD\,0{,}0257 por documento.
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lr@{}}
|
||||
\toprule
|
||||
\textbf{Concepto} & \textbf{Valor} \\
|
||||
\midrule
|
||||
Modelo de extracci\'on & gpt-5.4 mini \\
|
||||
Coste unitario por documento & USD\,0{,}0257 \\
|
||||
Volumen (pasan filtro de conteo) & 100\,000 \\
|
||||
\textbf{Coste estimado extracci\'on / mes} & \textbf{USD\,2\,570} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Presupuesto LLM: $100\,000 \times 0{,}0257 = 2\,570$ USD.
|
||||
No incluye agent Selenium, orquestaci\'on \texttt{bot-descarga}, OCR, infra ni reintentos.}
|
||||
\label{tab:coste-llm-100k}
|
||||
\end{table}
|
||||
|
||||
\begin{itemize}
|
||||
\item Si pasan $n < 100\,000$, el coste es lineal: $n \times 0{,}0257$.
|
||||
\item Ejemplo: 60\,000 v\'alidos $\Rightarrow$ $\approx$ USD\,1\,542.
|
||||
\item El unitario 0{,}0257 se us\'o como estimaci\'on en ST-EXT-02; contrastar con tokens reales si se instrumenta \texttt{usage}.
|
||||
\end{itemize}
|
||||
|
||||
\callout{\textbf{Coste de referencia}}{%
|
||||
\textbf{USD\,2\,570}/mes si 100k PDFs llegan a extracci\'on con gpt-5.4 mini.
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
\chapter{Veredicto}
|
||||
\label{ch:veredicto}
|
||||
|
||||
\section{La respuesta corta}
|
||||
\begin{center}
|
||||
\renewcommand{\arraystretch}{1.35}
|
||||
\begin{tabular}{@{}p{4.2cm}p{2.2cm}p{6.5cm}@{}}
|
||||
\toprule
|
||||
\textbf{Escenario} & \textbf{\textquestiondown 100k?} & \textbf{Por qu\'e} \\
|
||||
\midrule
|
||||
Host \textbf{actual} (compartido)
|
||||
& \parcial\ / \fail
|
||||
& Pipeline $\approx$ OCR $\sim$100--325/h.
|
||||
Piso $\sim$100 $<$ 139 (no garantizado). \\
|
||||
Host \textbf{dedicado} + dimensionado, 24$\times$7
|
||||
& \pass\ \emph{si}
|
||||
& Estimado $\sim$400/h $>$ 139/h si OCR/agents/LLM OK. \\
|
||||
Misma infra, jornada 8\,h (570/h)
|
||||
& \fail
|
||||
& 400 $<$ 570: falta m\'as OCR, m\'as agents Selenium y cuota LLM. \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\callout{\large Resumen para decidir}{%
|
||||
\textbf{Hoy (host compartido): 100k/mes 24$\times$7 no est\'a garantizado}
|
||||
(pipeline $\approx$ cuello OCR; piso $\sim$100 $<$ 139).\\
|
||||
\textbf{Con host dedicado + OCR dimensionado: s\'i plausible a 24$\times$7.}\\
|
||||
\textbf{Jornada 8\,h: no} sin escalar m\'as.\\[0.35em]
|
||||
\footnotesize El pipeline \textbf{no} se midi\'o E2E a 100k. Ritmo = $\min$ de etapas medidas
|
||||
(cap.~\ref{ch:modelo}).
|
||||
}
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
xbar,
|
||||
width=0.78\textwidth,
|
||||
height=4.6cm,
|
||||
xlabel={Facturas / hora que aguanta el pipeline},
|
||||
symbolic y coords={dedicado,actual},
|
||||
ytick=data,
|
||||
yticklabels={\empty,\empty},
|
||||
xmin=0, xmax=650,
|
||||
enlarge y limits=0.45,
|
||||
bar width=22pt,
|
||||
bar shift=0pt,
|
||||
grid=major,
|
||||
major grid style={ipsgrey!25},
|
||||
clip=false,
|
||||
]
|
||||
\addplot[fill=failred!75, draw=failred] coordinates {(100,actual)};
|
||||
\addplot[fill=passgreen!75, draw=passgreen] coordinates {(400,dedicado)};
|
||||
\node[anchor=east, font=\small\bfseries, align=right]
|
||||
at (axis cs:-5,actual) {Host actual\\(hoy)};
|
||||
\node[anchor=east, font=\small\bfseries, align=right]
|
||||
at (axis cs:-5,dedicado) {Host dedicado\\(estimado)};
|
||||
\node[anchor=west, font=\normalsize\bfseries] at (axis cs:110,actual) {$\sim$100};
|
||||
\node[anchor=west, font=\normalsize\bfseries] at (axis cs:410,dedicado) {$\sim$400};
|
||||
\draw[ipsblue, ultra thick, dashed]
|
||||
(axis cs:139,actual) -- (axis cs:139,dedicado);
|
||||
\node[font=\scriptsize\bfseries, text=ipsblue, anchor=south, align=center]
|
||||
at (axis cs:139,dedicado) {meta 139};
|
||||
\draw[warnorange, ultra thick, dashed]
|
||||
(axis cs:570,actual) -- (axis cs:570,dedicado);
|
||||
\node[font=\scriptsize\bfseries, text=warnorange, anchor=south, align=center]
|
||||
at (axis cs:570,dedicado) {meta 570};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Barras = capacidad. L\'ineas = metas. Rojo no llega a 139; verde llega a 139 pero no a 570.}
|
||||
\label{fig:veredicto-brecha}
|
||||
\end{figure}
|
||||
|
||||
\section{Si hoy no est\'a garantizado: qu\'e hace falta}
|
||||
\begin{enumerate}
|
||||
\item \textbf{Host dedicado} (sin MLflow/Airflow/n8n); RAM $\geq$16--32\,GiB.
|
||||
\item \textbf{OCR con margen} hasta sostener $\geq$139 fact/h bajo carga real.
|
||||
\item \textbf{LLM async} (\texttt{ASYNC\_QUEUE=true}, 1--2 workers; ya $\approx$506/h medidos).
|
||||
\item \textbf{Agent Selenium:} 1 PC basta para 139/h; $N$ PCs para 570/h.
|
||||
\item \textbf{Presupuesto LLM} $\approx$ USD\,2\,570/mes si pasan 100k a extracci\'on.
|
||||
\item \textbf{Validar E2E} de cientos de PDFs punta a punta (a\'un pendiente).
|
||||
\end{enumerate}
|
||||
|
||||
\section{Por meta operativa}
|
||||
\begin{description}
|
||||
\item[100k/mes en 24$\times$7 ($\geq$139 fact/h)]
|
||||
Plausible en host dedicado. En compartido depende de que el OCR no se quede en el piso $\sim$100.
|
||||
\item[100k/mes en jornada 8\,h ($\geq$570 fact/h)]
|
||||
No con el estimado dedicado ($\sim$400). Escalar OCR, agents Selenium y LLM, y re-medir.
|
||||
\end{description}
|
||||
@@ -0,0 +1,36 @@
|
||||
\chapter{Requisitos sugeridos y pr\'oximos pasos}
|
||||
\label{ch:requisitos}
|
||||
|
||||
\section{Para lograr 100k/mes (lista simple)}
|
||||
Si hoy es \fail\ en el host compartido, esto es lo m\'inimo para apuntar a \pass\ en 24$\times$7:
|
||||
|
||||
\begin{center}
|
||||
\renewcommand{\arraystretch}{1.3}
|
||||
\begin{tabular}{@{}clp{9.2cm}@{}}
|
||||
\toprule
|
||||
\textbf{\#} & \textbf{Estado} & \textbf{Qu\'e} \\
|
||||
\midrule
|
||||
1 & Obligatorio & Host \textbf{dedicado} IPS (sin MLflow/Airflow/n8n); RAM $\geq$16--32\,GiB \\
|
||||
2 & Obligatorio & OCR con margen (CPU/RAM o r\'eplicas) hasta $\geq$139 fact/h bajo carga \\
|
||||
3 & Obligatorio & \texttt{ASYNC\_QUEUE=true} + 1--2 workers LLM \\
|
||||
4 & Obligatorio & Presupuesto LLM $\approx$ USD\,2\,570/mes (si pasan 100k a extracci\'on) \\
|
||||
5 & Seg\'un meta & 1 agent Selenium basta para 139/h; $N$ agents PC para 570/h \\
|
||||
6 & Validaci\'on & Repetir ST-COUNT-01 / ST-EXT-01 lab + E2E de cientos de PDFs \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Pr\'oximas mediciones}
|
||||
\begin{enumerate}
|
||||
\item ST-COUNT-01 perfil lab ($\geq$3\,GiB libres).
|
||||
\item ST-EXT-01 perfil lab (100--500 docs, 2 workers).
|
||||
\item E2E controlado (200--500 PDFs punta a punta).
|
||||
\item Tokens reales del LLM para validar USD\,0{,}0257/doc.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Documentos relacionados}
|
||||
\begin{itemize}
|
||||
\item \textbf{Plan de pruebas} --- estrategia e IDs.
|
||||
\item \textbf{Informe de resultados} --- m\'etricas de la campa\~na.
|
||||
\item Modelo Markdown: \texttt{docs/plan\_pruebas/scripts/estimate\_capacity.md}.
|
||||
\end{itemize}
|
||||
@@ -2,35 +2,47 @@
|
||||
\centering
|
||||
\begin{tikzpicture}[
|
||||
font=\small,
|
||||
box/.style={draw=ipsblue, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm, minimum width=2.4cm},
|
||||
box/.style={draw=ipsblue, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm, minimum width=2.3cm},
|
||||
ext/.style={draw=ipsgrey, dashed, rounded corners=2pt, align=center, inner sep=4pt, minimum height=0.85cm},
|
||||
arr/.style={-{Latex}, thick, ipsblue}
|
||||
arr/.style={-{Latex}, thick, ipsblue},
|
||||
dbarr/.style={-{Latex}, thick, ipsgrey}
|
||||
]
|
||||
\node[box, fill=codebg] (desk) {Desktop\\Electron};
|
||||
\node[box, fill=codebg, right=1.8cm of desk] (gw) {api\_gateway\\:8189};
|
||||
\node[box, below=1.1cm of gw] (ext) {extraction\\:8001};
|
||||
\node[box, left=0.8cm of ext] (clean) {cleaning\\:8002};
|
||||
\node[box, right=0.8cm of ext] (count) {count-api\\:8080};
|
||||
\node[box, below=1.1cm of ext] (bot) {bot-descarga\\:8200};
|
||||
\node[box, below left=1.0cm and 0.2cm of bot] (pg) {PostgreSQL};
|
||||
\node[box, below right=1.0cm and 0.2cm of bot] (redis) {Redis};
|
||||
\node[ext, left=1.6cm of desk] (agent) {Agent PC\\Selenium};
|
||||
\node[ext, below=2.6cm of agent] (minio) {MinIO/S3};
|
||||
\node[ext, below=0.9cm of minio] (img) {Imaging};
|
||||
\node[box, fill=codebg, right=1.7cm of desk] (gw) {api\_gateway\\:8189};
|
||||
\node[box, below=1.05cm of gw] (ext) {extraction\\(+ workers)};
|
||||
\node[box, left=0.7cm of ext] (clean) {cleaning\\:8002};
|
||||
\node[box, right=0.7cm of ext] (count) {count-api\\:8080};
|
||||
\node[box, below=1.05cm of ext] (bot) {bot-descarga\\orquestador};
|
||||
\node[box, below left=1.15cm and 1.4cm of bot] (pg) {PostgreSQL};
|
||||
\node[box, below right=1.15cm and 1.4cm of bot] (redis) {Redis};
|
||||
\node[ext, left=1.5cm of desk] (agent) {Agent Selenium\\(PC externo)};
|
||||
\node[ext, below=2.5cm of agent] (minio) {MinIO/S3};
|
||||
\node[ext, below=0.85cm of minio] (img) {Imaging};
|
||||
|
||||
% HTTP via gateway
|
||||
\draw[arr] (desk) -- (gw);
|
||||
\draw[arr] (gw) -- (ext);
|
||||
\draw[arr] (gw) -- (clean);
|
||||
\draw[arr] (gw) -- (count);
|
||||
\draw[arr] (gw) -- (bot);
|
||||
\draw[arr] (bot) -- (pg);
|
||||
\draw[arr] (ext) -- (pg);
|
||||
\draw[arr] (ext) -- (redis);
|
||||
\draw[arr] (agent) |- (gw);
|
||||
|
||||
% PostgreSQL: solo servicios de dominio (NO gateway, NO count-api)
|
||||
\draw[dbarr] (ext) -- (pg);
|
||||
\draw[dbarr] (clean) |- (pg);
|
||||
\draw[dbarr] (bot) -- (pg);
|
||||
|
||||
% Redis: cola ARQ (extraction/workers)
|
||||
\draw[dbarr] (ext) -- (redis);
|
||||
|
||||
% MinIO / Imaging
|
||||
\draw[arr] (agent) -- (minio);
|
||||
\draw[arr] (bot) -| (minio);
|
||||
\draw[arr] (agent) -- (img);
|
||||
\end{tikzpicture}
|
||||
\caption{Arquitectura IPSExtractor bajo prueba.}
|
||||
\caption{Arquitectura bajo prueba. El \textbf{gateway no habla con Postgres}: solo hace proxy HTTP.
|
||||
Usan la BD compartida: \texttt{extraction} (+ workers), \texttt{cleaning} y \texttt{bot-descarga}.
|
||||
\texttt{count-api} hace OCR y \textbf{devuelve JSON} (no escribe en PG); el Desktop/extraction
|
||||
persiste esos resultados con \texttt{POST /count-jobs} en Postgres.}
|
||||
\label{fig:arquitectura}
|
||||
\end{figure}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\resizebox{\textwidth}{!}{%
|
||||
\begin{tikzpicture}[
|
||||
font=\footnotesize,
|
||||
node distance=0.45cm and 0.55cm,
|
||||
box/.style={draw=ipsblue, rounded corners=3pt, align=center, inner sep=4pt,
|
||||
minimum height=0.95cm, minimum width=2.55cm, fill=codebg, text=black},
|
||||
title/.style={font=\small\bfseries, text=ipsblue, anchor=west},
|
||||
arr/.style={-{Latex}, thick, draw=ipsblue}
|
||||
]
|
||||
% --- 1: API Gateway ---
|
||||
\node[title] (t1) at (0,0) {API Gateway};
|
||||
\node[box, below=0.3cm of t1.west, anchor=west] (client) {Desktop / Agent};
|
||||
\node[box, right=of client] (gw) {Gateway :8189};
|
||||
\node[box, right=of gw] (svc) {Microservicios};
|
||||
\draw[arr] (client) -- (gw);
|
||||
\draw[arr] (gw) -- (svc);
|
||||
|
||||
% --- 2: ARQ ---
|
||||
\node[title] (t2) at (0,-2.15) {Cola async (ARQ)};
|
||||
\node[box, below=0.3cm of t2.west, anchor=west] (api) {extraction API};
|
||||
\node[box, right=of api] (redis) {Redis / ARQ};
|
||||
\node[box, right=of redis] (w) {Workers $\times N$};
|
||||
\draw[arr] (api) -- (redis);
|
||||
\draw[arr] (redis) -- (w);
|
||||
|
||||
% --- 3: Claim ---
|
||||
\node[title] (t3) at (0,-4.3) {Claim / lease (orquestaci\'on)};
|
||||
\node[box, below=0.3cm of t3.west, anchor=west] (store) {bot-descarga\\(orquestador)};
|
||||
\node[box, right=of store] (claim) {claim at\'omico};
|
||||
\node[box, right=of claim] (agent) {Agent Selenium\\(PC)};
|
||||
\draw[arr] (store) -- (claim);
|
||||
\draw[arr] (claim) -- (agent);
|
||||
\end{tikzpicture}%
|
||||
}
|
||||
\caption{Patrones: Gateway, cola ARQ y claim. El servicio \texttt{bot-descarga} \textbf{orquesta};
|
||||
el \textbf{agent Selenium} en el PC ejecuta la descarga.}
|
||||
\label{fig:patrones}
|
||||
\end{figure}
|
||||
@@ -0,0 +1,30 @@
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}[
|
||||
font=\small,
|
||||
stage/.style={draw=ipsblue, rounded corners=3pt, align=center, inner sep=5pt,
|
||||
minimum height=1.0cm, minimum width=2.15cm, fill=codebg},
|
||||
arr/.style={-{Latex}, thick, ipsblue}
|
||||
]
|
||||
\node[stage] (dl) {1. Descarga\\agent Selenium};
|
||||
\node[stage, right=0.5cm of dl] (cnt) {2. Conteo\\OCR/ONNX};
|
||||
\node[stage, right=0.5cm of cnt] (ext) {3. Extracci\'on\\LLM + ARQ};
|
||||
\node[stage, right=0.5cm of ext] (cln) {4. Limpieza\\CSV/reglas};
|
||||
|
||||
\draw[arr] (dl) -- (cnt);
|
||||
\draw[arr] (cnt) -- (ext);
|
||||
\draw[arr] (ext) -- (cln);
|
||||
|
||||
\node[below=0.35cm of dl, font=\footnotesize\itshape, text=ipsgrey, align=center]
|
||||
{orquestado por\\bot-descarga};
|
||||
\node[below=0.35cm of cnt, font=\footnotesize\itshape, text=ipsgrey, align=center]
|
||||
{filtro};
|
||||
\node[below=0.35cm of ext, font=\footnotesize\itshape, text=ipsgrey, align=center]
|
||||
{filas PG};
|
||||
\node[below=0.35cm of cln, font=\footnotesize\itshape, text=ipsgrey, align=center]
|
||||
{lote limpio};
|
||||
\end{tikzpicture}
|
||||
\caption{Pipeline punta a punta. La descarga la hace el \textbf{agent Selenium} (PC);
|
||||
el servicio \texttt{bot-descarga} solo orquesta jobs/claim/heartbeats.}
|
||||
\label{fig:pipeline-negocio}
|
||||
\end{figure}
|
||||
@@ -1,6 +1,8 @@
|
||||
# Informe de Pruebas IPSExtractor
|
||||
|
||||
Entregable profesional de **resultados** (métricas, gráficas, capacidad 100k/mes).
|
||||
Entregable de **resultados** (métricas, gráficas, interpretación).
|
||||
|
||||
El análisis de **100 000 facturas/mes** está en [`../capacidad_100k/`](../capacidad_100k/).
|
||||
|
||||
## Compilar
|
||||
|
||||
@@ -16,5 +18,5 @@ Salida: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pd
|
||||
## Datos
|
||||
|
||||
- `data/metricas.csv` — latencias k6
|
||||
- `data/capacidad.csv` — modelo 100k/mes
|
||||
- `data/capacidad.csv` — modelo 100k/mes (referencia; detalle en PDF Capacidad)
|
||||
- `data/memoria.csv` — RAM contenedores
|
||||
|
||||
+164
-121
@@ -9,148 +9,191 @@
|
||||
\providecommand\HyField@AuxAddToFields[1]{}
|
||||
\providecommand\HyField@AuxAddToCoFields[2]{}
|
||||
\babel@aux{spanish}{}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{5}{chapter.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:resumen}{{1}{5}{Resumen ejecutivo}{chapter.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.1}Veredicto global}{5}{section.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{5}{section.1.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces KPIs ejecutivos de la campa\~na.}}{5}{table.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{5}{section.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.4}Riesgo principal}{6}{section.1.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {2}Objeto y alcance}{7}{chapter.2}\protected@file@percent }
|
||||
\newlabel{ch:resumen}{{1}{4}{Resumen ejecutivo}{chapter.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces KPIs (carga 13-jul; LLM/ARQ/OCR 17-jul).}}{4}{table.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.3}Capacidad 100k/mes (resumen)}{5}{section.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.5}Alcance de esta campa\~na}{5}{section.1.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura y patrones (contexto)}{6}{chapter.2}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:alcance}{{2}{7}{Objeto y alcance}{chapter.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.1}Objeto}{7}{section.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.2}Alcance incluido}{7}{section.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{7}{section.2.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.4}Definici\'on de factura}{7}{section.2.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{8}{chapter.3}\protected@file@percent }
|
||||
\newlabel{ch:arq-informe}{{2}{6}{Arquitectura y patrones (contexto)}{chapter.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.1}Vista de componentes}{6}{section.2.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres.}}{6}{figure.2.1}\protected@file@percent }
|
||||
\newlabel{fig:arquitectura}{{2.1}{6}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres}{figure.2.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga.}}{7}{figure.2.2}\protected@file@percent }
|
||||
\newlabel{fig:patrones}{{2.2}{7}{Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga}{figure.2.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.3}Pipeline de negocio}{7}{section.2.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.3}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{8}{figure.2.3}\protected@file@percent }
|
||||
\newlabel{fig:pipeline-negocio}{{2.3}{8}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.4}Definici\'on de factura}{8}{section.2.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{9}{chapter.3}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:entorno}{{3}{8}{Entorno bajo prueba}{chapter.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.1}Hardware y sistema}{8}{section.3.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Host de ejecuci\'on.}}{8}{table.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{8}{section.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.3}Salud del stack}{8}{section.3.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Huella de memoria por contenedor (idle). MLflow domina el host.}}{9}{figure.3.1}\protected@file@percent }
|
||||
\newlabel{fig:memoria}{{3.1}{9}{Huella de memoria por contenedor (idle). MLflow domina el host}{figure.3.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {4}Metodolog\'ia}{10}{chapter.4}\protected@file@percent }
|
||||
\newlabel{ch:entorno}{{3}{9}{Entorno bajo prueba}{chapter.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.1}Hardware y sistema}{9}{section.3.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Host de ejecuci\'on.}}{9}{table.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{9}{section.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.3}Salud del stack}{9}{section.3.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {3.1}{\ignorespaces Huella de memoria por contenedor (idle). MLflow domina el host.}}{10}{figure.3.1}\protected@file@percent }
|
||||
\newlabel{fig:memoria}{{3.1}{10}{Huella de memoria por contenedor (idle). MLflow domina el host}{figure.3.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {4}C\'omo leer este informe}{11}{chapter.4}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:metodologia}{{4}{10}{Metodolog\'ia}{chapter.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.1}Tipos de prueba}{10}{section.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.2}Herramientas}{10}{section.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{10}{section.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.4}Comandos representativos}{10}{section.4.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{11}{chapter.5}\protected@file@percent }
|
||||
\newlabel{ch:como-leer}{{4}{11}{C\'omo leer este informe}{chapter.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.1}Plantilla de interpretaci\'on}{11}{section.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.2}Leyenda de estados}{11}{section.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.3}Perfil de carga \texttt {shared8g}}{11}{section.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.4}Herramientas}{11}{section.4.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.5}Mapa de cap\'itulos de resultados}{12}{section.4.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{13}{chapter.5}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:unitarias}{{5}{11}{Resultados --- pruebas unitarias}{chapter.5}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.1}Scorecard}{11}{section.5.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Resultados unitarios.}}{11}{table.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.2}Cobertura agent}{11}{section.5.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.3}Cobertura API store}{11}{section.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.4}Cobertura count-api}{11}{section.5.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {5.1}{\ignorespaces Unitarias ejecutadas vs pendientes.}}{12}{figure.5.1}\protected@file@percent }
|
||||
\newlabel{fig:unitarias}{{5.1}{12}{Unitarias ejecutadas vs pendientes}{figure.5.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{13}{chapter.6}\protected@file@percent }
|
||||
\newlabel{ch:unitarias}{{5}{13}{Resultados --- pruebas unitarias}{chapter.5}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.1}Qu\'e eval\'uan}{13}{section.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.2}Scorecard}{13}{section.5.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.1}{\ignorespaces Resultados unitarios.}}{13}{table.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.3}Cobertura agent Selenium}{13}{section.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.4}Cobertura API \texttt {bot-descarga} (orquestador)}{14}{section.5.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.5}Cobertura count-api}{14}{section.5.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.6}Cobertura desktop (Vitest)}{14}{section.5.6}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {5.1}{\ignorespaces Unitarias ejecutadas (todas en verde tras Vitest desktop).}}{14}{figure.5.1}\protected@file@percent }
|
||||
\newlabel{fig:unitarias}{{5.1}{14}{Unitarias ejecutadas (todas en verde tras Vitest desktop)}{figure.5.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{15}{chapter.6}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:humo}{{6}{13}{Resultados --- humo y funcional}{chapter.6}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.1}Humo gateway}{13}{section.6.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {6.1}{\ignorespaces Humo HTTP.}}{13}{table.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{13}{section.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.3}Script preparado}{13}{section.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{14}{chapter.7}\protected@file@percent }
|
||||
\newlabel{ch:humo}{{6}{15}{Resultados --- humo y funcional}{chapter.6}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.1}Qu\'e eval\'uan}{15}{section.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.2}Humo gateway}{15}{section.6.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {6.1}{\ignorespaces Humo HTTP.}}{15}{table.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.3}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{15}{section.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.4}Scripts}{16}{section.6.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{17}{chapter.7}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:carga}{{7}{14}{Resultados --- pruebas de carga}{chapter.7}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.1}Tabla consolidada}{14}{section.7.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces M\'etricas de carga k6 (perfil shared8g).}}{14}{table.7.1}\protected@file@percent }
|
||||
\newlabel{tab:carga}{{7.1}{14}{M\'etricas de carga k6 (perfil shared8g)}{table.7.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.2}ST-GW-01}{14}{section.7.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{14}{section.7.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.4}Throughput gateway}{14}{section.7.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7.1}{\ignorespaces p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.}}{15}{figure.7.1}\protected@file@percent }
|
||||
\newlabel{fig:latencias}{{7.1}{15}{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla}{figure.7.1}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7.2}{\ignorespaces Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga}).}}{15}{figure.7.2}\protected@file@percent }
|
||||
\newlabel{fig:bot-before-after}{{7.2}{15}{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref {tab:carga})}{figure.7.2}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7.3}{\ignorespaces Throughput observado en health liveness (no equivale a PDFs/h).}}{15}{figure.7.3}\protected@file@percent }
|
||||
\newlabel{fig:throughput}{{7.3}{15}{Throughput observado en health liveness (no equivale a PDFs/h)}{figure.7.3}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{16}{chapter.8}\protected@file@percent }
|
||||
\newlabel{ch:carga}{{7}{17}{Resultados --- pruebas de carga}{chapter.7}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.1}Qu\'e eval\'uan}{17}{section.7.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.2}Tabla consolidada}{17}{section.7.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces M\'etricas de carga k6 (perfil shared8g).}}{17}{table.7.1}\protected@file@percent }
|
||||
\newlabel{tab:carga}{{7.1}{17}{M\'etricas de carga k6 (perfil shared8g)}{table.7.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.3}ST-GW-01}{17}{section.7.3}\protected@file@percent }
|
||||
\newlabel{sec:st-gw-01}{{7.3}{17}{ST-GW-01}{section.7.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.4}ST-BOT-01 --- comparativa}{18}{section.7.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7.1}{\ignorespaces Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1 fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores. Detalle num\'erico en la tabla~\ref {tab:carga}.}}{19}{figure.7.1}\protected@file@percent }
|
||||
\newlabel{fig:bot-before-after}{{7.1}{19}{Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1 fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores. Detalle num\'erico en la tabla~\ref {tab:carga}}{figure.7.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.5}Throughput gateway}{19}{section.7.5}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {7.2}{\ignorespaces Ritmo del gateway en health liveness ($\approx $57\,req/s). Solo mide HTTP ligero, no facturas procesadas.}}{19}{figure.7.2}\protected@file@percent }
|
||||
\newlabel{fig:throughput}{{7.2}{19}{Ritmo del gateway en health liveness ($\approx $57\,req/s). Solo mide HTTP ligero, no facturas procesadas}{figure.7.2}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {8}Count-api: qu\'e eval\'ua cada prueba}{20}{chapter.8}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:capacidad}{{8}{16}{Capacidad: 100\,000 facturas al mes}{chapter.8}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{16}{section.8.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {8.1}{\ignorespaces Throughput m\'inimo para cumplir 100k/mes.}}{16}{table.8.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.2}Modelo por etapa}{16}{section.8.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {8.2}{\ignorespaces Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo.}}{16}{table.8.2}\protected@file@percent }
|
||||
\newlabel{tab:capacidad}{{8.2}{16}{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger }$Con $N$ agents en paralelo}{table.8.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.3}Interpretaci\'on}{16}{section.8.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {8.1}{\ignorespaces Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad.}}{17}{figure.8.1}\protected@file@percent }
|
||||
\newlabel{fig:capacidad}{{8.1}{17}{Brecha de capacidad. Gateway real $\gg $1000/h; se trunca para legibilidad}{figure.8.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.4}Veredicto}{17}{section.8.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{17}{section.8.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{18}{chapter.9}\protected@file@percent }
|
||||
\newlabel{ch:count-latency}{{8}{20}{Count-api: qu\'e eval\'ua cada prueba}{chapter.8}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.1}Qu\'e eval\'ua / resultado / significado}{20}{section.8.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.2}Pipeline bajo prueba}{20}{section.8.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {8.1}{\ignorespaces Scorecard count-api --- campa\~na 23 PDFs (13 jul 2026).}}{21}{table.8.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {8.1}{\ignorespaces Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go).}}{21}{figure.8.1}\protected@file@percent }
|
||||
\newlabel{fig:count-score}{{8.1}{21}{Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go)}{figure.8.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.3}Resultado global count-api}{21}{section.8.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.4}Features: qu\'e se eval\'ua (con ejemplo)}{21}{section.8.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.4.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.7}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.5}Clustering: qu\'e se eval\'ua (con ejemplo)}{22}{section.8.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.5.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {8.2}{\ignorespaces Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales).}}{23}{figure.8.2}\protected@file@percent }
|
||||
\newlabel{fig:cluster-idea}{{8.2}{23}{Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales)}{figure.8.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.6}Pruebas con PDFs reales}{23}{section.8.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.1}Fixtures}{23}{subsection.8.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.2}TestProcessPDFTimingFixtures}{23}{subsection.8.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {8.6.3}Script HTTP \texttt {measure\_count\_latency}}{23}{subsection.8.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.7}Resultados agregados (23 PDFs)}{23}{section.8.7}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {8.2}{\ignorespaces Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt {ProcessPDF} directo.}}{23}{table.8.2}\protected@file@percent }
|
||||
\newlabel{tab:count-stats}{{8.2}{23}{Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt {ProcessPDF} directo}{table.8.2}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {8.3}{\ignorespaces El OCR representa $\sim $93\,\% del tiempo de procesamiento.}}{24}{figure.8.3}\protected@file@percent }
|
||||
\newlabel{fig:count-breakdown}{{8.3}{24}{El OCR representa $\sim $93\,\% del tiempo de procesamiento}{figure.8.3}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {8.4}{\ignorespaces Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms.}}{24}{figure.8.4}\protected@file@percent }
|
||||
\newlabel{fig:count-series}{{8.4}{24}{Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms}{figure.8.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.8}Resultados de latencia HTTP}{24}{section.8.8}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {8.3}{\ignorespaces Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026.}}{24}{table.8.3}\protected@file@percent }
|
||||
\newlabel{tab:count-http}{{8.3}{24}{Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026}{table.8.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.9}Detalle por archivo}{25}{section.8.9}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {8.10}C\'omo reproducir}{25}{section.8.10}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {9}Extracci\'on LLM: bench de latencia (ST-EXT-02)}{26}{chapter.9}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:analisis}{{9}{18}{An\'alisis y hallazgos}{chapter.9}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.1}Hallazgos}{18}{section.9.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.2}Scorecard global}{18}{section.9.2}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {9.1}{\ignorespaces Distribuci\'on de resultados por tipo de prueba.}}{18}{figure.9.1}\protected@file@percent }
|
||||
\newlabel{fig:scorecard}{{9.1}{18}{Distribuci\'on de resultados por tipo de prueba}{figure.9.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.3}Limitaciones}{18}{section.9.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{19}{chapter.10}\protected@file@percent }
|
||||
\newlabel{ch:llm-latency}{{9}{26}{Extracci\'on LLM: bench de latencia (ST-EXT-02)}{chapter.9}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.1}Qu\'e eval\'ua / resultado / significado}{26}{section.9.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.2}C\'omo ejecutar}{26}{section.9.2}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {9.1}ST-EXT-02 (Windows PowerShell)}{26}{lstlisting.9.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.3}Resultados (2026-07-17)}{27}{section.9.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {9.1}{\ignorespaces ST-EXT-02 --- latencia LLM medida (mismo schema/campos).}}{27}{table.9.1}\protected@file@percent }
|
||||
\newlabel{tab:llm-latency}{{9.1}{27}{ST-EXT-02 --- latencia LLM medida (mismo schema/campos)}{table.9.1}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {9.1}{\ignorespaces La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota).}}{27}{figure.9.1}\protected@file@percent }
|
||||
\newlabel{fig:llm-vs-obj}{{9.1}{27}{La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota)}{figure.9.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.4}Lectura para capacidad}{27}{section.9.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {10}Cola ARQ: bench ST-EXT-01 (LLM mock)}{28}{chapter.10}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:conclusiones}{{10}{19}{Conclusiones y recomendaciones}{chapter.10}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.1}Conclusiones}{19}{section.10.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.2}Recomendaciones}{19}{section.10.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{19}{section.10.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {11}Anexos}{20}{chapter.11}\protected@file@percent }
|
||||
\newlabel{ch:arq-ext01}{{10}{28}{Cola ARQ: bench ST-EXT-01 (LLM mock)}{chapter.10}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.1}Qu\'e eval\'ua / resultado / significado}{28}{section.10.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.2}Condiciones de la corrida}{28}{section.10.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {10.1}{\ignorespaces ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs).}}{29}{table.10.1}\protected@file@percent }
|
||||
\newlabel{tab:st-ext-01}{{10.1}{29}{ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs)}{table.10.1}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {10.1}{\ignorespaces Casi todo el tiempo es drenado por workers; el enqueue es despreciable.}}{29}{figure.10.1}\protected@file@percent }
|
||||
\newlabel{fig:st-ext-01-times}{{10.1}{29}{Casi todo el tiempo es drenado por workers; el enqueue es despreciable}{figure.10.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.3}Resultados}{29}{section.10.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.4}Lectura}{29}{section.10.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {11}Count-api bajo carga (ST-COUNT-01)}{30}{chapter.11}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:anexos}{{11}{20}{Anexos}{chapter.11}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.1}Datos fuente}{20}{section.11.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{20}{section.11.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{20}{section.11.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.4}Unitarias}{20}{section.11.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.5}Scripts}{20}{section.11.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.6}Reproducibilidad}{21}{section.11.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {12}Count-api: qu\'e eval\'ua cada prueba}{22}{chapter.12}\protected@file@percent }
|
||||
\newlabel{ch:st-count-01}{{11}{30}{Count-api bajo carga (ST-COUNT-01)}{chapter.11}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.1}Qu\'e eval\'ua / resultado / significado}{30}{section.11.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.2}Condiciones}{30}{section.11.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.3}Resultados}{30}{section.11.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.4}Lectura}{30}{section.11.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {11.1}{\ignorespaces ST-COUNT-01 --- carga OCR (PDFs reales, shared8g).}}{31}{table.11.1}\protected@file@percent }
|
||||
\newlabel{tab:st-count-01}{{11.1}{31}{ST-COUNT-01 --- carga OCR (PDFs reales, shared8g)}{table.11.1}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {11.1}{\ignorespaces Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h. La latencia por PDF se degrada mucho frente a la serie ($\sim $2\,s).}}{31}{figure.11.1}\protected@file@percent }
|
||||
\newlabel{fig:st-count-01}{{11.1}{31}{Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h. La latencia por PDF se degrada mucho frente a la serie ($\sim $2\,s)}{figure.11.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {12}An\'alisis y hallazgos}{32}{chapter.12}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:count-latency}{{12}{22}{Count-api: qu\'e eval\'ua cada prueba}{chapter.12}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.1}Pipeline bajo prueba}{22}{section.12.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.2}Resultado global count-api}{22}{section.12.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Scorecard count-api en esta campa\~na.}}{22}{table.12.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {12.1}{\ignorespaces Cobertura count-api por bloque.}}{22}{figure.12.1}\protected@file@percent }
|
||||
\newlabel{fig:count-score}{{12.1}{22}{Cobertura count-api por bloque}{figure.12.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.3}Features: qu\'e se eval\'ua (con ejemplo)}{23}{section.12.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.7}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.4}Clustering: qu\'e se eval\'ua (con ejemplo)}{24}{section.12.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.4.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.4}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {12.2}{\ignorespaces Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales).}}{24}{figure.12.2}\protected@file@percent }
|
||||
\newlabel{fig:cluster-idea}{{12.2}{24}{Ilustraci\'on del test de dos bandas (no son coordenadas OCR reales)}{figure.12.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.5}Pruebas con PDFs reales}{24}{section.12.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.1}Fixtures}{24}{subsection.12.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.2}TestProcessPDFTimingFixtures}{25}{subsection.12.5.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.5.3}Script HTTP \texttt {measure\_count\_latency}}{25}{subsection.12.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.6}Resultados de latencia HTTP (medidos)}{25}{section.12.6}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {12.2}{\ignorespaces Wall-clock HTTP. Fuente: \texttt {informes/count\_latency\_20260713.csv}.}}{25}{table.12.2}\protected@file@percent }
|
||||
\newlabel{tab:count-http}{{12.2}{25}{Wall-clock HTTP. Fuente: \texttt {informes/count\_latency\_20260713.csv}}{table.12.2}{}}
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {12.3}{\ignorespaces Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s.}}{25}{figure.12.3}\protected@file@percent }
|
||||
\newlabel{fig:count-http-bars}{{12.3}{25}{Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s}{figure.12.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.7}C\'omo reproducir}{25}{section.12.7}\protected@file@percent }
|
||||
\gdef \@abspage@last{27}
|
||||
\newlabel{ch:analisis}{{12}{32}{An\'alisis y hallazgos}{chapter.12}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.1}Hallazgos}{32}{section.12.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.2}Scorecard global}{32}{section.12.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.3}Limitaciones}{32}{section.12.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {12.1}{\ignorespaces Distribuci\'on de resultados por tipo de prueba.}}{33}{figure.12.1}\protected@file@percent }
|
||||
\newlabel{fig:scorecard}{{12.1}{33}{Distribuci\'on de resultados por tipo de prueba}{figure.12.1}{}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {13}Conclusiones y recomendaciones}{34}{chapter.13}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:conclusiones}{{13}{34}{Conclusiones y recomendaciones}{chapter.13}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {13.1}Conclusiones}{34}{section.13.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {13.2}Recomendaciones}{34}{section.13.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {13.3}Documentos relacionados}{34}{section.13.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {14}Anexos}{35}{chapter.14}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:anexos}{{14}{35}{Anexos}{chapter.14}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.1}Datos fuente}{35}{section.14.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.2}Extracto ST-GW-01}{35}{section.14.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.3}Extracto ST-BOT-01 \#2}{35}{section.14.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.4}Unitarias}{35}{section.14.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.5}Scripts}{35}{section.14.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.6}Extracto ST-EXT-01}{36}{section.14.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.7}Documentos hermanos}{36}{section.14.7}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {14.8}Reproducibilidad}{36}{section.14.8}\protected@file@percent }
|
||||
\gdef \@abspage@last{37}
|
||||
|
||||
@@ -1,76 +1,96 @@
|
||||
\BOOKMARK [0][-]{chapter.1}{\376\377\000R\000e\000s\000u\000m\000e\000n\000\040\000e\000j\000e\000c\000u\000t\000i\000v\000o}{}% 1
|
||||
\BOOKMARK [1][-]{section.1.1}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.1}% 2
|
||||
\BOOKMARK [1][-]{section.1.2}{\376\377\000K\000P\000I\000s\000\040\000d\000e\000\040\000l\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.1}% 3
|
||||
\BOOKMARK [1][-]{section.1.3}{\376\377\000R\000e\000s\000p\000u\000e\000s\000t\000a\000\040\000d\000i\000r\000e\000c\000t\000a\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000/\000m\000e\000s}{chapter.1}% 4
|
||||
\BOOKMARK [1][-]{section.1.3}{\376\377\000C\000a\000p\000a\000c\000i\000d\000a\000d\000\040\0001\0000\0000\000k\000/\000m\000e\000s\000\040\000\050\000r\000e\000s\000u\000m\000e\000n\000\051}{chapter.1}% 4
|
||||
\BOOKMARK [1][-]{section.1.4}{\376\377\000R\000i\000e\000s\000g\000o\000\040\000p\000r\000i\000n\000c\000i\000p\000a\000l}{chapter.1}% 5
|
||||
\BOOKMARK [0][-]{chapter.2}{\376\377\000O\000b\000j\000e\000t\000o\000\040\000y\000\040\000a\000l\000c\000a\000n\000c\000e}{}% 6
|
||||
\BOOKMARK [1][-]{section.2.1}{\376\377\000O\000b\000j\000e\000t\000o}{chapter.2}% 7
|
||||
\BOOKMARK [1][-]{section.2.2}{\376\377\000A\000l\000c\000a\000n\000c\000e\000\040\000i\000n\000c\000l\000u\000i\000d\000o}{chapter.2}% 8
|
||||
\BOOKMARK [1][-]{section.2.3}{\376\377\000F\000u\000e\000r\000a\000\040\000d\000e\000\040\000a\000l\000c\000a\000n\000c\000e\000\040\000d\000e\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.2}% 9
|
||||
\BOOKMARK [1][-]{section.2.4}{\376\377\000D\000e\000f\000i\000n\000i\000c\000i\000\363\000n\000\040\000d\000e\000\040\000f\000a\000c\000t\000u\000r\000a}{chapter.2}% 10
|
||||
\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 11
|
||||
\BOOKMARK [1][-]{section.3.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.3}% 12
|
||||
\BOOKMARK [1][-]{section.3.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000m\000e\000m\000o\000r\000i\000a\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\051}{chapter.3}% 13
|
||||
\BOOKMARK [1][-]{section.3.3}{\376\377\000S\000a\000l\000u\000d\000\040\000d\000e\000l\000\040\000s\000t\000a\000c\000k}{chapter.3}% 14
|
||||
\BOOKMARK [0][-]{chapter.4}{\376\377\000M\000e\000t\000o\000d\000o\000l\000o\000g\000\355\000a}{}% 15
|
||||
\BOOKMARK [1][-]{section.4.1}{\376\377\000T\000i\000p\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.4}% 16
|
||||
\BOOKMARK [1][-]{section.4.2}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s}{chapter.4}% 17
|
||||
\BOOKMARK [1][-]{section.4.3}{\376\377\000S\000L\000O\000s\000\040\000p\000e\000r\000f\000i\000l\000\040\000s\000h\000a\000r\000e\000d\0008\000g}{chapter.4}% 18
|
||||
\BOOKMARK [1][-]{section.4.4}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000r\000e\000p\000r\000e\000s\000e\000n\000t\000a\000t\000i\000v\000o\000s}{chapter.4}% 19
|
||||
\BOOKMARK [0][-]{chapter.5}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 20
|
||||
\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d}{chapter.5}% 21
|
||||
\BOOKMARK [1][-]{section.5.2}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000a\000g\000e\000n\000t}{chapter.5}% 22
|
||||
\BOOKMARK [1][-]{section.5.3}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000A\000P\000I\000\040\000s\000t\000o\000r\000e}{chapter.5}% 23
|
||||
\BOOKMARK [1][-]{section.5.4}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.5}% 24
|
||||
\BOOKMARK [0][-]{chapter.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000h\000u\000m\000o\000\040\000y\000\040\000f\000u\000n\000c\000i\000o\000n\000a\000l}{}% 25
|
||||
\BOOKMARK [1][-]{section.6.1}{\376\377\000H\000u\000m\000o\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.6}% 26
|
||||
\BOOKMARK [1][-]{section.6.2}{\376\377\000F\000u\000n\000c\000i\000o\000n\000a\000l\000\040\000E\0002\000E\000-\0000\0007\000\040\000\050\000w\000o\000r\000k\000e\000r\000\040\000o\000f\000f\000l\000i\000n\000e\000\040\000\040\0004\0000\0009\000\051}{chapter.6}% 27
|
||||
\BOOKMARK [1][-]{section.6.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000p\000r\000e\000p\000a\000r\000a\000d\000o}{chapter.6}% 28
|
||||
\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{}% 29
|
||||
\BOOKMARK [1][-]{section.7.1}{\376\377\000T\000a\000b\000l\000a\000\040\000c\000o\000n\000s\000o\000l\000i\000d\000a\000d\000a}{chapter.7}% 30
|
||||
\BOOKMARK [1][-]{section.7.2}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.7}% 31
|
||||
\BOOKMARK [1][-]{section.7.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000c\000o\000m\000p\000a\000r\000a\000t\000i\000v\000a}{chapter.7}% 32
|
||||
\BOOKMARK [1][-]{section.7.4}{\376\377\000T\000h\000r\000o\000u\000g\000h\000p\000u\000t\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.7}% 33
|
||||
\BOOKMARK [0][-]{chapter.8}{\376\377\000C\000a\000p\000a\000c\000i\000d\000a\000d\000:\000\040\0001\0000\0000\0000\0000\0000\000\040\000f\000a\000c\000t\000u\000r\000a\000s\000\040\000a\000l\000\040\000m\000e\000s}{}% 34
|
||||
\BOOKMARK [1][-]{section.8.1}{\376\377\000T\000r\000a\000d\000u\000c\000c\000i\000\363\000n\000\040\000d\000e\000l\000\040\000o\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 35
|
||||
\BOOKMARK [1][-]{section.8.2}{\376\377\000M\000o\000d\000e\000l\000o\000\040\000p\000o\000r\000\040\000e\000t\000a\000p\000a}{chapter.8}% 36
|
||||
\BOOKMARK [1][-]{section.8.3}{\376\377\000I\000n\000t\000e\000r\000p\000r\000e\000t\000a\000c\000i\000\363\000n}{chapter.8}% 37
|
||||
\BOOKMARK [1][-]{section.8.4}{\376\377\000V\000e\000r\000e\000d\000i\000c\000t\000o}{chapter.8}% 38
|
||||
\BOOKMARK [1][-]{section.8.5}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000m\000\355\000n\000i\000m\000o\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000o\000s\000\040\000p\000a\000r\000a\000\040\0001\0000\0000\000k\000/\000m\000e\000s}{chapter.8}% 39
|
||||
\BOOKMARK [0][-]{chapter.9}{\376\377\000A\000n\000\341\000l\000i\000s\000i\000s\000\040\000y\000\040\000h\000a\000l\000l\000a\000z\000g\000o\000s}{}% 40
|
||||
\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.9}% 41
|
||||
\BOOKMARK [1][-]{section.9.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d\000\040\000g\000l\000o\000b\000a\000l}{chapter.9}% 42
|
||||
\BOOKMARK [1][-]{section.9.3}{\376\377\000L\000i\000m\000i\000t\000a\000c\000i\000o\000n\000e\000s}{chapter.9}% 43
|
||||
\BOOKMARK [0][-]{chapter.10}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s\000\040\000y\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{}% 44
|
||||
\BOOKMARK [1][-]{section.10.1}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s}{chapter.10}% 45
|
||||
\BOOKMARK [1][-]{section.10.2}{\376\377\000R\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{chapter.10}% 46
|
||||
\BOOKMARK [1][-]{section.10.3}{\376\377\000P\000r\000\363\000x\000i\000m\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000s\000u\000g\000e\000r\000i\000d\000a}{chapter.10}% 47
|
||||
\BOOKMARK [0][-]{chapter.11}{\376\377\000A\000n\000e\000x\000o\000s}{}% 48
|
||||
\BOOKMARK [1][-]{section.11.1}{\376\377\000D\000a\000t\000o\000s\000\040\000f\000u\000e\000n\000t\000e}{chapter.11}% 49
|
||||
\BOOKMARK [1][-]{section.11.2}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.11}% 50
|
||||
\BOOKMARK [1][-]{section.11.3}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\000\043\0002}{chapter.11}% 51
|
||||
\BOOKMARK [1][-]{section.11.4}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s}{chapter.11}% 52
|
||||
\BOOKMARK [1][-]{section.11.5}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.11}% 53
|
||||
\BOOKMARK [1][-]{section.11.6}{\376\377\000R\000e\000p\000r\000o\000d\000u\000c\000i\000b\000i\000l\000i\000d\000a\000d}{chapter.11}% 54
|
||||
\BOOKMARK [0][-]{chapter.12}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000:\000\040\000q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000c\000a\000d\000a\000\040\000p\000r\000u\000e\000b\000a}{}% 55
|
||||
\BOOKMARK [1][-]{section.12.1}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{chapter.12}% 56
|
||||
\BOOKMARK [1][-]{section.12.2}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000g\000l\000o\000b\000a\000l\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.12}% 57
|
||||
\BOOKMARK [1][-]{section.12.3}{\376\377\000F\000e\000a\000t\000u\000r\000e\000s\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.12}% 58
|
||||
\BOOKMARK [2][-]{subsection.12.3.1}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 59
|
||||
\BOOKMARK [2][-]{subsection.12.3.2}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000s\000C\000o\000n\000t\000i\000n\000u\000a\000t\000i\000o\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 60
|
||||
\BOOKMARK [2][-]{subsection.12.3.3}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000H\000e\000a\000d\000e\000r\000K\000e\000y\000w\000o\000r\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 61
|
||||
\BOOKMARK [2][-]{subsection.12.3.4}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000C\000o\000d\000e\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 62
|
||||
\BOOKMARK [2][-]{subsection.12.3.5}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000N\000o\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 63
|
||||
\BOOKMARK [2][-]{subsection.12.3.6}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 64
|
||||
\BOOKMARK [2][-]{subsection.12.3.7}{\376\377\000T\000e\000s\000t\000F\000e\000a\000t\000u\000r\000e\000C\000o\000u\000n\000t\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.3}% 65
|
||||
\BOOKMARK [1][-]{section.12.4}{\376\377\000C\000l\000u\000s\000t\000e\000r\000i\000n\000g\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.12}% 66
|
||||
\BOOKMARK [2][-]{subsection.12.4.1}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000T\000w\000o\000B\000a\000n\000d\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 67
|
||||
\BOOKMARK [2][-]{subsection.12.4.2}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000S\000a\000m\000e\000B\000a\000n\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 68
|
||||
\BOOKMARK [2][-]{subsection.12.4.3}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 69
|
||||
\BOOKMARK [2][-]{subsection.12.4.4}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000N\000o\000i\000s\000e\000M\000i\000n\000S\000a\000m\000p\000l\000e\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.12.4}% 70
|
||||
\BOOKMARK [1][-]{section.12.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000c\000o\000n\000\040\000P\000D\000F\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.12}% 71
|
||||
\BOOKMARK [2][-]{subsection.12.5.1}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s}{section.12.5}% 72
|
||||
\BOOKMARK [2][-]{subsection.12.5.2}{\376\377\000T\000e\000s\000t\000P\000r\000o\000c\000e\000s\000s\000P\000D\000F\000T\000i\000m\000i\000n\000g\000F\000i\000x\000t\000u\000r\000e\000s}{section.12.5}% 73
|
||||
\BOOKMARK [2][-]{subsection.12.5.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000H\000T\000T\000P\000\040\000m\000e\000a\000s\000u\000r\000e\000\137\000c\000o\000u\000n\000t\000\137\000l\000a\000t\000e\000n\000c\000y}{section.12.5}% 74
|
||||
\BOOKMARK [1][-]{section.12.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000H\000T\000T\000P\000\040\000\050\000m\000e\000d\000i\000d\000o\000s\000\051}{chapter.12}% 75
|
||||
\BOOKMARK [1][-]{section.12.7}{\376\377\000C\000\363\000m\000o\000\040\000r\000e\000p\000r\000o\000d\000u\000c\000i\000r}{chapter.12}% 76
|
||||
\BOOKMARK [1][-]{section.1.5}{\376\377\000A\000l\000c\000a\000n\000c\000e\000\040\000d\000e\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.1}% 6
|
||||
\BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000y\000\040\000p\000a\000t\000r\000o\000n\000e\000s\000\040\000\050\000c\000o\000n\000t\000e\000x\000t\000o\000\051}{}% 7
|
||||
\BOOKMARK [1][-]{section.2.1}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 8
|
||||
\BOOKMARK [1][-]{section.2.2}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o}{chapter.2}% 9
|
||||
\BOOKMARK [1][-]{section.2.3}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 10
|
||||
\BOOKMARK [1][-]{section.2.4}{\376\377\000D\000e\000f\000i\000n\000i\000c\000i\000\363\000n\000\040\000d\000e\000\040\000f\000a\000c\000t\000u\000r\000a}{chapter.2}% 11
|
||||
\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 12
|
||||
\BOOKMARK [1][-]{section.3.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.3}% 13
|
||||
\BOOKMARK [1][-]{section.3.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000m\000e\000m\000o\000r\000i\000a\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\051}{chapter.3}% 14
|
||||
\BOOKMARK [1][-]{section.3.3}{\376\377\000S\000a\000l\000u\000d\000\040\000d\000e\000l\000\040\000s\000t\000a\000c\000k}{chapter.3}% 15
|
||||
\BOOKMARK [0][-]{chapter.4}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000e\000s\000t\000e\000\040\000i\000n\000f\000o\000r\000m\000e}{}% 16
|
||||
\BOOKMARK [1][-]{section.4.1}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000t\000e\000r\000p\000r\000e\000t\000a\000c\000i\000\363\000n}{chapter.4}% 17
|
||||
\BOOKMARK [1][-]{section.4.2}{\376\377\000L\000e\000y\000e\000n\000d\000a\000\040\000d\000e\000\040\000e\000s\000t\000a\000d\000o\000s}{chapter.4}% 18
|
||||
\BOOKMARK [1][-]{section.4.3}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000d\000e\000\040\000c\000a\000r\000g\000a\000\040\000s\000h\000a\000r\000e\000d\0008\000g}{chapter.4}% 19
|
||||
\BOOKMARK [1][-]{section.4.4}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s}{chapter.4}% 20
|
||||
\BOOKMARK [1][-]{section.4.5}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000c\000a\000p\000\355\000t\000u\000l\000o\000s\000\040\000d\000e\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.4}% 21
|
||||
\BOOKMARK [0][-]{chapter.5}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 22
|
||||
\BOOKMARK [1][-]{section.5.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.5}% 23
|
||||
\BOOKMARK [1][-]{section.5.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d}{chapter.5}% 24
|
||||
\BOOKMARK [1][-]{section.5.3}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000a\000g\000e\000n\000t\000\040\000S\000e\000l\000e\000n\000i\000u\000m}{chapter.5}% 25
|
||||
\BOOKMARK [1][-]{section.5.4}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000A\000P\000I\000\040\000b\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000\050\000o\000r\000q\000u\000e\000s\000t\000a\000d\000o\000r\000\051}{chapter.5}% 26
|
||||
\BOOKMARK [1][-]{section.5.5}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.5}% 27
|
||||
\BOOKMARK [1][-]{section.5.6}{\376\377\000C\000o\000b\000e\000r\000t\000u\000r\000a\000\040\000d\000e\000s\000k\000t\000o\000p\000\040\000\050\000V\000i\000t\000e\000s\000t\000\051}{chapter.5}% 28
|
||||
\BOOKMARK [0][-]{chapter.6}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000h\000u\000m\000o\000\040\000y\000\040\000f\000u\000n\000c\000i\000o\000n\000a\000l}{}% 29
|
||||
\BOOKMARK [1][-]{section.6.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.6}% 30
|
||||
\BOOKMARK [1][-]{section.6.2}{\376\377\000H\000u\000m\000o\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.6}% 31
|
||||
\BOOKMARK [1][-]{section.6.3}{\376\377\000F\000u\000n\000c\000i\000o\000n\000a\000l\000\040\000E\0002\000E\000-\0000\0007\000\040\000\050\000w\000o\000r\000k\000e\000r\000\040\000o\000f\000f\000l\000i\000n\000e\000\040\000\040\0004\0000\0009\000\051}{chapter.6}% 32
|
||||
\BOOKMARK [1][-]{section.6.4}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.6}% 33
|
||||
\BOOKMARK [0][-]{chapter.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\040\024\000\040\000p\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{}% 34
|
||||
\BOOKMARK [1][-]{section.7.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000n}{chapter.7}% 35
|
||||
\BOOKMARK [1][-]{section.7.2}{\376\377\000T\000a\000b\000l\000a\000\040\000c\000o\000n\000s\000o\000l\000i\000d\000a\000d\000a}{chapter.7}% 36
|
||||
\BOOKMARK [1][-]{section.7.3}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.7}% 37
|
||||
\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000c\000o\000m\000p\000a\000r\000a\000t\000i\000v\000a}{chapter.7}% 38
|
||||
\BOOKMARK [1][-]{section.7.5}{\376\377\000T\000h\000r\000o\000u\000g\000h\000p\000u\000t\000\040\000g\000a\000t\000e\000w\000a\000y}{chapter.7}% 39
|
||||
\BOOKMARK [0][-]{chapter.8}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000:\000\040\000q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000c\000a\000d\000a\000\040\000p\000r\000u\000e\000b\000a}{}% 40
|
||||
\BOOKMARK [1][-]{section.8.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.8}% 41
|
||||
\BOOKMARK [1][-]{section.8.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{chapter.8}% 42
|
||||
\BOOKMARK [1][-]{section.8.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000g\000l\000o\000b\000a\000l\000\040\000c\000o\000u\000n\000t\000-\000a\000p\000i}{chapter.8}% 43
|
||||
\BOOKMARK [1][-]{section.8.4}{\376\377\000F\000e\000a\000t\000u\000r\000e\000s\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.8}% 44
|
||||
\BOOKMARK [2][-]{subsection.8.4.1}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 45
|
||||
\BOOKMARK [2][-]{subsection.8.4.2}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000s\000C\000o\000n\000t\000i\000n\000u\000a\000t\000i\000o\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 46
|
||||
\BOOKMARK [2][-]{subsection.8.4.3}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000H\000e\000a\000d\000e\000r\000K\000e\000y\000w\000o\000r\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 47
|
||||
\BOOKMARK [2][-]{subsection.8.4.4}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000S\000e\000r\000v\000i\000c\000e\000C\000o\000d\000e\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 48
|
||||
\BOOKMARK [2][-]{subsection.8.4.5}{\376\377\000T\000e\000s\000t\000E\000x\000t\000r\000a\000c\000t\000R\000o\000w\000F\000e\000a\000t\000u\000r\000e\000s\000N\000o\000T\000a\000b\000l\000e\000P\000a\000t\000t\000e\000r\000n\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 49
|
||||
\BOOKMARK [2][-]{subsection.8.4.6}{\376\377\000T\000e\000s\000t\000B\000u\000i\000l\000d\000F\000e\000a\000t\000u\000r\000e\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 50
|
||||
\BOOKMARK [2][-]{subsection.8.4.7}{\376\377\000T\000e\000s\000t\000F\000e\000a\000t\000u\000r\000e\000C\000o\000u\000n\000t\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.4}% 51
|
||||
\BOOKMARK [1][-]{section.8.5}{\376\377\000C\000l\000u\000s\000t\000e\000r\000i\000n\000g\000:\000\040\000q\000u\000\351\000\040\000s\000e\000\040\000e\000v\000a\000l\000\372\000a\000\040\000\050\000c\000o\000n\000\040\000e\000j\000e\000m\000p\000l\000o\000\051}{chapter.8}% 52
|
||||
\BOOKMARK [2][-]{subsection.8.5.1}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000T\000w\000o\000B\000a\000n\000d\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 53
|
||||
\BOOKMARK [2][-]{subsection.8.5.2}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000S\000a\000m\000e\000B\000a\000n\000d\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 54
|
||||
\BOOKMARK [2][-]{subsection.8.5.3}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000E\000m\000p\000t\000y\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 55
|
||||
\BOOKMARK [2][-]{subsection.8.5.4}{\376\377\000T\000e\000s\000t\000C\000l\000u\000s\000t\000e\000r\000R\000o\000w\000s\000N\000o\000i\000s\000e\000M\000i\000n\000S\000a\000m\000p\000l\000e\000s\000\040\040\024\000\040\000P\000A\000S\000S}{section.8.5}% 56
|
||||
\BOOKMARK [1][-]{section.8.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000c\000o\000n\000\040\000P\000D\000F\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.8}% 57
|
||||
\BOOKMARK [2][-]{subsection.8.6.1}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s}{section.8.6}% 58
|
||||
\BOOKMARK [2][-]{subsection.8.6.2}{\376\377\000T\000e\000s\000t\000P\000r\000o\000c\000e\000s\000s\000P\000D\000F\000T\000i\000m\000i\000n\000g\000F\000i\000x\000t\000u\000r\000e\000s}{section.8.6}% 59
|
||||
\BOOKMARK [2][-]{subsection.8.6.3}{\376\377\000S\000c\000r\000i\000p\000t\000\040\000H\000T\000T\000P\000\040\000m\000e\000a\000s\000u\000r\000e\000\137\000c\000o\000u\000n\000t\000\137\000l\000a\000t\000e\000n\000c\000y}{section.8.6}% 60
|
||||
\BOOKMARK [1][-]{section.8.7}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000a\000g\000r\000e\000g\000a\000d\000o\000s\000\040\000\050\0002\0003\000\040\000P\000D\000F\000s\000\051}{chapter.8}% 61
|
||||
\BOOKMARK [1][-]{section.8.8}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000H\000T\000T\000P}{chapter.8}% 62
|
||||
\BOOKMARK [1][-]{section.8.9}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000p\000o\000r\000\040\000a\000r\000c\000h\000i\000v\000o}{chapter.8}% 63
|
||||
\BOOKMARK [1][-]{section.8.10}{\376\377\000C\000\363\000m\000o\000\040\000r\000e\000p\000r\000o\000d\000u\000c\000i\000r}{chapter.8}% 64
|
||||
\BOOKMARK [0][-]{chapter.9}{\376\377\000E\000x\000t\000r\000a\000c\000c\000i\000\363\000n\000\040\000L\000L\000M\000:\000\040\000b\000e\000n\000c\000h\000\040\000d\000e\000\040\000l\000a\000t\000e\000n\000c\000i\000a\000\040\000\050\000S\000T\000-\000E\000X\000T\000-\0000\0002\000\051}{}% 65
|
||||
\BOOKMARK [1][-]{section.9.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.9}% 66
|
||||
\BOOKMARK [1][-]{section.9.2}{\376\377\000C\000\363\000m\000o\000\040\000e\000j\000e\000c\000u\000t\000a\000r}{chapter.9}% 67
|
||||
\BOOKMARK [1][-]{section.9.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000\050\0002\0000\0002\0006\000-\0000\0007\000-\0001\0007\000\051}{chapter.9}% 68
|
||||
\BOOKMARK [1][-]{section.9.4}{\376\377\000L\000e\000c\000t\000u\000r\000a\000\040\000p\000a\000r\000a\000\040\000c\000a\000p\000a\000c\000i\000d\000a\000d}{chapter.9}% 69
|
||||
\BOOKMARK [0][-]{chapter.10}{\376\377\000C\000o\000l\000a\000\040\000A\000R\000Q\000:\000\040\000b\000e\000n\000c\000h\000\040\000S\000T\000-\000E\000X\000T\000-\0000\0001\000\040\000\050\000L\000L\000M\000\040\000m\000o\000c\000k\000\051}{}% 70
|
||||
\BOOKMARK [1][-]{section.10.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.10}% 71
|
||||
\BOOKMARK [1][-]{section.10.2}{\376\377\000C\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s\000\040\000d\000e\000\040\000l\000a\000\040\000c\000o\000r\000r\000i\000d\000a}{chapter.10}% 72
|
||||
\BOOKMARK [1][-]{section.10.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.10}% 73
|
||||
\BOOKMARK [1][-]{section.10.4}{\376\377\000L\000e\000c\000t\000u\000r\000a}{chapter.10}% 74
|
||||
\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000o\000u\000n\000t\000-\000a\000p\000i\000\040\000b\000a\000j\000o\000\040\000c\000a\000r\000g\000a\000\040\000\050\000S\000T\000-\000C\000O\000U\000N\000T\000-\0000\0001\000\051}{}% 75
|
||||
\BOOKMARK [1][-]{section.11.1}{\376\377\000Q\000u\000\351\000\040\000e\000v\000a\000l\000\372\000a\000\040\000/\000\040\000r\000e\000s\000u\000l\000t\000a\000d\000o\000\040\000/\000\040\000s\000i\000g\000n\000i\000f\000i\000c\000a\000d\000o}{chapter.11}% 76
|
||||
\BOOKMARK [1][-]{section.11.2}{\376\377\000C\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{chapter.11}% 77
|
||||
\BOOKMARK [1][-]{section.11.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s}{chapter.11}% 78
|
||||
\BOOKMARK [1][-]{section.11.4}{\376\377\000L\000e\000c\000t\000u\000r\000a}{chapter.11}% 79
|
||||
\BOOKMARK [0][-]{chapter.12}{\376\377\000A\000n\000\341\000l\000i\000s\000i\000s\000\040\000y\000\040\000h\000a\000l\000l\000a\000z\000g\000o\000s}{}% 80
|
||||
\BOOKMARK [1][-]{section.12.1}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.12}% 81
|
||||
\BOOKMARK [1][-]{section.12.2}{\376\377\000S\000c\000o\000r\000e\000c\000a\000r\000d\000\040\000g\000l\000o\000b\000a\000l}{chapter.12}% 82
|
||||
\BOOKMARK [1][-]{section.12.3}{\376\377\000L\000i\000m\000i\000t\000a\000c\000i\000o\000n\000e\000s}{chapter.12}% 83
|
||||
\BOOKMARK [0][-]{chapter.13}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s\000\040\000y\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{}% 84
|
||||
\BOOKMARK [1][-]{section.13.1}{\376\377\000C\000o\000n\000c\000l\000u\000s\000i\000o\000n\000e\000s}{chapter.13}% 85
|
||||
\BOOKMARK [1][-]{section.13.2}{\376\377\000R\000e\000c\000o\000m\000e\000n\000d\000a\000c\000i\000o\000n\000e\000s}{chapter.13}% 86
|
||||
\BOOKMARK [1][-]{section.13.3}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000r\000e\000l\000a\000c\000i\000o\000n\000a\000d\000o\000s}{chapter.13}% 87
|
||||
\BOOKMARK [0][-]{chapter.14}{\376\377\000A\000n\000e\000x\000o\000s}{}% 88
|
||||
\BOOKMARK [1][-]{section.14.1}{\376\377\000D\000a\000t\000o\000s\000\040\000f\000u\000e\000n\000t\000e}{chapter.14}% 89
|
||||
\BOOKMARK [1][-]{section.14.2}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000G\000W\000-\0000\0001}{chapter.14}% 90
|
||||
\BOOKMARK [1][-]{section.14.3}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\000\043\0002}{chapter.14}% 91
|
||||
\BOOKMARK [1][-]{section.14.4}{\376\377\000U\000n\000i\000t\000a\000r\000i\000a\000s}{chapter.14}% 92
|
||||
\BOOKMARK [1][-]{section.14.5}{\376\377\000S\000c\000r\000i\000p\000t\000s}{chapter.14}% 93
|
||||
\BOOKMARK [1][-]{section.14.6}{\376\377\000E\000x\000t\000r\000a\000c\000t\000o\000\040\000S\000T\000-\000E\000X\000T\000-\0000\0001}{chapter.14}% 94
|
||||
\BOOKMARK [1][-]{section.14.7}{\376\377\000D\000o\000c\000u\000m\000e\000n\000t\000o\000s\000\040\000h\000e\000r\000m\000a\000n\000o\000s}{chapter.14}% 95
|
||||
\BOOKMARK [1][-]{section.14.8}{\376\377\000R\000e\000p\000r\000o\000d\000u\000c\000i\000b\000i\000l\000i\000d\000a\000d}{chapter.14}% 96
|
||||
|
||||
@@ -6,33 +6,22 @@
|
||||
|
||||
\makecover
|
||||
|
||||
\chapter*{Control de cambios}
|
||||
\begin{tabularx}{\textwidth}{@{}l l X@{}}
|
||||
\toprule
|
||||
\textbf{Versi\'on} & \textbf{Fecha} & \textbf{Descripci\'on} \\
|
||||
\midrule
|
||||
1.0.0 & 13 jul 2026 & Informe profesional: campa\~na shared8g, m\'etricas, capacidad 100k/mes. \\
|
||||
1.1.0 & 13 jul 2026 & Unitarias count-api pipeline 3/3 PASS (Ubuntu). \\
|
||||
1.2.0 & 13 jul 2026 & Unitarias features/clustering ampliadas; timings PDF + scripts latencia. \\
|
||||
1.3.0 & 13 jul 2026 & Latencia HTTP 3 PDFs reales (2{,}2--4{,}5\,s); fix path Timing test. \\
|
||||
1.4.0 & 13 jul 2026 & Cap.\ count-api: explicaci\'on test a test, ejemplos y gr\'aficas. \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
|
||||
\tableofcontents
|
||||
\clearpage
|
||||
|
||||
\input{sections/01_resumen_ejecutivo}
|
||||
\input{sections/02_objeto_alcance}
|
||||
\input{sections/02_arquitectura_contexto}
|
||||
\input{sections/03_entorno}
|
||||
\input{sections/04_metodologia}
|
||||
\input{sections/04_como_leer}
|
||||
\input{sections/05_resultados_unitarias}
|
||||
\input{sections/06_resultados_humo_funcional}
|
||||
\input{sections/07_resultados_carga}
|
||||
\input{sections/08_capacidad_100k}
|
||||
\input{sections/12_count_latency}
|
||||
\input{sections/13_llm_latency}
|
||||
\input{sections/14_arq_ext01}
|
||||
\input{sections/15_st_count_01}
|
||||
\input{sections/09_analisis_hallazgos}
|
||||
\input{sections/10_conclusiones}
|
||||
\input{sections/11_anexos}
|
||||
\input{sections/12_count_latency}
|
||||
|
||||
\end{document}
|
||||
|
||||
@@ -1,77 +1,97 @@
|
||||
\babel@toc {spanish}{}\relax
|
||||
\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{5}{chapter.1}%
|
||||
\contentsline {section}{\numberline {1.1}Veredicto global}{5}{section.1.1}%
|
||||
\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{5}{section.1.2}%
|
||||
\contentsline {section}{\numberline {1.3}Respuesta directa: 100\,000 facturas/mes}{5}{section.1.3}%
|
||||
\contentsline {section}{\numberline {1.4}Riesgo principal}{6}{section.1.4}%
|
||||
\contentsline {chapter}{\numberline {2}Objeto y alcance}{7}{chapter.2}%
|
||||
\contentsline {section}{\numberline {2.1}Objeto}{7}{section.2.1}%
|
||||
\contentsline {section}{\numberline {2.2}Alcance incluido}{7}{section.2.2}%
|
||||
\contentsline {section}{\numberline {2.3}Fuera de alcance de esta campa\~na}{7}{section.2.3}%
|
||||
\contentsline {section}{\numberline {2.4}Definici\'on de factura}{7}{section.2.4}%
|
||||
\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{8}{chapter.3}%
|
||||
\contentsline {section}{\numberline {3.1}Hardware y sistema}{8}{section.3.1}%
|
||||
\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{8}{section.3.2}%
|
||||
\contentsline {section}{\numberline {3.3}Salud del stack}{8}{section.3.3}%
|
||||
\contentsline {chapter}{\numberline {4}Metodolog\'ia}{10}{chapter.4}%
|
||||
\contentsline {section}{\numberline {4.1}Tipos de prueba}{10}{section.4.1}%
|
||||
\contentsline {section}{\numberline {4.2}Herramientas}{10}{section.4.2}%
|
||||
\contentsline {section}{\numberline {4.3}SLOs perfil \texttt {shared8g}}{10}{section.4.3}%
|
||||
\contentsline {section}{\numberline {4.4}Comandos representativos}{10}{section.4.4}%
|
||||
\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{11}{chapter.5}%
|
||||
\contentsline {section}{\numberline {5.1}Scorecard}{11}{section.5.1}%
|
||||
\contentsline {section}{\numberline {5.2}Cobertura agent}{11}{section.5.2}%
|
||||
\contentsline {section}{\numberline {5.3}Cobertura API store}{11}{section.5.3}%
|
||||
\contentsline {section}{\numberline {5.4}Cobertura count-api}{11}{section.5.4}%
|
||||
\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{13}{chapter.6}%
|
||||
\contentsline {section}{\numberline {6.1}Humo gateway}{13}{section.6.1}%
|
||||
\contentsline {section}{\numberline {6.2}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{13}{section.6.2}%
|
||||
\contentsline {section}{\numberline {6.3}Script preparado}{13}{section.6.3}%
|
||||
\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{14}{chapter.7}%
|
||||
\contentsline {section}{\numberline {7.1}Tabla consolidada}{14}{section.7.1}%
|
||||
\contentsline {section}{\numberline {7.2}ST-GW-01}{14}{section.7.2}%
|
||||
\contentsline {section}{\numberline {7.3}ST-BOT-01 --- comparativa}{14}{section.7.3}%
|
||||
\contentsline {section}{\numberline {7.4}Throughput gateway}{14}{section.7.4}%
|
||||
\contentsline {chapter}{\numberline {8}Capacidad: 100\,000 facturas al mes}{16}{chapter.8}%
|
||||
\contentsline {section}{\numberline {8.1}Traducci\'on del objetivo}{16}{section.8.1}%
|
||||
\contentsline {section}{\numberline {8.2}Modelo por etapa}{16}{section.8.2}%
|
||||
\contentsline {section}{\numberline {8.3}Interpretaci\'on}{16}{section.8.3}%
|
||||
\contentsline {section}{\numberline {8.4}Veredicto}{17}{section.8.4}%
|
||||
\contentsline {section}{\numberline {8.5}Requisitos m\'inimos sugeridos para 100k/mes}{17}{section.8.5}%
|
||||
\contentsline {chapter}{\numberline {9}An\'alisis y hallazgos}{18}{chapter.9}%
|
||||
\contentsline {section}{\numberline {9.1}Hallazgos}{18}{section.9.1}%
|
||||
\contentsline {section}{\numberline {9.2}Scorecard global}{18}{section.9.2}%
|
||||
\contentsline {section}{\numberline {9.3}Limitaciones}{18}{section.9.3}%
|
||||
\contentsline {chapter}{\numberline {10}Conclusiones y recomendaciones}{19}{chapter.10}%
|
||||
\contentsline {section}{\numberline {10.1}Conclusiones}{19}{section.10.1}%
|
||||
\contentsline {section}{\numberline {10.2}Recomendaciones}{19}{section.10.2}%
|
||||
\contentsline {section}{\numberline {10.3}Pr\'oxima campa\~na sugerida}{19}{section.10.3}%
|
||||
\contentsline {chapter}{\numberline {11}Anexos}{20}{chapter.11}%
|
||||
\contentsline {section}{\numberline {11.1}Datos fuente}{20}{section.11.1}%
|
||||
\contentsline {section}{\numberline {11.2}Extracto ST-GW-01}{20}{section.11.2}%
|
||||
\contentsline {section}{\numberline {11.3}Extracto ST-BOT-01 \#2}{20}{section.11.3}%
|
||||
\contentsline {section}{\numberline {11.4}Unitarias}{20}{section.11.4}%
|
||||
\contentsline {section}{\numberline {11.5}Scripts}{20}{section.11.5}%
|
||||
\contentsline {section}{\numberline {11.6}Reproducibilidad}{21}{section.11.6}%
|
||||
\contentsline {chapter}{\numberline {12}Count-api: qu\'e eval\'ua cada prueba}{22}{chapter.12}%
|
||||
\contentsline {section}{\numberline {12.1}Pipeline bajo prueba}{22}{section.12.1}%
|
||||
\contentsline {section}{\numberline {12.2}Resultado global count-api}{22}{section.12.2}%
|
||||
\contentsline {section}{\numberline {12.3}Features: qu\'e se eval\'ua (con ejemplo)}{23}{section.12.3}%
|
||||
\contentsline {subsection}{\numberline {12.3.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.1}%
|
||||
\contentsline {subsection}{\numberline {12.3.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.2}%
|
||||
\contentsline {subsection}{\numberline {12.3.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.3}%
|
||||
\contentsline {subsection}{\numberline {12.3.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.4}%
|
||||
\contentsline {subsection}{\numberline {12.3.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.5}%
|
||||
\contentsline {subsection}{\numberline {12.3.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.6}%
|
||||
\contentsline {subsection}{\numberline {12.3.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{23}{subsection.12.3.7}%
|
||||
\contentsline {section}{\numberline {12.4}Clustering: qu\'e se eval\'ua (con ejemplo)}{24}{section.12.4}%
|
||||
\contentsline {subsection}{\numberline {12.4.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.1}%
|
||||
\contentsline {subsection}{\numberline {12.4.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.2}%
|
||||
\contentsline {subsection}{\numberline {12.4.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.3}%
|
||||
\contentsline {subsection}{\numberline {12.4.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{24}{subsection.12.4.4}%
|
||||
\contentsline {section}{\numberline {12.5}Pruebas con PDFs reales}{24}{section.12.5}%
|
||||
\contentsline {subsection}{\numberline {12.5.1}Fixtures}{24}{subsection.12.5.1}%
|
||||
\contentsline {subsection}{\numberline {12.5.2}TestProcessPDFTimingFixtures}{25}{subsection.12.5.2}%
|
||||
\contentsline {subsection}{\numberline {12.5.3}Script HTTP \texttt {measure\_count\_latency}}{25}{subsection.12.5.3}%
|
||||
\contentsline {section}{\numberline {12.6}Resultados de latencia HTTP (medidos)}{25}{section.12.6}%
|
||||
\contentsline {section}{\numberline {12.7}C\'omo reproducir}{25}{section.12.7}%
|
||||
\contentsline {chapter}{\numberline {1}Resumen ejecutivo}{4}{chapter.1}%
|
||||
\contentsline {section}{\numberline {1.1}Veredicto global}{4}{section.1.1}%
|
||||
\contentsline {section}{\numberline {1.2}KPIs de la campa\~na}{4}{section.1.2}%
|
||||
\contentsline {section}{\numberline {1.3}Capacidad 100k/mes (resumen)}{5}{section.1.3}%
|
||||
\contentsline {section}{\numberline {1.4}Riesgo principal}{5}{section.1.4}%
|
||||
\contentsline {section}{\numberline {1.5}Alcance de esta campa\~na}{5}{section.1.5}%
|
||||
\contentsline {chapter}{\numberline {2}Arquitectura y patrones (contexto)}{6}{chapter.2}%
|
||||
\contentsline {section}{\numberline {2.1}Vista de componentes}{6}{section.2.1}%
|
||||
\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}%
|
||||
\contentsline {section}{\numberline {2.3}Pipeline de negocio}{7}{section.2.3}%
|
||||
\contentsline {section}{\numberline {2.4}Definici\'on de factura}{8}{section.2.4}%
|
||||
\contentsline {chapter}{\numberline {3}Entorno bajo prueba}{9}{chapter.3}%
|
||||
\contentsline {section}{\numberline {3.1}Hardware y sistema}{9}{section.3.1}%
|
||||
\contentsline {section}{\numberline {3.2}Baseline de memoria (2026-07-13)}{9}{section.3.2}%
|
||||
\contentsline {section}{\numberline {3.3}Salud del stack}{9}{section.3.3}%
|
||||
\contentsline {chapter}{\numberline {4}C\'omo leer este informe}{11}{chapter.4}%
|
||||
\contentsline {section}{\numberline {4.1}Plantilla de interpretaci\'on}{11}{section.4.1}%
|
||||
\contentsline {section}{\numberline {4.2}Leyenda de estados}{11}{section.4.2}%
|
||||
\contentsline {section}{\numberline {4.3}Perfil de carga \texttt {shared8g}}{11}{section.4.3}%
|
||||
\contentsline {section}{\numberline {4.4}Herramientas}{11}{section.4.4}%
|
||||
\contentsline {section}{\numberline {4.5}Mapa de cap\'itulos de resultados}{12}{section.4.5}%
|
||||
\contentsline {chapter}{\numberline {5}Resultados --- pruebas unitarias}{13}{chapter.5}%
|
||||
\contentsline {section}{\numberline {5.1}Qu\'e eval\'uan}{13}{section.5.1}%
|
||||
\contentsline {section}{\numberline {5.2}Scorecard}{13}{section.5.2}%
|
||||
\contentsline {section}{\numberline {5.3}Cobertura agent Selenium}{13}{section.5.3}%
|
||||
\contentsline {section}{\numberline {5.4}Cobertura API \texttt {bot-descarga} (orquestador)}{14}{section.5.4}%
|
||||
\contentsline {section}{\numberline {5.5}Cobertura count-api}{14}{section.5.5}%
|
||||
\contentsline {section}{\numberline {5.6}Cobertura desktop (Vitest)}{14}{section.5.6}%
|
||||
\contentsline {chapter}{\numberline {6}Resultados --- humo y funcional}{15}{chapter.6}%
|
||||
\contentsline {section}{\numberline {6.1}Qu\'e eval\'uan}{15}{section.6.1}%
|
||||
\contentsline {section}{\numberline {6.2}Humo gateway}{15}{section.6.2}%
|
||||
\contentsline {section}{\numberline {6.3}Funcional E2E-07 (worker offline $\rightarrow $ 409)}{15}{section.6.3}%
|
||||
\contentsline {section}{\numberline {6.4}Scripts}{16}{section.6.4}%
|
||||
\contentsline {chapter}{\numberline {7}Resultados --- pruebas de carga}{17}{chapter.7}%
|
||||
\contentsline {section}{\numberline {7.1}Qu\'e eval\'uan}{17}{section.7.1}%
|
||||
\contentsline {section}{\numberline {7.2}Tabla consolidada}{17}{section.7.2}%
|
||||
\contentsline {section}{\numberline {7.3}ST-GW-01}{17}{section.7.3}%
|
||||
\contentsline {section}{\numberline {7.4}ST-BOT-01 --- comparativa}{18}{section.7.4}%
|
||||
\contentsline {section}{\numberline {7.5}Throughput gateway}{19}{section.7.5}%
|
||||
\contentsline {chapter}{\numberline {8}Count-api: qu\'e eval\'ua cada prueba}{20}{chapter.8}%
|
||||
\contentsline {section}{\numberline {8.1}Qu\'e eval\'ua / resultado / significado}{20}{section.8.1}%
|
||||
\contentsline {section}{\numberline {8.2}Pipeline bajo prueba}{20}{section.8.2}%
|
||||
\contentsline {section}{\numberline {8.3}Resultado global count-api}{21}{section.8.3}%
|
||||
\contentsline {section}{\numberline {8.4}Features: qu\'e se eval\'ua (con ejemplo)}{21}{section.8.4}%
|
||||
\contentsline {subsection}{\numberline {8.4.1}TestExtractRowFeaturesServiceTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.1}%
|
||||
\contentsline {subsection}{\numberline {8.4.2}TestBuildFeaturesContinuation --- \textcolor {passgreen}{\textbf {PASS}}}{21}{subsection.8.4.2}%
|
||||
\contentsline {subsection}{\numberline {8.4.3}TestExtractRowFeaturesHeaderKeyword --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.3}%
|
||||
\contentsline {subsection}{\numberline {8.4.4}TestExtractRowFeaturesServiceCode --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.4}%
|
||||
\contentsline {subsection}{\numberline {8.4.5}TestExtractRowFeaturesNoTablePattern --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.5}%
|
||||
\contentsline {subsection}{\numberline {8.4.6}TestBuildFeatureRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.6}%
|
||||
\contentsline {subsection}{\numberline {8.4.7}TestFeatureCount --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.4.7}%
|
||||
\contentsline {section}{\numberline {8.5}Clustering: qu\'e se eval\'ua (con ejemplo)}{22}{section.8.5}%
|
||||
\contentsline {subsection}{\numberline {8.5.1}TestClusterRowsTwoBands --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.1}%
|
||||
\contentsline {subsection}{\numberline {8.5.2}TestClusterRowsSameBand --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.2}%
|
||||
\contentsline {subsection}{\numberline {8.5.3}TestClusterRowsEmpty --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.3}%
|
||||
\contentsline {subsection}{\numberline {8.5.4}TestClusterRowsNoiseMinSamples --- \textcolor {passgreen}{\textbf {PASS}}}{22}{subsection.8.5.4}%
|
||||
\contentsline {section}{\numberline {8.6}Pruebas con PDFs reales}{23}{section.8.6}%
|
||||
\contentsline {subsection}{\numberline {8.6.1}Fixtures}{23}{subsection.8.6.1}%
|
||||
\contentsline {subsection}{\numberline {8.6.2}TestProcessPDFTimingFixtures}{23}{subsection.8.6.2}%
|
||||
\contentsline {subsection}{\numberline {8.6.3}Script HTTP \texttt {measure\_count\_latency}}{23}{subsection.8.6.3}%
|
||||
\contentsline {section}{\numberline {8.7}Resultados agregados (23 PDFs)}{23}{section.8.7}%
|
||||
\contentsline {section}{\numberline {8.8}Resultados de latencia HTTP}{24}{section.8.8}%
|
||||
\contentsline {section}{\numberline {8.9}Detalle por archivo}{25}{section.8.9}%
|
||||
\contentsline {section}{\numberline {8.10}C\'omo reproducir}{25}{section.8.10}%
|
||||
\contentsline {chapter}{\numberline {9}Extracci\'on LLM: bench de latencia (ST-EXT-02)}{26}{chapter.9}%
|
||||
\contentsline {section}{\numberline {9.1}Qu\'e eval\'ua / resultado / significado}{26}{section.9.1}%
|
||||
\contentsline {section}{\numberline {9.2}C\'omo ejecutar}{26}{section.9.2}%
|
||||
\contentsline {section}{\numberline {9.3}Resultados (2026-07-17)}{27}{section.9.3}%
|
||||
\contentsline {section}{\numberline {9.4}Lectura para capacidad}{27}{section.9.4}%
|
||||
\contentsline {chapter}{\numberline {10}Cola ARQ: bench ST-EXT-01 (LLM mock)}{28}{chapter.10}%
|
||||
\contentsline {section}{\numberline {10.1}Qu\'e eval\'ua / resultado / significado}{28}{section.10.1}%
|
||||
\contentsline {section}{\numberline {10.2}Condiciones de la corrida}{28}{section.10.2}%
|
||||
\contentsline {section}{\numberline {10.3}Resultados}{29}{section.10.3}%
|
||||
\contentsline {section}{\numberline {10.4}Lectura}{29}{section.10.4}%
|
||||
\contentsline {chapter}{\numberline {11}Count-api bajo carga (ST-COUNT-01)}{30}{chapter.11}%
|
||||
\contentsline {section}{\numberline {11.1}Qu\'e eval\'ua / resultado / significado}{30}{section.11.1}%
|
||||
\contentsline {section}{\numberline {11.2}Condiciones}{30}{section.11.2}%
|
||||
\contentsline {section}{\numberline {11.3}Resultados}{30}{section.11.3}%
|
||||
\contentsline {section}{\numberline {11.4}Lectura}{30}{section.11.4}%
|
||||
\contentsline {chapter}{\numberline {12}An\'alisis y hallazgos}{32}{chapter.12}%
|
||||
\contentsline {section}{\numberline {12.1}Hallazgos}{32}{section.12.1}%
|
||||
\contentsline {section}{\numberline {12.2}Scorecard global}{32}{section.12.2}%
|
||||
\contentsline {section}{\numberline {12.3}Limitaciones}{32}{section.12.3}%
|
||||
\contentsline {chapter}{\numberline {13}Conclusiones y recomendaciones}{34}{chapter.13}%
|
||||
\contentsline {section}{\numberline {13.1}Conclusiones}{34}{section.13.1}%
|
||||
\contentsline {section}{\numberline {13.2}Recomendaciones}{34}{section.13.2}%
|
||||
\contentsline {section}{\numberline {13.3}Documentos relacionados}{34}{section.13.3}%
|
||||
\contentsline {chapter}{\numberline {14}Anexos}{35}{chapter.14}%
|
||||
\contentsline {section}{\numberline {14.1}Datos fuente}{35}{section.14.1}%
|
||||
\contentsline {section}{\numberline {14.2}Extracto ST-GW-01}{35}{section.14.2}%
|
||||
\contentsline {section}{\numberline {14.3}Extracto ST-BOT-01 \#2}{35}{section.14.3}%
|
||||
\contentsline {section}{\numberline {14.4}Unitarias}{35}{section.14.4}%
|
||||
\contentsline {section}{\numberline {14.5}Scripts}{35}{section.14.5}%
|
||||
\contentsline {section}{\numberline {14.6}Extracto ST-EXT-01}{36}{section.14.6}%
|
||||
\contentsline {section}{\numberline {14.7}Documentos hermanos}{36}{section.14.7}%
|
||||
\contentsline {section}{\numberline {14.8}Reproducibilidad}{36}{section.14.8}%
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
\fancyhf{}
|
||||
\fancyhead[L]{\small IPSExtractor --- Informe de Pruebas}
|
||||
\fancyhead[R]{\small\thepage}
|
||||
\fancyfoot[C]{\footnotesize Confidencial --- Uso interno}
|
||||
\fancyfoot[C]{\footnotesize Documento para compartir}
|
||||
\renewcommand{\headrulewidth}{0.4pt}
|
||||
|
||||
\usepackage{titlesec}
|
||||
@@ -58,9 +58,10 @@
|
||||
\usepgfplotslibrary{statistics}
|
||||
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{arrows.meta,positioning}
|
||||
\usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric}
|
||||
|
||||
\usepackage{graphicx}
|
||||
\usepackage{float}
|
||||
\usepackage{hyperref}
|
||||
\hypersetup{
|
||||
colorlinks=true,
|
||||
@@ -70,14 +71,19 @@
|
||||
pdfauthor={IPSExtractor QA}
|
||||
}
|
||||
|
||||
\newcommand{\docversion}{1.4.0}
|
||||
\newcommand{\docdate}{13 de julio de 2026}
|
||||
\newcommand{\docdate}{17 de julio de 2026}
|
||||
|
||||
\newcommand{\pass}{\textcolor{passgreen}{\textbf{PASS}}}
|
||||
\newcommand{\fail}{\textcolor{failred}{\textbf{FAIL}}}
|
||||
\newcommand{\parcial}{\textcolor{warnorange}{\textbf{PARCIAL}}}
|
||||
\newcommand{\neje}{\textcolor{ipsgrey}{\textbf{N/E}}}
|
||||
|
||||
\newcommand{\callout}[2]{%
|
||||
\begin{center}
|
||||
\fbox{\parbox{0.92\textwidth}{\centering #1\\[0.3em]\normalsize #2}}
|
||||
\end{center}
|
||||
}
|
||||
|
||||
\newcommand{\makecover}{%
|
||||
\begin{titlepage}
|
||||
\centering
|
||||
@@ -87,16 +93,16 @@
|
||||
{\Large\color{ipsgrey} Extracci\'on y an\'alisis de facturas m\'edicas}\\[0.35cm]
|
||||
{\color{ipsblue}\rule{\textwidth}{2.5pt}}\\[1.5cm]
|
||||
{\LARGE\bfseries Informe de Pruebas}\\[0.25cm]
|
||||
{\large Resultados $\cdot$ M\'etricas $\cdot$ Capacidad 100\,000/mes}\\[1.8cm]
|
||||
{\large Resultados $\cdot$ M\'etricas $\cdot$ Interpretaci\'on}\\[1.8cm]
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\textbf{Versi\'on} & \docversion \\
|
||||
\textbf{Fecha} & \docdate \\
|
||||
\textbf{Campa\~na} & 2026-07-13 (perfil \texttt{shared8g}) \\
|
||||
\textbf{Campa\~na carga} & 13 de julio de 2026 (perfil \texttt{shared8g}) \\
|
||||
\textbf{Bench LLM} & 17 de julio de 2026 (ST-EXT-02) \\
|
||||
\textbf{Host} & ubuntu --- ASUS X441UAK, 8\,GiB \\
|
||||
\textbf{Clasificaci\'on} & Uso interno \\
|
||||
\textbf{Audiencia} & Documento para compartir \\
|
||||
\end{tabular}
|
||||
\vfill
|
||||
{\small Documento de \emph{resultados} (no plan). Incluye an\'alisis de viabilidad de 100\,000 facturas/mes.}\\[0.25cm]
|
||||
{\small Resultados de pruebas. El an\'alisis de 100\,000 facturas/mes est\'a en el PDF \emph{Capacidad 100k}.}\\[0.25cm]
|
||||
{\color{ipsblue}\rule{0.45\textwidth}{0.6pt}}
|
||||
\end{titlepage}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
\label{ch:resumen}
|
||||
|
||||
\section{Veredicto global}
|
||||
La campa\~na de pruebas del \textbf{13 de julio de 2026} sobre el stack IPSExtractor desplegado en el host \texttt{ubuntu} (perfil \texttt{shared8g}) concluye:
|
||||
La campa\~na de pruebas (carga 13-jul-2026 + LLM/ARQ 17-jul-2026) sobre el stack
|
||||
IPSExtractor en el host \texttt{ubuntu} (perfil \texttt{shared8g}) concluye:
|
||||
|
||||
\begin{center}
|
||||
\large
|
||||
Stack \textbf{operativo y estable} para orquestaci\'on y control remoto del bot.\\[0.3em]
|
||||
\normalsize
|
||||
Carga ligera del gateway y heartbeats: \pass\ bajo contenci\'on moderada.\\[0.2em]
|
||||
Pipeline completo a \textbf{100\,000 facturas/mes} en este host: \fail\ (ver cap.~\ref{ch:capacidad}).
|
||||
\end{center}
|
||||
\callout{\large Stack operativo: orquestador y agent}{%
|
||||
Gateway y heartbeats del orquestador \texttt{bot-descarga} (ST-BOT-*): \pass\ bajo contenci\'on moderada.\\
|
||||
Unitarias agent Selenium / API \texttt{bot-descarga}: \pass.\\
|
||||
Vitest desktop: \pass\ (25/25).\\
|
||||
LLM ST-EXT-02 (23 PDFs): \pass\ --- p50 7{,}1\,s $\approx$506 fact/h.\\
|
||||
Cola ARQ ST-EXT-01 (30 docs, mock): \pass\ --- drenado en 22{,}5\,s.\\
|
||||
OCR ST-COUNT-01 (9 PDFs, 3 conc.): \pass\ --- p50 35{,}8\,s; $\approx$325 fact/h obs.\\
|
||||
E2E-07 (agent offline $\rightarrow$ 409): \pass.\\[0.3em]
|
||||
\textbf{100\,000 facturas/mes} en este host: \fail\ del pipeline completo\\
|
||||
(detalle en el documento \emph{Capacidad 100k/mes}).
|
||||
}
|
||||
|
||||
\section{KPIs de la campa\~na}
|
||||
\begin{table}[ht]
|
||||
@@ -22,20 +27,35 @@ Pipeline completo a \textbf{100\,000 facturas/mes} en este host: \fail\ (ver cap
|
||||
ST-GW-01 p95 & 40{,}85\,ms & $<$500\,ms & \pass \\
|
||||
ST-GW-01 fail rate & 0\,\% & $<$1\,\% & \pass \\
|
||||
ST-BOT-01 p95 (reintento) & 85{,}55\,ms & $<$800\,ms & \pass \\
|
||||
Unitarias agent bot & 19/19 & 100\,\% & \pass \\
|
||||
Unitarias API bot store & 7/7 & 100\,\% & \pass \\
|
||||
Unitarias agent Selenium & 19/19 & 100\,\% & \pass \\
|
||||
Unitarias API \texttt{bot-descarga} (orquestador) & 7/7 & 100\,\% & \pass \\
|
||||
Unitarias count-api (pipeline) & 3/3 & 100\,\% & \pass \\
|
||||
ST-EXT-02 LLM p50 & 7{,}114\,s & --- & $\approx$506 fact/h \\
|
||||
ST-EXT-01 ARQ mock & 30/30 drain & --- & 22{,}5\,s; $\approx$4\,5k docs/h cola \\
|
||||
ST-COUNT-01 OCR & p50 35{,}8\,s & --- & 9/9; $\approx$325 fact/h obs. \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{KPIs ejecutivos de la campa\~na.}
|
||||
\caption{KPIs (carga 13-jul; LLM/ARQ/OCR 17-jul).}
|
||||
\end{table}
|
||||
|
||||
\section{Respuesta directa: 100\,000 facturas/mes}
|
||||
\section{Capacidad 100k/mes (resumen)}
|
||||
\begin{description}
|
||||
\item[Host actual (8\,GiB + MLflow/n8n/MinIO/\ldots)] \fail\ para el \emph{pipeline completo} (conteo + extracci\'on + bot + limpieza).
|
||||
\item[Infra dedicada dimensionada] \pass\ \emph{condicional}: workers LLM, OCR con RAM $\geq$16--32\,GiB, $N$ agents Selenium, cuota API, sin zoo Docker competidor.
|
||||
\item[Host actual] \parcial\ / \fail\ --- pipeline $\approx$ OCR $\sim$100--325/h; 139/h no garantizado.
|
||||
\item[Infra dedicada] \pass\ condicional (OCR, workers, $N$ agents, cuota LLM).
|
||||
\item[Coste LLM] $\approx$ USD\,2\,570/mes si 100k pasan a extracci\'on (gpt-5.4 mini).
|
||||
\end{description}
|
||||
Detalle cuantitativo en el cap\'itulo~\ref{ch:capacidad}.
|
||||
El modelo por etapa, gr\'aficas y requisitos est\'an en el PDF
|
||||
\textbf{Capacidad100k\_IPSExtractor.pdf} (carpeta \texttt{docs/plan\_pruebas/capacidad\_100k/}).
|
||||
|
||||
\section{Riesgo principal}
|
||||
Contenci\'on de plataforma: swap $\approx$2{,}1\,GiB y \texttt{mlflow-server} $\approx$1{,}84\,GiB provocaron un outlier de 23\,s en heartbeats (corrida 1). Tras aliviar carga, p95 cay\'o a 86\,ms. Los fallos de latencia deben correlacionarse siempre con \texttt{free -h} / \texttt{docker stats}.
|
||||
Contenci\'on de plataforma: swap $\approx$2{,}1\,GiB y \texttt{mlflow-server} $\approx$1{,}84\,GiB
|
||||
provocaron un outlier de 23\,s en heartbeats (corrida 1). Tras aliviar carga, p95 cay\'o a 86\,ms.
|
||||
Correlacionar siempre latencias con \texttt{free -h} / \texttt{docker stats}.
|
||||
|
||||
\section{Alcance de esta campa\~na}
|
||||
\textbf{Incluido:} stack Docker IPS, perfil shared8g, unitarias agent Selenium / orquestador \texttt{bot-descarga} / count / Vitest desktop (25/25),
|
||||
humo gateway, E2E-07 (worker offline $\rightarrow$ 409), k6 ST-GW-01/ST-BOT-01 (orquestador),
|
||||
bench LLM ST-EXT-02 (23 PDFs), cola ARQ ST-EXT-01 (30 docs, mock),
|
||||
ST-COUNT-01 (9 PDFs reales, 3 concurrentes).\\
|
||||
\textbf{Fuera de alcance:} ST-COUNT-01 lab agresivo, ST-EXT-01 lab (100--500), E2E Imaging/VPN,
|
||||
ST-BOT-03 events, procesar 100k PDFs reales.
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
\chapter{Arquitectura y patrones (contexto)}
|
||||
\label{ch:arq-informe}
|
||||
|
||||
Este cap\'itulo resume c\'omo est\'a construido IPSExtractor para que los resultados
|
||||
de las pruebas tengan sentido sin leer el plan completo.
|
||||
|
||||
\callout{\textbf{Nombres (importante)}}{%
|
||||
\textbf{bot-descarga} = servicio de orquestaci\'on (API Docker): jobs, claim, heartbeats.\\
|
||||
\textbf{Agent Selenium} = programa en el PC que descarga PDFs.\\
|
||||
ST-BOT-* mide el orquestador; $\sim$422 fact/h es capacidad del agent.
|
||||
}
|
||||
|
||||
\section{Vista de componentes}
|
||||
El host solo expone el Gateway en \textbf{:8189}. El resto de APIs est\'a en la red Docker.
|
||||
Los agents Selenium corren en PCs externas.
|
||||
|
||||
\input{../diagrams/arquitectura}
|
||||
|
||||
\section{Patrones de dise\~no}
|
||||
\begin{description}[style=nextline,leftmargin=0.5cm]
|
||||
\item[API Gateway]
|
||||
Puerta \'unica. Las pruebas ST-GW-* miden latencia/errores del proxy, no del OCR ni del LLM.
|
||||
\item[Microservicios]
|
||||
count-api (OCR), extraction+workers (LLM), \texttt{bot-descarga} (orquestador), cleaning (CSV).
|
||||
Cada uno tiene su propio cuello de botella.
|
||||
\item[Cola ARQ]
|
||||
Con \texttt{ASYNC\_QUEUE=true} el ingest encola y los workers drenan.
|
||||
El bench LLM de esta campa\~na corri\'o en modo sync (respuesta en el mismo request).
|
||||
\item[Claim del orquestador (\texttt{bot-descarga})]
|
||||
Un job pending lo toma un solo agent Selenium (claim at\'omico). Heartbeats marcan al worker online.
|
||||
\end{description}
|
||||
|
||||
\input{../diagrams/patrones}
|
||||
|
||||
\section{Pipeline de negocio}
|
||||
\input{../diagrams/pipeline_negocio}
|
||||
|
||||
Cuando este informe habla de ``capacidad'', se refiere a cu\'antas facturas/hora
|
||||
puede sostener \textbf{cada etapa} y, en el documento de Capacidad 100k, el pipeline completo.
|
||||
|
||||
\section{Definici\'on de factura}
|
||||
Una \textbf{factura} = un PDF de cuenta m\'edica evaluado en una etapa
|
||||
(conteo OCR, extracci\'on LLM, descarga agent Selenium o limpieza).
|
||||
@@ -8,17 +8,18 @@ Documentar de forma profesional los \textbf{resultados} de pruebas de IPSExtract
|
||||
\begin{itemize}
|
||||
\item Stack Docker IPS: gateway, extraction, workers, cleaning, count-api, bot-descarga, PostgreSQL, Redis.
|
||||
\item Perfil de carga \texttt{shared8g} en host de staging compartido.
|
||||
\item Unitarias del agent (\texttt{bots/bot\_descarga}) y del store API (\texttt{services/bot\_descarga}).
|
||||
\item Unitarias del agent Selenium (\texttt{bots/bot\_descarga}) y del orquestador API (\texttt{services/bot\_descarga}).
|
||||
\item Modelo anal\'itico de capacidad por etapa del pipeline.
|
||||
\item E2E-07: rechazo de job si el agent Selenium est\'a offline (HTTP 409).
|
||||
\end{itemize}
|
||||
|
||||
\section{Fuera de alcance de esta campa\~na}
|
||||
\begin{itemize}
|
||||
\item ST-COUNT-01 / OCR masivo y ST-EXT bajo LLM real a escala.
|
||||
\item E2E Imaging/Selenium con VPN.
|
||||
\item E2E Imaging/Selenium con VPN (E2E-05/08 a volumen).
|
||||
\item ST-BOT-03 (events) --- requiere \texttt{DOWNLOAD\_JOB\_ID} de un job real (\neje).
|
||||
\item Procesar 100\,000 PDFs reales.
|
||||
\end{itemize}
|
||||
|
||||
\section{Definici\'on de factura}
|
||||
Una \textbf{factura} = un PDF de cuenta m\'edica evaluado por etapa (conteo OCR, extracci\'on LLM, descarga bot, limpieza). El veredicto 100k/mes se expresa por etapa y para el pipeline completo.
|
||||
Una \textbf{factura} = un PDF de cuenta m\'edica evaluado por etapa (conteo OCR, extracci\'on LLM, descarga agent Selenium, limpieza). El veredicto 100k/mes se expresa por etapa y para el pipeline completo.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
\chapter{C\'omo leer este informe}
|
||||
\label{ch:como-leer}
|
||||
|
||||
\section{Plantilla de interpretaci\'on}
|
||||
Cada bloque de resultados sigue (expl\'icita o impl\'icitamente) cuatro preguntas:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.2cm}p{9.5cm}@{}}
|
||||
\toprule
|
||||
\textbf{Pregunta} & \textbf{Qu\'e responde} \\
|
||||
\midrule
|
||||
\textbf{Qu\'e eval\'ua} & Riesgo o propiedad del sistema bajo prueba \\
|
||||
\textbf{C\'omo se midi\'o} & Herramienta, perfil, comando o script \\
|
||||
\textbf{Resultado} & N\'umeros, PASS/FAIL/PARCIAL/N/E \\
|
||||
\textbf{Qu\'e significa} & Implicaci\'on pr\'actica (seguir, dimensionar o investigar) \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Leyenda de estados}
|
||||
\begin{itemize}
|
||||
\item \pass --- cumple el umbral o criterio acordado.
|
||||
\item \fail --- no cumple; requiere acci\'on o redespliegue de entorno.
|
||||
\item \parcial --- funcional OK pero latencia/SLO degradado (t\'ipico: contenci\'on host).
|
||||
\item \neje --- no ejecutado en esta campa\~na (falta dato, VPN, job id, etc.).
|
||||
\end{itemize}
|
||||
|
||||
\section{Perfil de carga \texttt{shared8g}}
|
||||
Host compartido 8\,GiB con MLflow, n8n, MinIO, etc. Los SLOs son m\'as permisivos
|
||||
que en laboratorio dedicado:
|
||||
|
||||
\begin{itemize}
|
||||
\item Gateway \texttt{/health/live}: p95 $<$500\,ms; fail $<$1\,\%.
|
||||
\item Heartbeats bot: p95 $<$800\,ms; checks $>$95\,\%.
|
||||
\item No usar \texttt{/health} agregado como SLO de latencia (puede superar 10\,s).
|
||||
\end{itemize}
|
||||
|
||||
\section{Herramientas}
|
||||
k6, pytest, go test, Docker stats, curl, scripts en \texttt{docs/plan\_pruebas/scripts/}.
|
||||
|
||||
\section{Mapa de cap\'itulos de resultados}
|
||||
\begin{enumerate}
|
||||
\item Unitarias --- correctitud de dominio (agent, store, count-api).
|
||||
\item Humo / funcional --- el stack responde.
|
||||
\item Carga k6 --- gateway y heartbeats bajo VUs.
|
||||
\item Count-api --- qu\'e prueba el OCR y latencias medidas.
|
||||
\item LLM ST-EXT-02 --- latencia real gpt-5.4-mini.
|
||||
\item Cola ARQ ST-EXT-01 --- enqueue + drenado con mock.
|
||||
\item ST-COUNT-01 --- OCR bajo carga concurrente.
|
||||
\item Hallazgos y conclusiones --- s\'intesis.
|
||||
\end{enumerate}
|
||||
@@ -1,6 +1,24 @@
|
||||
\chapter{Resultados --- pruebas unitarias}
|
||||
\label{ch:unitarias}
|
||||
|
||||
\callout{\textbf{Veredicto unitarias:} \pass}{%
|
||||
54 casos verdes: 19 agent Selenium + 7 API \texttt{bot-descarga} + 11 count-api + \textbf{25 Vitest desktop}.
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'uan}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{2.8cm}p{4.2cm}p{2.2cm}p{3.5cm}@{}}
|
||||
\toprule
|
||||
\textbf{Suite} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\
|
||||
\midrule
|
||||
Agent Selenium & Parsers, cancel, MinIO keys, GUI & 19/19 \pass & Dominio del PC agent est\'able \\
|
||||
API \texttt{bot-descarga} & Register, claim, events, auth & 7/7 \pass & Orquestador estable en unitario \\
|
||||
count-api & Features + clustering OCR & 11/11 \pass & Pipeline de conteo en izolaci\'on \\
|
||||
Desktop (Vitest) & UI shell, API client, CSV, schema & 25/25 \pass & Cliente Electron coherente \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Scorecard}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
@@ -11,43 +29,72 @@
|
||||
\texttt{bots/bot\_descarga/tests} & \pass & 19/19 & 3{,}21\,s \\
|
||||
\texttt{services/bot\_descarga/tests} & \pass & 7/7 & 1{,}45\,s \\
|
||||
\texttt{go test} count-api (\texttt{internal/pipeline}) & \pass & 11/11 & + Timing/HTTP en cap.~\ref{ch:count-latency} \\
|
||||
Vitest desktop & \neje & --- & No ejecutado en esta sesi\'on \\
|
||||
Vitest \texttt{services/desktop\_app} & \pass & 25/25 & $\approx$40\,s (7 archivos) \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Resultados unitarios.}
|
||||
\end{table}
|
||||
|
||||
\section{Cobertura agent}
|
||||
\section{Cobertura agent Selenium}
|
||||
Incluye excel reader, PDF validator, file manager, cancel, browser fatal, MinIO keys, GUI settings (19 casos).
|
||||
|
||||
\section{Cobertura API store}
|
||||
\section{Cobertura API \texttt{bot-descarga} (orquestador)}
|
||||
Incluye helpers MinIO, register/heartbeat/offline, claim at\'omico, cancel, events, filtros por job padre (7 casos).
|
||||
|
||||
\section{Cobertura count-api}
|
||||
Ver cap\'itulo~\ref{ch:count-latency}: explicaci\'on test a test con ejemplos,
|
||||
gr\'aficas y resultados (11 unitarias PASS; HTTP 3 PDFs ok en 2{,}2--4{,}5\,s).
|
||||
Ver cap\'itulo~\ref{ch:count-latency}: 11 unitarias PASS + \textbf{23 PDFs} medidos (HTTP y Go ok; promedio $\sim$2\,s/PDF).
|
||||
|
||||
\section{Cobertura desktop (Vitest)}
|
||||
Ejecutado: \texttt{cd services/desktop\_app \&\& npm test}\\
|
||||
7 archivos / 25 tests \pass\ (warnings \texttt{act(...)} en \texttt{App.test.tsx}, sin fallos).
|
||||
Incluye: \texttt{App} (shell/home/config), \texttt{apiClient}, CSV, path mapping, payloads,
|
||||
cleaning settings, schema display.
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
xbar,
|
||||
width=0.85\textwidth,
|
||||
height=4.2cm,
|
||||
xbar stacked,
|
||||
width=0.88\textwidth,
|
||||
height=5.2cm,
|
||||
xlabel={Casos},
|
||||
symbolic y coords={agent,api-store,count-api,desktop},
|
||||
symbolic y coords={desktop,count-api,api-store,agent},
|
||||
ytick=data,
|
||||
xmin=0, xmax=22,
|
||||
nodes near coords,
|
||||
bar width=10pt,
|
||||
legend style={at={(0.98,0.35)},anchor=east,font=\small}
|
||||
yticklabels={desktop Vitest,count-api,bot-descarga API,agent Selenium},
|
||||
xmin=0, xmax=28,
|
||||
enlarge y limits=0.18,
|
||||
bar width=12pt,
|
||||
axis y line*=left,
|
||||
axis x line*=bottom,
|
||||
tick align=outside,
|
||||
legend style={
|
||||
at={(0.98,0.12)},
|
||||
anchor=south east,
|
||||
font=\small,
|
||||
draw=ipsgrey!40,
|
||||
fill=white
|
||||
},
|
||||
visualization depends on={rawx \as \rawxval},
|
||||
nodes near coords={%
|
||||
\pgfmathparse{abs(\rawxval)<0.5 ? "":int(\rawxval)}%
|
||||
\pgfmathresult
|
||||
},
|
||||
every node near coord/.append style={
|
||||
font=\footnotesize\bfseries,
|
||||
text=black,
|
||||
/pgf/number format/assume math mode=true
|
||||
},
|
||||
]
|
||||
\addplot[fill=passgreen!70] coordinates {(19,agent) (7,api-store) (11,count-api) (0,desktop)};
|
||||
\addplot[fill=passgreen!75, draw=passgreen] coordinates {
|
||||
(25,desktop) (11,count-api) (7,api-store) (19,agent)
|
||||
};
|
||||
\addlegendentry{PASS}
|
||||
\addplot[fill=ipsgrey!40] coordinates {(0,agent) (0,api-store) (0,count-api) (1,desktop)};
|
||||
\addplot[fill=ipsgrey!35, draw=ipsgrey] coordinates {
|
||||
(0,desktop) (0,count-api) (0,api-store) (0,agent)
|
||||
};
|
||||
\addlegendentry{N/E}
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Unitarias ejecutadas vs pendientes.}
|
||||
\caption{Unitarias ejecutadas (todas en verde tras Vitest desktop).}
|
||||
\label{fig:unitarias}
|
||||
\end{figure}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
\chapter{Resultados --- humo y funcional}
|
||||
\label{ch:humo}
|
||||
|
||||
\callout{\textbf{Veredicto humo / funcional:} \pass}{%
|
||||
Gateway live y health agregado OK.\\
|
||||
E2E-07 (worker offline $\rightarrow$ 409): \pass.
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'uan}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.2cm}p{3.2cm}p{2.4cm}p{4.0cm}@{}}
|
||||
\toprule
|
||||
\textbf{Check} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\
|
||||
\midrule
|
||||
\texttt{/health/live} & Liveness gateway & \pass\ 11\,ms & Responde sin depender de upstreams \\
|
||||
\texttt{/health} & Health agregado & \pass\ 194\,ms & extraction, cleaning, counting, bot-descarga ok \\
|
||||
E2E-07 & Job con agent offline & \pass\ 409 & Orquestador no asigna sin agent online \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Humo gateway}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
@@ -10,14 +28,49 @@
|
||||
\midrule
|
||||
\texttt{GET /health/live} & \pass & 11\,ms & Liveness \\
|
||||
\texttt{GET /health} & \pass & 194\,ms & 4 servicios ok \\
|
||||
Contenedores IPS healthy & \pass & --- & 17\,h uptime \\
|
||||
Contenedores IPS healthy & \pass & --- & Stack arriba \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Humo HTTP.}
|
||||
\end{table}
|
||||
|
||||
\section{Funcional E2E-07 (worker offline $\rightarrow$ 409)}
|
||||
\neje\ en esta campa\~na (no se envi\'o \texttt{POST /bot-descarga/jobs} contra worker offline). Queda como follow-up de 2 minutos en staging.
|
||||
\subsection*{Qu\'e es}
|
||||
Comprueba que el orquestador \texttt{bot-descarga} \textbf{no} crea un job de descarga
|
||||
si el \textbf{agent Selenium} no est\'a online (sin heartbeat). Debe responder \textbf{HTTP 409}.
|
||||
|
||||
\section{Script preparado}
|
||||
Se a\~nadi\'o \texttt{docs/plan\_pruebas/scripts/k6\_smoke\_suite.js} para repeticiones automatizadas de live/health/config.
|
||||
\subsection*{C\'omo se ejecut\'o}
|
||||
Script: \texttt{docs/plan\_pruebas/scripts/e2e\_07\_worker\_offline.py}
|
||||
\begin{enumerate}
|
||||
\item \texttt{POST /jobs} --- crea job Desktop padre.
|
||||
\item Verifica que el worker \texttt{e2e07-offline-pc} no est\'a online.
|
||||
\item \texttt{POST /bot-descarga/jobs} (multipart Excel + \texttt{worker\_id} offline).
|
||||
\end{enumerate}
|
||||
|
||||
\subsection*{Resultado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\toprule
|
||||
\textbf{Paso} & \textbf{Resultado} \\
|
||||
\midrule
|
||||
Health live & 200 \\
|
||||
Job padre Desktop & 200 (id creado) \\
|
||||
Worker online? & no \\
|
||||
Crear job descarga & \textbf{409} (esperado) \\
|
||||
Detalle API & Worker no est\'a online; enciende el agent primero \\
|
||||
\midrule
|
||||
\textbf{Veredicto} & \pass \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\callout{\textbf{Significado}}{%
|
||||
El orquestador protege el flujo: sin agent Selenium encendido no se encola descarga.\\
|
||||
Evidencia en \texttt{docs/plan\_pruebas/informes/e2e\_07\_latest.json}.
|
||||
}
|
||||
|
||||
\section{Scripts}
|
||||
\begin{itemize}
|
||||
\item Humo: \texttt{docs/plan\_pruebas/scripts/k6\_smoke\_suite.js}
|
||||
\item E2E-07: \texttt{docs/plan\_pruebas/scripts/e2e\_07\_worker\_offline.py}
|
||||
\end{itemize}
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
\chapter{Resultados --- pruebas de carga}
|
||||
\label{ch:carga}
|
||||
|
||||
\callout{\textbf{Veredicto carga:} \pass\ (con matiz)}{%
|
||||
ST-GW-01 \pass. ST-BOT-01: corrida 1 \parcial\ (contenci\'on); corrida 2 \pass.\\
|
||||
ST-BOT-03 \neje\ (falt\'o DOWNLOAD\_JOB\_ID).
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'uan}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{2.2cm}p{3.8cm}p{3.0cm}p{3.8cm}@{}}
|
||||
\toprule
|
||||
\textbf{ID} & \textbf{Qu\'e eval\'ua} & \textbf{Resultado} & \textbf{Significado} \\
|
||||
\midrule
|
||||
ST-GW-01 & Gateway bajo VUs (\texttt{/health/live}) & p95 41\,ms, 0\,\% fail \pass & Orquestaci\'on no es cuello \\
|
||||
ST-BOT-01 & Heartbeats masivos (orquestador \texttt{bot-descarga}) & \#2 p95 86\,ms \pass & Orquestador OK si hay RAM \\
|
||||
ST-BOT-03 & Tormenta de events & \neje & Pendiente job real \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Tabla consolidada}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
@@ -19,46 +37,68 @@ ST-BOT-03 & --- & --- & --- & --- & --- & \neje \\
|
||||
\end{table}
|
||||
|
||||
\section{ST-GW-01}
|
||||
\label{sec:st-gw-01}
|
||||
|
||||
\subsection*{Qu\'e es}
|
||||
Prueba de \textbf{carga del API Gateway} (puerto :8189). Con la herramienta k6 se simulan
|
||||
varios usuarios virtuales (VUs) que llaman una y otra vez a \texttt{GET /health/live}
|
||||
(``\textquestiondown est\'as vivo?'').
|
||||
|
||||
No procesa facturas: no pasa por OCR, LLM ni agent Selenium. Solo comprueba que la \textbf{puerta de entrada}
|
||||
sigue respondiendo r\'apido cuando hay muchas peticiones a la vez.
|
||||
|
||||
\subsection*{Umbrales que deb\'ia cumplir}
|
||||
\begin{itemize}
|
||||
\item Checks 13768/13768 (100\,\%); throughput $\approx$57\,req/s.
|
||||
\item Thresholds p95$<$500\,ms y fail$<$1\,\% en verde.
|
||||
\item p95 (el 95\,\% de las respuestas) $<$ 500\,ms
|
||||
\item porcentaje de fallos $<$ 1\,\%
|
||||
\end{itemize}
|
||||
|
||||
\section{ST-BOT-01 --- comparativa}
|
||||
Corrida 1: funcional OK, latencia FAIL (contenci\'on host).\\
|
||||
Corrida 2: funcional OK, latencia PASS (host m\'as libre).
|
||||
\subsection*{Resultado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}lrl@{}}
|
||||
\toprule
|
||||
\textbf{M\'etrica} & \textbf{Valor} & \textbf{Lectura} \\
|
||||
\midrule
|
||||
Peticiones & 6\,884 & carga real enviada \\
|
||||
Fallos & 0\,\% & ninguna petici\'on rota \\
|
||||
Checks k6 & 13\,768/13\,768 (100\,\%) & todas las aserciones OK \\
|
||||
Promedio & 12{,}6\,ms & muy por debajo del umbral \\
|
||||
p95 & 40{,}9\,ms & \pass\ (l\'imite 500\,ms) \\
|
||||
M\'aximo & 125\,ms & sin outliers graves \\
|
||||
Ritmo & $\approx$57\,req/s & ver secci\'on~7.5 \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.95\textwidth,
|
||||
height=7cm,
|
||||
ylabel={Latencia (ms)},
|
||||
symbolic x coords={GW-p95,BOT1-p95,BOT2-p95,GW-max,BOT1-max,BOT2-max},
|
||||
xtick=data,
|
||||
x tick label style={rotate=30, anchor=east, font=\small},
|
||||
nodes near coords,
|
||||
nodes near coords style={font=\scriptsize},
|
||||
bar width=12pt,
|
||||
ymin=0,
|
||||
enlarge x limits=0.12,
|
||||
grid=major,
|
||||
legend entries={medici\'on, umbral GW 500, umbral BOT 800},
|
||||
legend style={at={(0.02,0.98)},anchor=north west,font=\scriptsize}
|
||||
]
|
||||
\addplot[fill=ipsblue!75] coordinates {
|
||||
(GW-p95,40.85) (BOT1-p95,2280) (BOT2-p95,85.55)
|
||||
(GW-max,125.37) (BOT1-max,23120) (BOT2-max,98.31)
|
||||
};
|
||||
\addplot[red, sharp plot, update limits=false, forget plot] coordinates {(GW-p95,500) (BOT2-max,500)};
|
||||
\addplot[orange, sharp plot, update limits=false] coordinates {(BOT1-p95,800) (BOT2-p95,800)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{p95 y max por caso. BOT1-max (23\,s) fuera de escala visual relativa --- ver tabla.}
|
||||
\label{fig:latencias}
|
||||
\end{figure}
|
||||
\callout{\textbf{Veredicto ST-GW-01:} \pass}{%
|
||||
El gateway aguanta carga concurrente ligera sin errores y con latencia baja.\\
|
||||
\textbf{Significado:} la orquestaci\'on HTTP \textbf{no} es el cuello de botella del sistema;
|
||||
el l\'imite de capacidad est\'a en OCR / LLM / agent Selenium (otros cap\'itulos), no en :8189.
|
||||
}
|
||||
|
||||
|
||||
\section{ST-BOT-01 --- comparativa}
|
||||
\subsection*{Qu\'e es}
|
||||
Prueba de \textbf{carga del orquestador \texttt{bot-descarga}} (API Docker en :8200, v\'ia gateway).
|
||||
k6 env\'ia 80 \texttt{POST /bot-descarga/workers/\{id\}/heartbeat} simulando agents online.
|
||||
\textbf{No} mide descarga Selenium ni la capacidad $\sim$422 fact/h del agent PC; solo la API de control
|
||||
(jobs, claim, heartbeats) bajo presi\'on.
|
||||
|
||||
Misma prueba (80 heartbeats k6 contra el orquestador \texttt{bot-descarga}), dos momentos del host:
|
||||
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.6cm}p{4.6cm}p{4.6cm}@{}}
|
||||
\toprule
|
||||
& \textbf{Corrida 1 --- host compartido} & \textbf{Corrida 2 --- host m\'as libre} \\
|
||||
\midrule
|
||||
Qu\'e pasaba & Swap $\approx$2{,}1\,GiB; MLflow y otros servicios compitiendo & Menos carga; m\'as RAM libre \\
|
||||
Funcional & 100\,\% HTTP 200 & 100\,\% HTTP 200 \\
|
||||
p95 & \textbf{2{,}28\,s} (umbral 800\,ms) & \textbf{86\,ms} \\
|
||||
max & \textbf{23{,}1\,s} & \textbf{98\,ms} \\
|
||||
Veredicto & \parcial\ (latencia) & \pass \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
@@ -66,45 +106,91 @@ Corrida 2: funcional OK, latencia PASS (host m\'as libre).
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.9\textwidth,
|
||||
height=6cm,
|
||||
ylabel={ms},
|
||||
symbolic x coords={avg,med,p95,max},
|
||||
height=6.4cm,
|
||||
ylabel={Latencia heartbeat (ms)},
|
||||
symbolic x coords={promedio,p95,maximo},
|
||||
xtick=data,
|
||||
legend style={at={(0.02,0.98)},anchor=north west},
|
||||
bar width=10pt,
|
||||
ymin=0, ymax=600,
|
||||
enlarge x limits=0.15,
|
||||
xticklabels={promedio,p95,m\'aximo},
|
||||
enlarge x limits=0.25,
|
||||
ymin=0, ymax=2800,
|
||||
bar width=16pt,
|
||||
grid=major,
|
||||
title={ST-BOT-01: corrida 1 vs 2 (max de \#1 truncado en eje)}
|
||||
major grid style={ipsgrey!25},
|
||||
legend style={
|
||||
at={(0.5,-0.2)},
|
||||
anchor=north,
|
||||
legend columns=2,
|
||||
font=\small,
|
||||
draw=ipsgrey!40
|
||||
},
|
||||
point meta=explicit symbolic,
|
||||
nodes near coords,
|
||||
every node near coord/.append style={font=\scriptsize\bfseries, anchor=south, yshift=1pt},
|
||||
]
|
||||
\addplot[fill=failred!60] coordinates {(avg,511) (med,57) (p95,500) (max,600)};
|
||||
\addlegendentry{\#1 (p95 real 2280; max 23120)}
|
||||
\addplot[fill=passgreen!70] coordinates {(avg,57) (med,56) (p95,86) (max,98)};
|
||||
\addlegendentry{\#2 PASS}
|
||||
% Maximo corrida 1: valor visual 2500 (recortado); etiqueta indica 23,1 s real
|
||||
\addplot[fill=failred!65, draw=failred] coordinates {
|
||||
(promedio,511) [511 ms]
|
||||
(p95,2280) [2,28 s]
|
||||
(maximo,2500) [23,1 s*]
|
||||
};
|
||||
\addlegendentry{Corrida 1 --- host compartido}
|
||||
\addplot[fill=passgreen!75, draw=passgreen] coordinates {
|
||||
(promedio,57) [57 ms]
|
||||
(p95,86) [86 ms]
|
||||
(maximo,98) [98 ms]
|
||||
};
|
||||
\addlegendentry{Corrida 2 --- host m\'as libre}
|
||||
\draw[orange, thick, dashed] (axis cs:promedio,800) -- (axis cs:maximo,800)
|
||||
node[midway, above, font=\scriptsize, text=orange] {umbral p95 = 800\,ms};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Before/after heartbeats (eje limitado; valores exactos en tabla~\ref{tab:carga}).}
|
||||
\caption{Misma prueba ST-BOT-01, distinto estado del host. (*) El m\'aximo real de la corrida 1
|
||||
fue 23{,}1\,s; la barra est\'a recortada solo para que se lean el resto de valores.
|
||||
Detalle num\'erico en la tabla~\ref{tab:carga}.}
|
||||
\label{fig:bot-before-after}
|
||||
\end{figure}
|
||||
|
||||
\callout{\textbf{Conclusi\'on de esta prueba:}}{%
|
||||
Con un \textbf{host dedicado} (sin competir con MLflow/swap/otros stacks) el servicio \texttt{bot-descarga} se comporta
|
||||
mucho mejor: p95 baja de 2{,}28\,s a 86\,ms y desaparece el outlier de 23\,s.
|
||||
El fallo de la corrida 1 no es un bug del heartbeat (siempre HTTP 200), sino
|
||||
\textbf{contenci\'on del host compartido}. Para SLOs estables en staging/prod hace falta
|
||||
aislar IPSExtractor en un nodo dedicado.
|
||||
}
|
||||
|
||||
|
||||
\section{Throughput gateway}
|
||||
\textbf{Qu\'e mide.} En ST-GW-01, k6 martillea \texttt{GET /health/live} del gateway
|
||||
(:8189). Ese endpoint solo comprueba que el proceso responde; \textbf{no} cuenta PDFs,
|
||||
\textbf{no} pasa por OCR ni por el LLM.
|
||||
|
||||
\textbf{Resultado.} $\approx$57 peticiones por segundo (req/s), con p95 41\,ms y 0\,\% de fallos.
|
||||
Eso indica que la ``puerta\'\' del sistema aguanta bien la orquestaci\'on HTTP ligera.
|
||||
|
||||
\textbf{Qu\'e \emph{no} significa.} 57\,req/s \textbf{no} se traduce a 57 facturas/hora ni a
|
||||
capacidad del pipeline. La capacidad real la marcan las etapas pesadas (OCR, LLM, agent Selenium),
|
||||
documentadas en otros cap\'itulos y en Capacidad 100k.
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
width=0.7\textwidth,
|
||||
height=5cm,
|
||||
width=0.65\textwidth,
|
||||
height=4.2cm,
|
||||
ybar,
|
||||
symbolic x coords={ST-GW-01},
|
||||
xtick=data,
|
||||
ylabel={req/s},
|
||||
ylabel={peticiones / segundo},
|
||||
nodes near coords,
|
||||
ymin=0,
|
||||
bar width=30pt
|
||||
every node near coord/.append style={font=\small\bfseries},
|
||||
ymin=0, ymax=80,
|
||||
bar width=36pt,
|
||||
enlarge x limits=0.6
|
||||
]
|
||||
\addplot[fill=ipsblue] coordinates {(ST-GW-01,57.33)};
|
||||
\addplot[fill=ipsblue!80, draw=ipsblue] coordinates {(ST-GW-01,57.33)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Throughput observado en health liveness (no equivale a PDFs/h).}
|
||||
\caption{Ritmo del gateway en health liveness ($\approx$57\,req/s). Solo mide HTTP ligero,
|
||||
no facturas procesadas.}
|
||||
\label{fig:throughput}
|
||||
\end{figure}
|
||||
|
||||
@@ -1,104 +1,10 @@
|
||||
\chapter{Capacidad: 100\,000 facturas al mes}
|
||||
\label{ch:capacidad}
|
||||
\chapter*{Movido}
|
||||
\label{ch:capacidad-moved}
|
||||
|
||||
\section{Traducci\'on del objetivo}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lrr@{}}
|
||||
\toprule
|
||||
\textbf{Escenario operativo} & \textbf{Facturas/d\'ia} & \textbf{Facturas/hora} \\
|
||||
\midrule
|
||||
100\,000 / mes, 24$\times$7 & $\approx$3\,333 & $\approx$\textbf{139} \\
|
||||
100\,000 / mes, 8\,h $\times$ 22\,d & $\approx$4\,545 & $\approx$\textbf{570} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Throughput m\'inimo para cumplir 100k/mes.}
|
||||
\end{table}
|
||||
El an\'alisis de \textbf{100\,000 facturas/mes} vive ahora en el documento independiente:
|
||||
|
||||
\section{Modelo por etapa}
|
||||
Supuestos: host compartido medido el 2026-07-13; estimaciones \textbf{conservadoras} (no bench OCR/LLM masivo). Detalle en \texttt{scripts/estimate\_capacity.md}.
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lrrll@{}}
|
||||
\toprule
|
||||
\textbf{Etapa} & \textbf{Host act.} & \textbf{Dedicado} & \textbf{vs 139/h} & \textbf{vs 570/h} \\
|
||||
\midrule
|
||||
Gateway (orquestaci\'on) & $\gg$10\,000 & $\gg$20\,000 & OK & OK \\
|
||||
count-api OCR & $\sim$40 & $\sim$400 & RIESGO & NO \\
|
||||
extraction LLM & $\sim$100 & $\sim$800 & \'AMBAR & NO$^{*}$ \\
|
||||
bot Selenium (1 agent) & $\sim$60 & $\sim$500$^{\dagger}$ & RIESGO & NO \\
|
||||
cleaning & $\sim$800 & $\sim$3\,000 & OK & \'AMBAR \\
|
||||
\textbf{Pipeline completo} & $\sim$35 & $\sim$450 & \fail & \'AMBAR \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Facturas/hora estimadas. $^{*}$Sujeto a cuota LLM. $^{\dagger}$Con $N$ agents en paralelo.}
|
||||
\label{tab:capacidad}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=\textwidth,
|
||||
height=7.5cm,
|
||||
ylabel={Facturas / hora (estimadas)},
|
||||
symbolic x coords={Gateway,OCR,LLM,Bot,Cleaning,Pipeline},
|
||||
xtick=data,
|
||||
x tick label style={font=\small},
|
||||
legend style={at={(0.02,0.98)},anchor=north west,font=\small},
|
||||
bar width=8pt,
|
||||
ymin=0, ymax=1200,
|
||||
enlarge x limits=0.08,
|
||||
grid=major,
|
||||
title={Capacidad estimada vs objetivos 139/h y 570/h}
|
||||
]
|
||||
\addplot[fill=ipsblue!70] coordinates {
|
||||
(Gateway,1000) (OCR,40) (LLM,100) (Bot,60) (Cleaning,800) (Pipeline,35)
|
||||
};
|
||||
\addlegendentry{Host actual (Gateway truncado)}
|
||||
\addplot[fill=passgreen!55] coordinates {
|
||||
(Gateway,1000) (OCR,400) (LLM,800) (Bot,500) (Cleaning,1000) (Pipeline,450)
|
||||
};
|
||||
\addlegendentry{Dedicado (estim.)}
|
||||
\addplot[failred, ultra thick, sharp plot, update limits=false] coordinates {
|
||||
(Gateway,139) (Pipeline,139)
|
||||
};
|
||||
\addlegendentry{Objetivo 139/h}
|
||||
\addplot[warnorange, ultra thick, sharp plot, update limits=false] coordinates {
|
||||
(Gateway,570) (Pipeline,570)
|
||||
};
|
||||
\addlegendentry{Objetivo 570/h}
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Brecha de capacidad. Gateway real $\gg$1000/h; se trunca para legibilidad.}
|
||||
\label{fig:capacidad}
|
||||
\end{figure}
|
||||
|
||||
\section{Interpretaci\'on}
|
||||
\begin{enumerate}
|
||||
\item El \textbf{gateway} no es el cuello de botella (evidencia ST-GW-01).
|
||||
\item En el \textbf{host actual}, OCR + Selenium + LLM compiten por CPU/RAM/swap con MLflow: el pipeline completo queda $\ll$139/h.
|
||||
\item Con infra \textbf{dedicada} (sin zoo, m\'as RAM, workers, $N$ PCs bot, cuota LLM), acercarse a 139/h (24$\times$7) es plausible; 570/h exige dimensionamiento agresivo y prueba de carga espec\'ifica.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Veredicto}
|
||||
\begin{center}
|
||||
\fbox{\parbox{0.92\textwidth}{\centering
|
||||
\large \textbf{Se pueden procesar +100\,000 facturas/mes?}\\[0.4em]
|
||||
\normalsize
|
||||
En el servidor actual compartido: \fail.\\[0.2em]
|
||||
En arquitectura dedicada y dimensionada: \pass\ condicional (cuota LLM, OCR, agents).\\[0.3em]
|
||||
\footnotesize Esta campa\~na no ejecut\'o 100k PDFs; el veredicto es modelo + evidencia de carga ligera.
|
||||
}}
|
||||
\texttt{docs/plan\_pruebas/capacidad\_100k/} $\rightarrow$ \textbf{Capacidad100k\_IPSExtractor.pdf}
|
||||
\end{center}
|
||||
|
||||
\section{Requisitos m\'inimos sugeridos para 100k/mes}
|
||||
\begin{itemize}
|
||||
\item Host(s) dedicados IPS: RAM $\geq$16--32\,GiB; SSD; sin MLflow/Airflow en el mismo nodo.
|
||||
\item count-api: pool OCR dimensionado; l\'imite de memoria realista.
|
||||
\item extraction: r\'eplicas worker + concurrencia acorde a RPM del LLM; presupuesto de coste.
|
||||
\item bot: varios agents PC en paralelo hacia Imaging/MinIO.
|
||||
\item Observabilidad: colas ARQ, p95 por etapa, alertas de swap.
|
||||
\end{itemize}
|
||||
Este archivo se conserva solo como marcador para no romper enlaces antiguos.
|
||||
|
||||
@@ -3,10 +3,21 @@
|
||||
|
||||
\section{Hallazgos}
|
||||
\begin{enumerate}
|
||||
\item \textbf{H1 --- Contenci\'on de plataforma:} ST-BOT-01 \#1 (p95 2{,}28\,s, max 23\,s) vs \#2 (p95 86\,ms) demuestra sensibilidad a swap/MLflow, no a un bug funcional del heartbeat (100\,\% HTTP 200 en ambas).
|
||||
\item \textbf{H1 --- Contenci\'on de plataforma:} ST-BOT-01 \#1 (p95 2{,}28\,s, max 23\,s) vs \#2 (p95 86\,ms)
|
||||
demuestra que un \textbf{host dedicado / sin contenci\'on} se comporta mucho mejor.
|
||||
No es un bug del heartbeat (100\,\% HTTP 200 en ambas); es sensibilidad a swap/MLflow
|
||||
en el host compartido.
|
||||
\item \textbf{H2 --- Gateway saludable:} 0\,\% errores y p95 41\,ms bajo 25 VUs en liveness.
|
||||
\item \textbf{H3 --- Unitarias s\'olidas:} 29 casos verdes (19 agent + 7 API store + 3 count-api pipeline).
|
||||
\item \textbf{H4 --- Capacidad 100k:} imposible en host compartido actual; viable solo con escalado dedicado.
|
||||
\item \textbf{H3 --- Unitarias s\'olidas:} agent Selenium + \texttt{bot-descarga} + count-api
|
||||
+ Vitest desktop (25/25) en verde.
|
||||
\item \textbf{H4 --- LLM medido:} p50 7{,}1\,s $\approx$506 fact/h; etapa viable vs 139/h.
|
||||
\item \textbf{H5 --- Cola ARQ validada:} ST-EXT-01 30/30 drenados en 22{,}5\,s (mock); prod debe usar \texttt{ASYNC\_QUEUE=true}.
|
||||
\item \textbf{H6 --- OCR bajo carga:} ST-COUNT-01 9/9 ok; p50 35{,}8\,s con 3 concurrentes
|
||||
(vs $\sim$2\,s en serie) $\Rightarrow$ $\approx$100--325 fact/h en shared8g.
|
||||
\item \textbf{H7 --- Capacidad 100k:} imposible en host compartido actual (OCR/contenci\'on);
|
||||
ver PDF \emph{Capacidad 100k/mes}.
|
||||
\item \textbf{H8 --- E2E-07:} crear job con agent offline $\rightarrow$ HTTP 409 \pass;
|
||||
el orquestador \texttt{bot-descarga} no asigna descarga sin agent online.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Scorecard global}
|
||||
@@ -18,18 +29,18 @@
|
||||
width=0.9\textwidth,
|
||||
height=5.5cm,
|
||||
ylabel={Cantidad de casos},
|
||||
symbolic x coords={Unitaria,Humo,Carga,Funcional,Capacidad},
|
||||
symbolic x coords={Unitaria,Humo,Carga,LLM,ARQ,Count},
|
||||
xtick=data,
|
||||
legend style={at={(0.5,-0.22)},anchor=north,legend columns=3,font=\small},
|
||||
bar width=18pt,
|
||||
bar width=14pt,
|
||||
ymin=0, ymax=8,
|
||||
enlarge x limits=0.12
|
||||
enlarge x limits=0.1
|
||||
]
|
||||
\addplot[fill=passgreen!70] coordinates {(Unitaria,3) (Humo,3) (Carga,2) (Funcional,0) (Capacidad,0)};
|
||||
\addplot[fill=passgreen!70] coordinates {(Unitaria,4) (Humo,4) (Carga,2) (LLM,1) (ARQ,1) (Count,3)};
|
||||
\addlegendentry{PASS}
|
||||
\addplot[fill=warnorange!70] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (Funcional,0) (Capacidad,0)};
|
||||
\addplot[fill=warnorange!70] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (LLM,0) (ARQ,0) (Count,0)};
|
||||
\addlegendentry{PARCIAL}
|
||||
\addplot[fill=ipsgrey!50] coordinates {(Unitaria,1) (Humo,0) (Carga,1) (Funcional,1) (Capacidad,1)};
|
||||
\addplot[fill=ipsgrey!50] coordinates {(Unitaria,0) (Humo,0) (Carga,1) (LLM,0) (ARQ,0) (Count,0)};
|
||||
\addlegendentry{N/E}
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
@@ -38,4 +49,6 @@
|
||||
\end{figure}
|
||||
|
||||
\section{Limitaciones}
|
||||
No se midieron tiempos reales OCR/LLM por PDF en esta campa\~na; las cifras de capacidad son estimaciones documentadas para decisi\'on de arquitectura, no un compromiso de SLA.
|
||||
No se ejecut\'o ST-COUNT-01 lab agresivo ni E2E Imaging a volumen; ST-EXT-01 lab (100--500) pendiente.
|
||||
El veredicto 100k/mes del pipeline completo es modelo + evidencia parcial
|
||||
(documento Capacidad 100k), no un compromiso de SLA.
|
||||
|
||||
@@ -3,24 +3,34 @@
|
||||
|
||||
\section{Conclusiones}
|
||||
\begin{enumerate}
|
||||
\item IPSExtractor en staging responde correctamente a humo y carga ligera de control (gateway + heartbeats).
|
||||
\item La calidad de latencia depende cr\'iticamente de no saturar el host compartido.
|
||||
\item \textbf{100\,000 facturas/mes no son alcanzables} en el laptop Ubuntu actual con el zoo Docker observado.
|
||||
\item Con infraestructura dedicada y dimensionada, el objetivo es \textbf{condicional\-mente viable}; requiere prueba de carga espec\'ifica por etapa (OCR, LLM, bot).
|
||||
\item IPSExtractor en staging responde correctamente a humo y carga ligera (gateway + heartbeats).
|
||||
\item \textbf{Host dedicado vs compartido:} ST-BOT-01 muestra que, con la misma prueba,
|
||||
un host libre (sin MLflow/swap compitiendo) baja el p95 de 2{,}28\,s a 86\,ms y elimina
|
||||
el outlier de 23\,s. El comportamiento es claramente mejor en nodo dedicado;
|
||||
la latencia mala no fue un bug funcional (HTTP 200 en ambas corridas).
|
||||
\item Agent Selenium ($\approx$422\,fact/h) y LLM ($\approx$506\,fact/h) cubren el objetivo 24$\times$7 \emph{por etapa}.
|
||||
\item La cola ARQ (ST-EXT-01) dren\'o 30 docs en $\approx$23\,s con mock; arquitectura async validada.
|
||||
\item OCR bajo 3 concurrentes (ST-COUNT-01): p50 35{,}8\,s; $\approx$100--325 fact/h en shared8g;
|
||||
degradaci\'on fuerte vs serie ($\sim$2\,s).
|
||||
\item El pipeline a 100k/mes en el host actual \textbf{no est\'a garantizado}
|
||||
(ritmo $\approx\min$ etapas $=$ OCR $\sim$100--325/h; piso $\sim$100 $<$ 139);
|
||||
detalle en \textbf{Capacidad100k\_IPSExtractor.pdf}.
|
||||
\item Con infra dedicada + OCR dimensionado, 24$\times$7 es \textbf{condicionalmente viable}.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Recomendaciones}
|
||||
\begin{enumerate}
|
||||
\item Separar IPS de MLflow/Airflow/n8n en el nodo de producci\'on/staging de rendimiento.
|
||||
\item Ejecutar ST-COUNT-01 y bench LLM solo con $\geq$3\,GiB libres y sin swap activo.
|
||||
\item Completar E2E-07 y ST-BOT-03; archivar JSON de k6 en \texttt{informes/}.
|
||||
\item Dimensionar agents bot ($N$) antes de comprometer vol\'umenes mensuales de descarga.
|
||||
\item Incorporar CI: pytest agent + store en cada PR.
|
||||
\item Separar IPSExtractor en un \textbf{host dedicado} (sin MLflow/Airflow/n8n en el mismo nodo
|
||||
de rendimiento); ST-BOT-01 lo justifica con evidencia.
|
||||
\item Producci\'on: \texttt{ASYNC\_QUEUE=true} + workers dimensionados a la cuota LLM.
|
||||
\item Repetir ST-COUNT-01 en perfil lab (m\'as rps / VUs) con $\geq$3\,GiB libres.
|
||||
\item Completar ST-BOT-03, ST-EXT-01 perfil lab (100--500) y E2E pendientes.
|
||||
\item Dimensionar agents Selenium ($N$) y workers extraction antes de comprometer vol\'umenes.
|
||||
\item Incorporar CI: pytest agent Selenium + orquestador \texttt{bot-descarga} en cada PR.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Pr\'oxima campa\~na sugerida}
|
||||
\section{Documentos relacionados}
|
||||
\begin{itemize}
|
||||
\item Ventana con MLflow detenido.
|
||||
\item Bench: 50--200 PDFs reales por etapa (OCR, LLM) midiendo p50/p95 y coste.
|
||||
\item Actualizar gr\'afica de capacidad con datos medidos (reemplazar estimaciones).
|
||||
\item \textbf{Plan de pruebas} --- estrategia e IDs.
|
||||
\item \textbf{Capacidad 100k/mes} --- modelo, coste LLM, veredicto de volumen.
|
||||
\end{itemize}
|
||||
|
||||
@@ -26,11 +26,27 @@ services/bot_descarga/tests: 7 passed in 1.45s
|
||||
count-api internal/pipeline: 3 passed (+8 features/clustering nuevos)
|
||||
Timing: go test -run Timing -v (3 PDFs en docs/plan_pruebas/data/)
|
||||
HTTP: bash docs/plan_pruebas/scripts/measure_count_latency.sh
|
||||
LLM: python docs/plan_pruebas/scripts/measure_llm_latency.py
|
||||
\end{lstlisting}
|
||||
|
||||
\section{Scripts}
|
||||
\texttt{k6\_gateway\_health.js}, \texttt{k6\_bot\_heartbeats.js}, \texttt{k6\_bot\_events.js},
|
||||
\texttt{k6\_count\_batches.js}, \texttt{k6\_smoke\_suite.js}, \texttt{estimate\_capacity.md}.
|
||||
\texttt{k6\_count\_batches.js}, \texttt{k6\_smoke\_suite.js},
|
||||
\texttt{st\_ext\_01\_arq\_bench.py}, \texttt{st\_ext\_02\_llm\_bench.py},
|
||||
\texttt{llm\_mock\_server.py}, \texttt{measure\_count\_latency.*}, \texttt{estimate\_capacity.md}.
|
||||
|
||||
\section{Extracto ST-EXT-01}
|
||||
\begin{lstlisting}
|
||||
mode=async queued=30/30 drained=True
|
||||
enqueue_wall_s=1.307 drain_s=22.544 total_s=23.851
|
||||
throughput_docs_per_h ~ 4528 (mock; no SLO LLM)
|
||||
\end{lstlisting}
|
||||
|
||||
\section{Documentos hermanos}
|
||||
\begin{itemize}
|
||||
\item Plan: \texttt{PlanPruebas\_IPSExtractor.pdf}
|
||||
\item Capacidad 100k: \texttt{Capacidad100k\_IPSExtractor.pdf}
|
||||
\end{itemize}
|
||||
|
||||
\section{Reproducibilidad}
|
||||
\begin{enumerate}
|
||||
|
||||
@@ -1,9 +1,26 @@
|
||||
\chapter{Count-api: qu\'e eval\'ua cada prueba}
|
||||
\label{ch:count-latency}
|
||||
|
||||
Este cap\'itulo explica, con ejemplos, las pruebas del microservicio \textbf{count-api}
|
||||
(conteo de servicios en facturas PDF) y resume los \textbf{resultados} obtenidos
|
||||
en el host Ubuntu el 13 de julio de 2026.
|
||||
\callout{\textbf{Veredicto count-api:} \pass\ en muestra}{%
|
||||
11/11 unitarias; 23/23 PDFs HTTP ok; latencia tipica $\sim$2\,s/PDF.\\
|
||||
No sustituye ST-COUNT-01 agresivo (capacidad OCR bajo carga).
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'ua / resultado / significado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.0cm}p{4.0cm}p{5.5cm}@{}}
|
||||
\toprule
|
||||
\textbf{Bloque} & \textbf{Resultado} & \textbf{Qu\'e significa} \\
|
||||
\midrule
|
||||
Features + clustering & 11/11 \pass & L\'ogica OCR de conteo correcta sin PDF \\
|
||||
HTTP 23 PDFs & 23/23 \pass & Endpoint responde con PDFs reales \\
|
||||
Latencia $\sim$2\,s/PDF & medido & En host dedicado $\sim$1800/h te\'oricos; en shared8g contenci\'on baja a $\sim$40/h estimados \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
Este cap\'itulo explica las pruebas del microservicio \textbf{count-api}
|
||||
(conteo de servicios en facturas PDF) y resume los \textbf{resultados} del 13 de julio de 2026.
|
||||
|
||||
\section{Pipeline bajo prueba}
|
||||
\begin{center}
|
||||
@@ -27,7 +44,7 @@ en el host Ubuntu el 13 de julio de 2026.
|
||||
\end{center}
|
||||
|
||||
Las unitarias de \texttt{features} y \texttt{clustering} \textbf{no} abren el PDF:
|
||||
simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP s\'i usan los 3 PDFs reales.
|
||||
simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP usan los \textbf{23 PDFs} reales en \texttt{docs/plan\_pruebas/data/}.
|
||||
|
||||
\section{Resultado global count-api}
|
||||
\begin{table}[ht]
|
||||
@@ -37,11 +54,11 @@ simulan ``cajas'' OCR \texttt{\{texto, X, Y\}}. Timing y HTTP s\'i usan los 3 PD
|
||||
\textbf{Bloque} & \textbf{Resultado} & \textbf{Casos} & \textbf{Notas} \\
|
||||
\midrule
|
||||
Features + clustering & \pass & 11/11 & Ubuntu, \texttt{go test -v} \\
|
||||
HTTP 3 PDFs reales & \pass & 3/3 & 200, status ok, $\sim$2--4{,}5\,s \\
|
||||
Timing Go por etapa & pendiente / skip path & --- & re-ejecutar \texttt{-run Timing -count=1} \\
|
||||
HTTP 23 PDFs reales & \pass & 23/23 & 200, status ok \\
|
||||
Timing Go por etapa & \pass & 23/23 & 46{,}3\,s total; todos ok \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Scorecard count-api en esta campa\~na.}
|
||||
\caption{Scorecard count-api --- campa\~na 23 PDFs (13 jul 2026).}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
@@ -55,18 +72,16 @@ Timing Go por etapa & pendiente / skip path & --- & re-ejecutar \texttt{-run Tim
|
||||
symbolic x coords={Features,Clustering,HTTP-PDF,Timing-Go},
|
||||
xtick=data,
|
||||
nodes near coords,
|
||||
ymin=0, ymax=12,
|
||||
ymin=0, ymax=25,
|
||||
bar width=14pt,
|
||||
enlarge x limits=0.15,
|
||||
legend style={at={(0.98,0.98)},anchor=north east,font=\small}
|
||||
]
|
||||
\addplot[fill=passgreen!75] coordinates {(Features,7) (Clustering,4) (HTTP-PDF,3) (Timing-Go,0)};
|
||||
\addplot[fill=passgreen!75] coordinates {(Features,11) (Clustering,0) (HTTP-PDF,23) (Timing-Go,23)};
|
||||
\addlegendentry{PASS}
|
||||
\addplot[fill=ipsgrey!45] coordinates {(Features,0) (Clustering,0) (HTTP-PDF,0) (Timing-Go,1)};
|
||||
\addlegendentry{Pendiente}
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Cobertura count-api por bloque.}
|
||||
\caption{Cobertura count-api: 11 unitarias + 23 PDFs medidos (HTTP y Go).}
|
||||
\label{fig:count-score}
|
||||
\end{figure}
|
||||
|
||||
@@ -168,12 +183,8 @@ un par cercano forma cluster.
|
||||
|
||||
\section{Pruebas con PDFs reales}
|
||||
\subsection{Fixtures}
|
||||
\begin{itemize}
|
||||
\item \texttt{500000284335937.pdf}
|
||||
\item \texttt{500000284335940.pdf}
|
||||
\item \texttt{500000284335954.pdf}
|
||||
\end{itemize}
|
||||
Ubicaci\'on: \texttt{docs/plan\_pruebas/data/}.
|
||||
\textbf{23 PDFs} en \texttt{docs/plan\_pruebas/data/} (facturas m\'edicas reales, $\sim$280--294\,KB c/u).
|
||||
Listado completo en el CSV \texttt{informes/count\_latency\_20260713.csv}.
|
||||
|
||||
\subsection{TestProcessPDFTimingFixtures}
|
||||
\textbf{Eval\'ua:} pipeline completo (render$\rightarrow$OCR$\rightarrow$cluster$\rightarrow$conteo) y que
|
||||
@@ -182,55 +193,125 @@ Ubicaci\'on: \texttt{docs/plan\_pruebas/data/}.
|
||||
\subsection{Script HTTP \texttt{measure\_count\_latency}}
|
||||
\textbf{Eval\'ua:} el camino real del usuario/Desktop: gateway \texttt{POST /count/v1/count}.
|
||||
|
||||
\section{Resultados de latencia HTTP (medidos)}
|
||||
Host \texttt{ubuntu}, gateway \texttt{127.0.0.1:8189}, 13 jul 2026:
|
||||
\section{Resultados agregados (23 PDFs)}
|
||||
Host \texttt{ubuntu}, 13 jul 2026. Corrida secuencial (\texttt{-count=1}).
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lrrll@{}}
|
||||
\begin{tabular}{@{}lrrrr@{}}
|
||||
\toprule
|
||||
\textbf{PDF} & \textbf{t (s)} & \textbf{HTTP} & \textbf{servicios} & \textbf{status} \\
|
||||
\textbf{M\'etrica} & \textbf{HTTP (s)} & \textbf{Total Go (ms)} & \textbf{OCR (ms)} & \textbf{Render (ms)} \\
|
||||
\midrule
|
||||
500000284335937.pdf & 4{,}45 & 200 & 1 & ok \\
|
||||
500000284335940.pdf & 2{,}29 & 200 & 1 & ok \\
|
||||
500000284335954.pdf & 2{,}19 & 200 & 1 & ok \\
|
||||
Promedio & 1{,}81 & 2008 & 1876 & 131 \\
|
||||
M\'inimo & 1{,}62 & 1823 & 1689 & 108 \\
|
||||
M\'aximo & 2{,}42 & 2435 & 2285 & 155 \\
|
||||
Desv.\ est.\ aprox. & 0{,}21 & 150 & 140 & 14 \\
|
||||
\midrule
|
||||
Promedio & 2{,}98 & --- & --- & --- \\
|
||||
M\'in / M\'ax & 2{,}19 / 4{,}45 & --- & --- & --- \\
|
||||
Servicios detectados/factura & \multicolumn{4}{c}{41--48 (promedio 43{,}5)} \\
|
||||
Cluster (ms) & \multicolumn{4}{c}{$<$2 (despreciable)} \\
|
||||
Infer (ms) & \multicolumn{4}{c}{0 (heur\'istica sin ONNX en test)} \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Wall-clock HTTP. Fuente: \texttt{informes/count\_latency\_20260713.csv}.}
|
||||
\label{tab:count-http}
|
||||
\caption{Estad\'isticas sobre 23 facturas. HTTP v\'ia gateway; Go v\'ia \texttt{ProcessPDF} directo.}
|
||||
\label{tab:count-stats}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.95\textwidth,
|
||||
ybar stacked,
|
||||
width=0.92\textwidth,
|
||||
height=6cm,
|
||||
ylabel={Segundos (HTTP)},
|
||||
symbolic x coords={...937,...940,...954},
|
||||
ylabel={Milisegundos (promedio)},
|
||||
symbolic x coords={Pipeline},
|
||||
xtick=data,
|
||||
nodes near coords,
|
||||
nodes near coords style={font=\small},
|
||||
ymin=0, ymax=5.5,
|
||||
bar width=22pt,
|
||||
enlarge x limits=0.2,
|
||||
title={Tiempo de conteo por PDF (gateway)}
|
||||
ymin=0, ymax=2200,
|
||||
legend style={at={(0.5,-0.18)},anchor=north,legend columns=3,font=\small},
|
||||
title={Desglose promedio del tiempo por etapa (Go)}
|
||||
]
|
||||
\addplot[fill=ipsblue!80] coordinates {(...937,4.45) (...940,2.29) (...954,2.19)};
|
||||
\addplot[warnorange, ultra thick, sharp plot, update limits=false] coordinates {(...937,2.98) (...954,2.98)};
|
||||
\addplot[fill=ipsblue!60] coordinates {(Pipeline,131)};
|
||||
\addlegendentry{Render}
|
||||
\addplot[fill=warnorange!70] coordinates {(Pipeline,1876)};
|
||||
\addlegendentry{OCR}
|
||||
\addplot[fill=passgreen!50] coordinates {(Pipeline,1)};
|
||||
\addlegendentry{Cluster}
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Barras: tiempo por PDF. L\'inea naranja: promedio 2{,}98\,s.}
|
||||
\label{fig:count-http-bars}
|
||||
\caption{El OCR representa $\sim$93\,\% del tiempo de procesamiento.}
|
||||
\label{fig:count-breakdown}
|
||||
\end{figure}
|
||||
|
||||
\textbf{Lectura:} en este host, contar \emph{una} factura tarda del orden de \textbf{2--5 segundos}
|
||||
(OCR suele dominar). Extrapolaci\'on ingenua a 3\,s/PDF $\approx$ 1\,200 facturas/hora de conteo
|
||||
si el nodo estuviera dedicado y sin colas --- ver cap\'itulo de capacidad 100k/mes para el contexto completo.
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
width=0.95\textwidth,
|
||||
height=5.5cm,
|
||||
xlabel={PDF (orden alfab\'etico, n=23)},
|
||||
ylabel={Total (ms)},
|
||||
title={Tiempo total por factura (TestProcessPDFTimingFixtures)},
|
||||
grid=major,
|
||||
ymin=1700, ymax=2500,
|
||||
]
|
||||
\addplot[blue, mark=*, thick] coordinates {
|
||||
(1,2435) (2,1954) (3,1980) (4,1994) (5,1823) (6,1955) (7,1965) (8,1873)
|
||||
(9,1937) (10,1947) (11,2430) (12,1999) (13,2016) (14,1963) (15,1875)
|
||||
(16,2186) (17,1901) (18,2002) (19,2024) (20,1840) (21,2280) (22,1884) (23,1916)
|
||||
};
|
||||
\addplot[warnorange, dashed, thick] coordinates {(0,2008) (24,2008)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Serie de tiempos totales. L\'inea punteada: promedio 2008\,ms.}
|
||||
\label{fig:count-series}
|
||||
\end{figure}
|
||||
|
||||
\section{Resultados de latencia HTTP}
|
||||
Gateway \texttt{127.0.0.1:8189}, \texttt{POST /count/v1/count}. Los 23 PDFs respondieron \textbf{200 / ok}.
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{@{}rr@{}}
|
||||
\toprule
|
||||
\textbf{HTTP wall-clock} & \textbf{Valor} \\
|
||||
\midrule
|
||||
Promedio & 1{,}81\,s \\
|
||||
M\'in / M\'ax & 1{,}62 / 2{,}42\,s \\
|
||||
Throughput secuencial & $\approx$0{,}55 PDF/s ($\approx$1\,980/h) \\
|
||||
Tiempo total corrida HTTP & $\approx$42\,s (23 archivos) \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Resumen HTTP (sin concurrencia). Fuente: CSV del 13 jul 2026.}
|
||||
\label{tab:count-http}
|
||||
\end{table}
|
||||
|
||||
\textbf{Lectura:}
|
||||
\begin{itemize}
|
||||
\item \textbf{100\,\% PASS} en 23/23 PDFs (HTTP y Go).
|
||||
\item El cuello de botella es \textbf{OCR Tesseract} ($\sim$1{,}7--2{,}3\,s por factura).
|
||||
\item Render PDF $\sim$130\,ms; clustering $<$2\,ms.
|
||||
\item Cada factura devuelve $\sim$41--48 l\'ineas de servicio detectadas.
|
||||
\item A $\sim$2\,s/PDF secuencial $\Rightarrow$ $\approx$1\,800--2\,000 conteos/hora en este host si estuviera dedicado al count-api.
|
||||
\end{itemize}
|
||||
|
||||
\section{Detalle por archivo}
|
||||
El listado completo (HTTP + ms por etapa) est\'a en:
|
||||
\texttt{docs/plan\_pruebas/informes/count\_latency\_20260713.csv}.
|
||||
|
||||
Ejemplo de fila (\texttt{500000284335937.pdf}):
|
||||
\begin{center}
|
||||
\small
|
||||
\begin{tabular}{@{}lr@{}}
|
||||
Render & 155\,ms \\
|
||||
OCR & 2279\,ms \\
|
||||
Cluster & 1\,ms \\
|
||||
Total Go & 2435\,ms \\
|
||||
HTTP & 2{,}42\,s \\
|
||||
Servicios & 44 \\
|
||||
Status & ok \\
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{C\'omo reproducir}
|
||||
\begin{lstlisting}[language=bash]
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
\chapter{Extracci\'on LLM: bench de latencia (ST-EXT-02)}
|
||||
\label{ch:llm-latency}
|
||||
|
||||
\callout{\textbf{Veredicto LLM:} \pass\ (23/23)}{%
|
||||
Modelo \textbf{gpt-5.4-mini}; 20 campos congelados.\\
|
||||
p50 $=\mathbf{7{,}114}$\,s $\Rightarrow$ $\approx\mathbf{506}$ fact/h (1 worker).\\
|
||||
0 errores, 0$\times$429. Coste corrida est.\ USD 0{,}59.
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'ua / resultado / significado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.2cm}p{4.2cm}p{5.2cm}@{}}
|
||||
\toprule
|
||||
\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\
|
||||
\midrule
|
||||
Qu\'e eval\'ua & Latencia punta a punta ingest+LLM con mismos campos & Comparabilidad entre PDFs \\
|
||||
C\'omo & Script \texttt{st\_ext\_02\_llm\_bench} & Schema congelado en JSON \\
|
||||
Resultado & 23 ok; p50 7{,}1\,s; p95 8{,}6\,s & Etapa LLM cubre 139/h \\
|
||||
Significado & $\approx$506 fact/h (1\,w); $\approx$1012 (2\,w) & Cuello del mes no es el LLM en esta muestra \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
El script mide wall-clock por PDF
|
||||
(\texttt{POST /extraction/schemas/upload} $\rightarrow$ \texttt{POST /tasks/extraction}
|
||||
$\rightarrow$ \texttt{POST /tasks/\{id\}/ingest}).
|
||||
|
||||
\section{C\'omo ejecutar}
|
||||
\begin{lstlisting}[language=bash,caption={ST-EXT-02 (Windows PowerShell)}]
|
||||
.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1
|
||||
.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10 -Model gpt-5.4-mini
|
||||
\end{lstlisting}
|
||||
|
||||
Schema congelado: \texttt{docs/plan\_pruebas/informes/st\_ext\_02\_schema.json}.
|
||||
CSV: \texttt{informes/llm\_latency\_20260717.csv}.
|
||||
|
||||
\section{Resultados (2026-07-17)}
|
||||
\texttt{task\_id=aeee04f9-34c5-4cb0-a303-353dde128265}; modo ingest sync en esta campa\~na.
|
||||
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}llrl@{}}
|
||||
\toprule
|
||||
\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\
|
||||
\midrule
|
||||
Modelo & gpt-5.4-mini & --- & 20 campos fijos \\
|
||||
PDFs ok / total & 23 / 23 & --- & 0 fallos, 0$\times$429 \\
|
||||
wall\_s promedio & 7{,}326 & s & \\
|
||||
wall\_s p50 & \textbf{7{,}114} & s & \\
|
||||
wall\_s p95 & 8{,}582 & s & min 6{,}463 / max 9{,}944 \\
|
||||
Throughput 1 worker & \textbf{$\approx$506} & fact/h & $3600/\mathrm{p50}$ \\
|
||||
Throughput 2 workers & $\approx$1\,012 & fact/h & sin cuota RPM \\
|
||||
Coste corrida (est.) & 0{,}5911 & USD & $23\times 0{,}0257$ \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{ST-EXT-02 --- latencia LLM medida (mismo schema/campos).}
|
||||
\label{tab:llm-latency}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.85\textwidth,
|
||||
height=5.5cm,
|
||||
ylabel={Facturas / hora},
|
||||
symbolic x coords={LLM-1w,LLM-2w,Obj-139,Obj-570},
|
||||
xtick=data,
|
||||
nodes near coords,
|
||||
bar width=14pt,
|
||||
ymin=0, ymax=1200,
|
||||
enlarge x limits=0.15,
|
||||
grid=major,
|
||||
title={Throughput LLM vs objetivos 100k/mes}
|
||||
]
|
||||
\addplot[fill=ipsblue!70] coordinates {(LLM-1w,506) (LLM-2w,1012) (Obj-139,139) (Obj-570,570)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{La etapa LLM sola supera 139/h; con 2 workers supera 570/h (sin cuota).}
|
||||
\label{fig:llm-vs-obj}
|
||||
\end{figure}
|
||||
|
||||
\section{Lectura para capacidad}
|
||||
Detalle del modelo 100k/mes (pipeline, OCR, coste USD\,2\,570) en el documento
|
||||
\textbf{Capacidad100k\_IPSExtractor.pdf}. Aqu\'i solo: la etapa LLM \textbf{no} es el cuello
|
||||
en el host actual frente a OCR/contenci\'on.
|
||||
|
||||
\subsection*{Nota agent Selenium}
|
||||
633 PDFs / 90\,min $\Rightarrow$ $\approx$422 fact/h (1 agent PC) --- tambi\'en cubre 139/h en descarga.
|
||||
@@ -0,0 +1,83 @@
|
||||
\chapter{Cola ARQ: bench ST-EXT-01 (LLM mock)}
|
||||
\label{ch:arq-ext01}
|
||||
|
||||
\callout{\textbf{Veredicto ST-EXT-01:} \pass}{%
|
||||
30/30 encolados y drenados; modo \textbf{async}; mock LLM.\\
|
||||
Enqueue 1{,}3\,s; drain 22{,}5\,s; throughput cola $\approx$4\,500 docs/h (mock, no LLM real).
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'ua / resultado / significado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.2cm}p{4.5cm}p{5.0cm}@{}}
|
||||
\toprule
|
||||
\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\
|
||||
\midrule
|
||||
Qu\'e eval\'ua & Enqueue + drenado ARQ con LLM mock & Capacidad de cola, no latencia OpenAI \\
|
||||
C\'omo & \texttt{st\_ext\_01\_arq\_bench} + mock :8099 & \texttt{ASYNC\_QUEUE=true}, 1 worker \\
|
||||
Resultado & 30/30 queued; drained; 23{,}9\,s total & Cola operativa en shared8g \\
|
||||
Significado & Prod debe usar async + workers & El cuello real sigue siendo OCR/LLM real \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Condiciones de la corrida}
|
||||
\begin{itemize}
|
||||
\item Fecha: 2026-07-17; perfil \texttt{shared8g}; \texttt{Limit=30}.
|
||||
\item \texttt{task\_id=25890e3d-9e07-46a7-b961-046421d26eb6}.
|
||||
\item Mock OpenAI-compatible (\texttt{llm\_mock\_server}, delay $\sim$50\,ms).
|
||||
\item \texttt{ASYNC\_QUEUE=true}; \texttt{EXTRACTOR\_BASE\_URL}$\rightarrow$ mock; 1 r\'eplica worker.
|
||||
\item CSV: \texttt{informes/st\_ext\_01\_arq\_20260717.csv}.
|
||||
\end{itemize}
|
||||
|
||||
\section{Resultados}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}llrl@{}}
|
||||
\toprule
|
||||
\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\
|
||||
\midrule
|
||||
mode & async & --- & \texttt{queued=true} en ingest \\
|
||||
Encolados & 30 / 30 & docs & 0 fallos HTTP \\
|
||||
enqueue wall & 1{,}307 & s & avg 0{,}043\,s/doc \\
|
||||
drain & 22{,}544 & s & backlog vaciado \\
|
||||
total & 23{,}851 & s & enqueue + drain \\
|
||||
processed & 30 & docs & \pass\ drained \\
|
||||
Throughput cola & $\approx$4\,528 & docs/h & \textbf{solo mock}; no usar como SLO LLM \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{ST-EXT-01 --- cola ARQ con LLM mock (shared8g, 30 docs).}
|
||||
\label{tab:st-ext-01}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.75\textwidth,
|
||||
height=5.2cm,
|
||||
ylabel={Segundos},
|
||||
symbolic x coords={Enqueue,Drain,Total},
|
||||
xtick=data,
|
||||
nodes near coords,
|
||||
bar width=18pt,
|
||||
ymin=0, ymax=30,
|
||||
enlarge x limits=0.25,
|
||||
grid=major,
|
||||
title={ST-EXT-01 --- tiempos (30 docs, mock)}
|
||||
]
|
||||
\addplot[fill=ipsblue!70] coordinates {(Enqueue,1.307) (Drain,22.544) (Total,23.851)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Casi todo el tiempo es drenado por workers; el enqueue es despreciable.}
|
||||
\label{fig:st-ext-01-times}
|
||||
\end{figure}
|
||||
|
||||
\section{Lectura}
|
||||
\begin{itemize}
|
||||
\item La arquitectura \textbf{Gateway $\rightarrow$ Redis/ARQ $\rightarrow$ workers} funciona bajo el perfil shared8g.
|
||||
\item El throughput $\approx$4\,500 docs/h \textbf{no} es capacidad de extracci\'on real:
|
||||
ST-EXT-02 midió $\approx$506 fact/h con gpt-5.4-mini.
|
||||
\item Recomendaci\'on prod: \texttt{ASYNC\_QUEUE=true} + workers dimensionados a la cuota LLM.
|
||||
\item Pendiente de escala: perfil \texttt{lab} (100--500 docs, 2 workers).
|
||||
\end{itemize}
|
||||
@@ -0,0 +1,85 @@
|
||||
\chapter{Count-api bajo carga (ST-COUNT-01)}
|
||||
\label{ch:st-count-01}
|
||||
|
||||
\callout{\textbf{Veredicto ST-COUNT-01:} \pass\ (shared8g)}{%
|
||||
9/9 PDFs reales OK; 0$\times$5xx; sin OOM observado.\\
|
||||
Bajo concurrencia 3, p50 sube a \textbf{35{,}8\,s} (vs $\sim$2\,s en serie).\\
|
||||
Throughput observado $\approx$\textbf{325} fact/h en la ventana.
|
||||
}
|
||||
|
||||
\section{Qu\'e eval\'ua / resultado / significado}
|
||||
\begin{center}
|
||||
\begin{tabular}{@{}p{3.2cm}p{4.5cm}p{5.0cm}@{}}
|
||||
\toprule
|
||||
\textbf{Pregunta} & \textbf{Respuesta} & \textbf{Implicaci\'on} \\
|
||||
\midrule
|
||||
Qu\'e eval\'ua & OCR count-api con lotes concurrentes & Cuello de 100k/mes \\
|
||||
C\'omo & \texttt{st\_count\_01\_bench} 60\,s, max 3 workers & PDFs reales en \texttt{data/} \\
|
||||
Resultado & 9 ok; p50 35{,}8\,s; 0 fallos & Contenci\'on fuerte vs serie \\
|
||||
Significado & Host shared: $\sim$100--325 fact/h & vs 139/h \'ambar/OK; vs 570/h NO \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\end{center}
|
||||
|
||||
\section{Condiciones}
|
||||
\begin{itemize}
|
||||
\item Fecha: 2026-07-17; perfil \texttt{shared8g}; rate objetivo 1/s; max 3 concurrentes; 60\,s.
|
||||
\item Endpoint: \texttt{POST /count/v1/count} (campo multipart \texttt{files}).
|
||||
\item CSV: \texttt{informes/st\_count\_01\_20260717.csv}.
|
||||
\item Nota: k6 no estaba en el PATH de Windows; se us\'o el bench Python equivalente.
|
||||
El script k6 se corrigi\'o (\texttt{files} en lugar de \texttt{file}).
|
||||
\end{itemize}
|
||||
|
||||
\section{Resultados}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}llrl@{}}
|
||||
\toprule
|
||||
\textbf{M\'etrica} & \textbf{Valor} & \textbf{Unidad} & \textbf{Notas} \\
|
||||
\midrule
|
||||
ok / total & 9 / 9 & --- & 0 fallos \\
|
||||
wall\_s p50 & \textbf{35{,}825} & s & bajo 3 concurrentes \\
|
||||
wall\_s p95 & 43{,}927 & s & min 17{,}6 / max 44{,}7 \\
|
||||
wall total & 99{,}7 & s & incluye drenado final \\
|
||||
Throughput observado & $\approx$325 & fact/h & $9/99{,}7\times 3600$ \\
|
||||
Throughput serie (p50) & $\approx$100 & fact/h & $3600/\mathrm{p50}$ \\
|
||||
$\times$3 ideal & $\approx$301 & fact/h & sin m\'as contenci\'on \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{ST-COUNT-01 --- carga OCR (PDFs reales, shared8g).}
|
||||
\label{tab:st-count-01}
|
||||
\end{table}
|
||||
|
||||
\begin{figure}[ht]
|
||||
\centering
|
||||
\begin{tikzpicture}
|
||||
\begin{axis}[
|
||||
ybar,
|
||||
width=0.8\textwidth,
|
||||
height=5.5cm,
|
||||
ylabel={Facturas / hora},
|
||||
symbolic x coords={Serie-p50,Obs-3conc,Obj-139,Obj-570},
|
||||
xtick=data,
|
||||
x tick label style={font=\small},
|
||||
nodes near coords,
|
||||
bar width=12pt,
|
||||
ymin=0, ymax=700,
|
||||
enlarge x limits=0.15,
|
||||
grid=major,
|
||||
title={OCR ST-COUNT-01 vs objetivos 100k}
|
||||
]
|
||||
\addplot[fill=ipsblue!70] coordinates {(Serie-p50,100) (Obs-3conc,325) (Obj-139,139) (Obj-570,570)};
|
||||
\end{axis}
|
||||
\end{tikzpicture}
|
||||
\caption{Bajo 3 concurrentes el OCR cubre 139/h en esta muestra; no alcanza 570/h.
|
||||
La latencia por PDF se degrada mucho frente a la serie ($\sim$2\,s).}
|
||||
\label{fig:st-count-01}
|
||||
\end{figure}
|
||||
|
||||
\section{Lectura}
|
||||
\begin{itemize}
|
||||
\item \pass\ funcional: count-api aguanta concurrencia ligera sin 5xx/OOM en esta corrida.
|
||||
\item La degradaci\'on 2\,s $\rightarrow$ 36\,s confirma que el OCR es sensible a CPU/RAM compartida.
|
||||
\item Actualiza la fila OCR del modelo 100k: de $\sim$40 estimados a $\sim$100--325 medidos (shared8g).
|
||||
\item Para afirmar 570/h hace falta host dedicado + pool OCR (perfil lab / ST-COUNT agresivo).
|
||||
\end{itemize}
|
||||
@@ -1,4 +1,24 @@
|
||||
file,http_time_s,http_code,service_count,status
|
||||
500000284335937.pdf,4.449556,200,1,ok
|
||||
500000284335940.pdf,2.290720,200,1,ok
|
||||
500000284335954.pdf,2.190165,200,1,ok
|
||||
file,http_time_s,http_code,service_count,status,render_ms,ocr_ms,cluster_ms,total_ms
|
||||
500000284335937.pdf,2.419290,200,1,ok,155,2279,1,2435
|
||||
500000284335940.pdf,2.189024,200,1,ok,134,1820,0,1954
|
||||
500000284335954.pdf,1.615784,200,1,ok,127,1851,0,1980
|
||||
500000284335956.pdf,1.956593,200,1,ok,147,1846,1,1994
|
||||
500000284335963.pdf,1.981622,200,1,ok,132,1689,1,1823
|
||||
500000284335964.pdf,1.696154,200,1,ok,117,1837,0,1955
|
||||
500000284335968.pdf,1.948234,200,1,ok,150,1813,1,1965
|
||||
500000284335997.pdf,1.659651,200,1,ok,125,1746,1,1873
|
||||
500000284336071.pdf,1.667741,200,1,ok,127,1808,0,1937
|
||||
500000284336521.pdf,1.741292,200,1,ok,130,1816,0,1947
|
||||
500000284336836.pdf,1.916852,200,1,ok,143,2285,1,2430
|
||||
500000284336915.pdf,1.679965,200,1,ok,144,1853,0,1999
|
||||
500000284337016.pdf,1.666228,200,1,ok,138,1877,0,2016
|
||||
500000284337418.pdf,1.642242,200,1,ok,133,1829,1,1963
|
||||
500000284337861.pdf,1.768077,200,1,ok,142,1730,1,1875
|
||||
500000284338027.pdf,1.677103,200,1,ok,116,2068,0,2186
|
||||
500000284338092.pdf,1.667427,200,1,ok,109,1792,0,1901
|
||||
500000284338214.pdf,1.781158,200,1,ok,128,1873,0,2002
|
||||
500000284338327.pdf,1.796004,200,1,ok,126,1897,0,2024
|
||||
500000284338353.pdf,1.803265,200,1,ok,108,1731,0,1840
|
||||
500000284338458.pdf,1.824801,200,1,ok,135,2144,0,2280
|
||||
500000284338503.pdf,1.897579,200,1,ok,117,1766,0,1884
|
||||
500000284338624.pdf,1.635177,200,1,ok,126,1788,0,1916
|
||||
|
||||
|
@@ -0,0 +1,24 @@
|
||||
file,wall_s,http_code,status,queued,cost_usd_est,error
|
||||
500000284335937.pdf,9.944,200,ok,0,0.0257,
|
||||
500000284335940.pdf,7.925,200,ok,0,0.0257,
|
||||
500000284335954.pdf,7.659,200,ok,0,0.0257,
|
||||
500000284335956.pdf,7.114,200,ok,0,0.0257,
|
||||
500000284335963.pdf,6.997,200,ok,0,0.0257,
|
||||
500000284335964.pdf,7.889,200,ok,0,0.0257,
|
||||
500000284335968.pdf,7.001,200,ok,0,0.0257,
|
||||
500000284335997.pdf,6.822,200,ok,0,0.0257,
|
||||
500000284336071.pdf,6.463,200,ok,0,0.0257,
|
||||
500000284336521.pdf,7.472,200,ok,0,0.0257,
|
||||
500000284336836.pdf,7.199,200,ok,0,0.0257,
|
||||
500000284336915.pdf,7.094,200,ok,0,0.0257,
|
||||
500000284337016.pdf,6.874,200,ok,0,0.0257,
|
||||
500000284337418.pdf,7.146,200,ok,0,0.0257,
|
||||
500000284337861.pdf,6.816,200,ok,0,0.0257,
|
||||
500000284338027.pdf,7.161,200,ok,0,0.0257,
|
||||
500000284338092.pdf,7.927,200,ok,0,0.0257,
|
||||
500000284338214.pdf,6.563,200,ok,0,0.0257,
|
||||
500000284338327.pdf,8.655,200,ok,0,0.0257,
|
||||
500000284338353.pdf,6.963,200,ok,0,0.0257,
|
||||
500000284338458.pdf,7.327,200,ok,0,0.0257,
|
||||
500000284338503.pdf,6.672,200,ok,0,0.0257,
|
||||
500000284338624.pdf,6.816,200,ok,0,0.0257,
|
||||
|
@@ -0,0 +1 @@
|
||||
file,wall_s,http_code,status,queued,error
|
||||
|
@@ -0,0 +1,10 @@
|
||||
file,wall_s,http_code,status,service_count,error
|
||||
500000284335954.pdf,17.562,200,ok,1,
|
||||
500000284335940.pdf,18.557,200,ok,1,
|
||||
500000284335937.pdf,19.556,200,ok,1,
|
||||
500000284335956.pdf,35.423,200,ok,1,
|
||||
500000284335963.pdf,35.825,200,ok,1,
|
||||
500000284335964.pdf,35.909,200,ok,1,
|
||||
500000284335997.pdf,42.798,200,ok,1,
|
||||
500000284336071.pdf,41.739,200,ok,1,
|
||||
500000284335968.pdf,44.679,200,ok,1,
|
||||
|
@@ -0,0 +1,31 @@
|
||||
radicado,file,enqueue_s,http_code,status,queued,error
|
||||
500000284335937_0000,500000284335937.pdf,0.23,200,queued,1,
|
||||
500000284335940_0001,500000284335940.pdf,0.042,200,queued,1,
|
||||
500000284335954_0002,500000284335954.pdf,0.026,200,queued,1,
|
||||
500000284335956_0003,500000284335956.pdf,0.042,200,queued,1,
|
||||
500000284335963_0004,500000284335963.pdf,0.052,200,queued,1,
|
||||
500000284335964_0005,500000284335964.pdf,0.025,200,queued,1,
|
||||
500000284335968_0006,500000284335968.pdf,0.028,200,queued,1,
|
||||
500000284335997_0007,500000284335997.pdf,0.035,200,queued,1,
|
||||
500000284336071_0008,500000284336071.pdf,0.029,200,queued,1,
|
||||
500000284336521_0009,500000284336521.pdf,0.028,200,queued,1,
|
||||
500000284336836_0010,500000284336836.pdf,0.031,200,queued,1,
|
||||
500000284336915_0011,500000284336915.pdf,0.041,200,queued,1,
|
||||
500000284337016_0012,500000284337016.pdf,0.046,200,queued,1,
|
||||
500000284337418_0013,500000284337418.pdf,0.043,200,queued,1,
|
||||
500000284337861_0014,500000284337861.pdf,0.058,200,queued,1,
|
||||
500000284338027_0015,500000284338027.pdf,0.045,200,queued,1,
|
||||
500000284338092_0016,500000284338092.pdf,0.084,200,queued,1,
|
||||
500000284338214_0017,500000284338214.pdf,0.041,200,queued,1,
|
||||
500000284338327_0018,500000284338327.pdf,0.057,200,queued,1,
|
||||
500000284338353_0019,500000284338353.pdf,0.027,200,queued,1,
|
||||
500000284338458_0020,500000284338458.pdf,0.023,200,queued,1,
|
||||
500000284338503_0021,500000284338503.pdf,0.028,200,queued,1,
|
||||
500000284338624_0022,500000284338624.pdf,0.042,200,queued,1,
|
||||
500000284335937_0023,500000284335937.pdf,0.026,200,queued,1,
|
||||
500000284335940_0024,500000284335940.pdf,0.021,200,queued,1,
|
||||
500000284335954_0025,500000284335954.pdf,0.026,200,queued,1,
|
||||
500000284335956_0026,500000284335956.pdf,0.022,200,queued,1,
|
||||
500000284335963_0027,500000284335963.pdf,0.023,200,queued,1,
|
||||
500000284335964_0028,500000284335964.pdf,0.028,200,queued,1,
|
||||
500000284335968_0029,500000284335968.pdf,0.033,200,queued,1,
|
||||
|
+101
-103
@@ -9,59 +9,67 @@
|
||||
\providecommand\HyField@AuxAddToFields[1]{}
|
||||
\providecommand\HyField@AuxAddToCoFields[2]{}
|
||||
\babel@aux{spanish}{}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {1}Introducci\'on}{6}{chapter.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {1}Introducci\'on}{5}{chapter.1}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:intro}{{1}{6}{Introducci\'on}{chapter.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.1}Objetivo}{6}{section.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.2}Alcance}{6}{section.1.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{6}{subsection.1.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{6}{subsection.1.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{6}{section.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.4}Referencias del sistema}{6}{section.1.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Cobertura existente y gaps.}}{7}{table.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{8}{chapter.2}\protected@file@percent }
|
||||
\newlabel{ch:intro}{{1}{5}{Introducci\'on}{chapter.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.1}Objetivo}{5}{section.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.2}Alcance}{5}{section.1.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{5}{subsection.1.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{5}{subsection.1.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{5}{section.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {1.4}Referencias del sistema}{5}{section.1.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {1.1}{\ignorespaces Cobertura existente y gaps.}}{6}{table.1.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{7}{chapter.2}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:arquitectura}{{2}{8}{Arquitectura bajo prueba}{chapter.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.1}Vista de componentes}{8}{section.2.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura IPSExtractor bajo prueba.}}{8}{figure.2.1}\protected@file@percent }
|
||||
\newlabel{fig:arquitectura}{{2.1}{8}{Arquitectura IPSExtractor bajo prueba}{figure.2.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.2}Inventario de componentes}{8}{section.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.3}Flujo cr\'itico: bot de descarga}{8}{section.2.3}\protected@file@percent }
|
||||
\newlabel{ch:arquitectura}{{2}{7}{Arquitectura bajo prueba}{chapter.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.1}C\'omo est\'a dise\~nado el sistema}{7}{section.2.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.1}{\ignorespaces Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres.}}{7}{figure.2.1}\protected@file@percent }
|
||||
\newlabel{fig:arquitectura}{{2.1}{7}{Arquitectura bajo prueba. El \textbf {gateway no habla con Postgres}: solo hace proxy HTTP. Usan la BD compartida: \texttt {extraction} (+ workers), \texttt {cleaning} y \texttt {bot-descarga}. \texttt {count-api} es OCR sin Postgres}{figure.2.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga.}}{8}{figure.2.2}\protected@file@percent }
|
||||
\newlabel{fig:patrones}{{2.2}{8}{Patrones: Gateway, cola ARQ y claim. El servicio \texttt {bot-descarga} \textbf {orquesta}; el \textbf {agent Selenium} en el PC ejecuta la descarga}{figure.2.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.3}Pipeline de negocio}{8}{section.2.3}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.3}{\ignorespaces Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats.}}{9}{figure.2.3}\protected@file@percent }
|
||||
\newlabel{fig:pipeline-negocio}{{2.3}{9}{Pipeline punta a punta. La descarga la hace el \textbf {agent Selenium} (PC); el servicio \texttt {bot-descarga} solo orquesta jobs/claim/heartbeats}{figure.2.3}{}}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {2.1}{\ignorespaces Componentes del sistema bajo prueba.}}{9}{table.2.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.2}{\ignorespaces Secuencia del flujo bot de descarga.}}{9}{figure.2.2}\protected@file@percent }
|
||||
\newlabel{fig:flujo-bot}{{2.2}{9}{Secuencia del flujo bot de descarga}{figure.2.2}{}}
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {2.1}Prefijos MinIO del bot}{9}{lstlisting.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.4}Otros flujos de negocio}{9}{section.2.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{10}{chapter.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.4}Inventario de componentes}{9}{section.2.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.5}Flujo cr\'itico: descarga (orquestador + agent Selenium)}{9}{section.2.5}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {2.1}Prefijos MinIO del pipeline de descarga}{9}{lstlisting.2.1}\protected@file@percent }
|
||||
\@writefile{lof}{\contentsline {figure}{\numberline {2.4}{\ignorespaces Secuencia del flujo bot de descarga.}}{10}{figure.2.4}\protected@file@percent }
|
||||
\newlabel{fig:flujo-bot}{{2.4}{10}{Secuencia del flujo bot de descarga}{figure.2.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {2.6}Otros flujos de negocio}{10}{section.2.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{11}{chapter.3}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:estrategia}{{3}{10}{Estrategia de pruebas}{chapter.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.1}Pir\'amide}{10}{section.3.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Pir\'amide de pruebas IPSExtractor.}}{10}{table.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{10}{section.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.3}Entornos}{10}{section.3.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.2}{\ignorespaces TC = Testcontainers; LS = LocalStack.}}{11}{table.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.4}Mocks vs sistemas reales}{11}{section.3.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.5}Principios}{11}{section.3.5}\protected@file@percent }
|
||||
\newlabel{ch:estrategia}{{3}{11}{Estrategia de pruebas}{chapter.3}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.1}Pir\'amide}{11}{section.3.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.1}{\ignorespaces Pir\'amide de pruebas IPSExtractor.}}{11}{table.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{11}{section.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.3}C\'omo leer un caso del cat\'alogo}{11}{section.3.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.4}Entornos}{11}{section.3.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.5}Mocks vs sistemas reales}{12}{section.3.5}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {3.2}{\ignorespaces TC = Testcontainers; LS = LocalStack.}}{12}{table.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {3.6}Principios}{12}{section.3.6}\protected@file@percent }
|
||||
\gdef \LT@i {\LT@entry
|
||||
{2}{74.28644pt}\LT@entry
|
||||
{1}{131.50148pt}\LT@entry
|
||||
{1}{162.49014pt}}
|
||||
\gdef \LT@ii {\LT@entry
|
||||
{1}{79.97733pt}\LT@entry
|
||||
{1}{68.59596pt}\LT@entry
|
||||
{1}{111.58464pt}\LT@entry
|
||||
{1}{125.81102pt}\LT@entry
|
||||
{1}{162.49014pt}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {4}Pruebas unitarias}{12}{chapter.4}\protected@file@percent }
|
||||
{1}{91.35826pt}}
|
||||
\gdef \LT@ii {\LT@entry
|
||||
{1}{74.28644pt}\LT@entry
|
||||
{1}{108.73918pt}\LT@entry
|
||||
{1}{120.12054pt}\LT@entry
|
||||
{1}{91.35826pt}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {4}Pruebas unitarias}{13}{chapter.4}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:unitarias}{{4}{12}{Pruebas unitarias}{chapter.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.1}Gateway}{12}{section.4.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.1}{Unitarias gateway.}}{12}{table.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{12}{section.4.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.2}{Unitarias bot-descarga API.}}{12}{table.4.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.3}Agent bot (PC)}{12}{section.4.3}\protected@file@percent }
|
||||
\newlabel{ch:unitarias}{{4}{13}{Pruebas unitarias}{chapter.4}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.1}Gateway}{13}{section.4.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.1}{Unitarias gateway.}}{13}{table.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{13}{section.4.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.2}{Unitarias bot-descarga API.}}{13}{table.4.2}\protected@file@percent }
|
||||
\gdef \LT@iii {\LT@entry
|
||||
{1}{79.97733pt}\LT@entry
|
||||
{1}{125.81102pt}\LT@entry
|
||||
@@ -70,11 +78,12 @@
|
||||
{1}{79.97733pt}\LT@entry
|
||||
{1}{131.50148pt}\LT@entry
|
||||
{1}{156.79968pt}}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.3}{Unitarias agent.}}{13}{table.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{13}{section.4.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.4}{Unitarias resto de servicios.}}{13}{table.4.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{13}{section.4.5}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {4.1}Suite unitaria r\'apida}{13}{lstlisting.4.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.3}Agent bot (PC)}{14}{section.4.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.3}{Unitarias agent.}}{14}{table.4.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{14}{section.4.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {4.4}{Unitarias resto de servicios.}}{14}{table.4.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{14}{section.4.5}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {4.1}Suite unitaria r\'apida}{14}{lstlisting.4.1}\protected@file@percent }
|
||||
\gdef \LT@v {\LT@entry
|
||||
{1}{74.28644pt}\LT@entry
|
||||
{1}{131.50148pt}\LT@entry
|
||||
@@ -83,62 +92,57 @@
|
||||
{1}{79.97733pt}\LT@entry
|
||||
{1}{140.0374pt}\LT@entry
|
||||
{1}{148.26378pt}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{14}{chapter.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{15}{chapter.5}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:integracion}{{5}{14}{Pruebas de integraci\'on}{chapter.5}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{14}{section.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.2}Gateway}{14}{section.5.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.1}{Integraci\'on gateway.}}{14}{table.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.3}Bot-descarga}{14}{section.5.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.2}{Integraci\'on bot-descarga.}}{14}{table.5.2}\protected@file@percent }
|
||||
\newlabel{ch:integracion}{{5}{15}{Pruebas de integraci\'on}{chapter.5}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{15}{section.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.2}Gateway}{15}{section.5.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.1}{Integraci\'on gateway.}}{15}{table.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.3}Bot-descarga}{15}{section.5.3}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.2}{Integraci\'on bot-descarga.}}{15}{table.5.2}\protected@file@percent }
|
||||
\gdef \LT@vii {\LT@entry
|
||||
{1}{79.97733pt}\LT@entry
|
||||
{1}{140.0374pt}\LT@entry
|
||||
{1}{148.26378pt}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{15}{section.5.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.3}{Integraci\'on extraction, cleaning, count y BD.}}{15}{table.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{15}{section.5.5}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {5.1}Smoke health}{15}{lstlisting.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{16}{section.5.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {5.3}{Integraci\'on extraction, cleaning, count y BD.}}{16}{table.5.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{16}{section.5.5}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {5.1}Smoke health}{16}{lstlisting.5.1}\protected@file@percent }
|
||||
\gdef \LT@viii {\LT@entry
|
||||
{1}{57.21504pt}\LT@entry
|
||||
{1}{91.6678pt}\LT@entry
|
||||
{1}{154.26378pt}\LT@entry
|
||||
{1}{91.35826pt}}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{16}{chapter.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{17}{chapter.6}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:e2e}{{6}{16}{Pruebas end-to-end}{chapter.6}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{16}{section.6.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {6.1}{Casos E2E cr\'iticos.}}{16}{table.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{16}{section.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{16}{subsection.6.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.2}Pasos}{17}{subsection.6.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.3}Asserts}{17}{subsection.6.2.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.3}Desktop}{17}{section.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{18}{chapter.7}\protected@file@percent }
|
||||
\newlabel{ch:e2e}{{6}{17}{Pruebas end-to-end}{chapter.6}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{17}{section.6.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {6.1}{Casos E2E cr\'iticos.}}{17}{table.6.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{17}{section.6.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{17}{subsection.6.2.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.2}Pasos}{18}{subsection.6.2.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {6.2.3}Asserts}{18}{subsection.6.2.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {6.3}Desktop}{18}{section.6.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{19}{chapter.7}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:estres}{{7}{18}{Pruebas de estr\'es y carga}{chapter.7}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.1}Herramienta}{18}{section.7.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.2}Perfiles de carga}{18}{section.7.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces Cargas por perfil de entorno.}}{18}{table.7.1}\protected@file@percent }
|
||||
\newlabel{tab:perfiles-carga}{{7.1}{18}{Cargas por perfil de entorno}{table.7.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{18}{section.7.3}\protected@file@percent }
|
||||
\gdef \LT@ix {\LT@entry
|
||||
{1}{74.28644pt}\LT@entry
|
||||
{1}{103.04872pt}\LT@entry
|
||||
{1}{111.58464pt}\LT@entry
|
||||
{1}{97.04872pt}}
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.2}{Escenarios de estr\'es.}}{19}{table.7.2}\protected@file@percent }
|
||||
\newlabel{ch:estres}{{7}{19}{Pruebas de estr\'es y carga}{chapter.7}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.1}Herramienta}{19}{section.7.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.2}Perfiles de carga}{19}{section.7.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.1}{\ignorespaces Cargas por perfil de entorno.}}{19}{table.7.1}\protected@file@percent }
|
||||
\newlabel{tab:perfiles-carga}{{7.1}{19}{Cargas por perfil de entorno}{table.7.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{19}{section.7.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.4}Scripts k6 incluidos}{19}{section.7.4}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.3}{\ignorespaces Mapeo script $\leftrightarrow $ escenario.}}{19}{table.7.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{19}{section.7.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{19}{subsection.7.5.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.2}{\ignorespaces Escenarios de estr\'es (objetivo y riesgo).}}{20}{table.7.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {7.3}{\ignorespaces Mapeo script $\leftrightarrow $ escenario.}}{20}{table.7.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{20}{section.7.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{20}{subsection.7.5.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {7.5.2}Perfil laboratorio}{20}{subsection.7.5.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {7.6}Ejecuci\'on ejemplo}{20}{section.7.6}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {7.1}Estr\'es gateway en host compartido}{20}{lstlisting.7.1}\protected@file@percent }
|
||||
\gdef \LT@x {\LT@entry
|
||||
\gdef \LT@ix {\LT@entry
|
||||
{1}{57.21504pt}\LT@entry
|
||||
{1}{111.58464pt}\LT@entry
|
||||
{1}{125.81102pt}\LT@entry
|
||||
@@ -167,7 +171,7 @@
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {9.1.3}Carga concurrente del host (no IPS)}{24}{subsection.9.1.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {9.1.4}Requisitos previos a una campa\~na de estr\'es}{24}{subsection.9.1.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.2}Fixtures recomendadas}{24}{section.9.2}\protected@file@percent }
|
||||
\gdef \LT@xi {\LT@entry
|
||||
\gdef \LT@x {\LT@entry
|
||||
{1}{125.50148pt}\LT@entry
|
||||
{1}{233.62204pt}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {9.3}Variables de entorno relevantes}{25}{section.9.3}\protected@file@percent }
|
||||
@@ -183,7 +187,7 @@
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.3}Compilar este documento}{26}{section.10.3}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {10.1}latexmk}{26}{lstlisting.10.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {10.4}Prioridad de implementaci\'on de tests (ROI)}{27}{section.10.4}\protected@file@percent }
|
||||
\gdef \LT@xii {\LT@entry
|
||||
\gdef \LT@xi {\LT@entry
|
||||
{1}{48.67912pt}\LT@entry
|
||||
{1}{140.0374pt}\LT@entry
|
||||
{1}{182.407pt}}
|
||||
@@ -198,29 +202,23 @@
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}\protected@file@percent }
|
||||
\@writefile{lol}{\contentsline {lstlisting}{\numberline {11.1}Campos m\'inimos del informe}{29}{lstlisting.11.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {12}Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{30}{chapter.12}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\newlabel{ch:informe-2026-07-13}{{12}{30}{Informe de ejecuci\'on --- campa\~na 2026-07-13}{chapter.12}{}}
|
||||
\newlabel{ch:informe-2026-07-13}{{12}{30}{Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{chapter.12}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Metadatos de la campa\~na.}}{30}{table.12.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {12.2}{\ignorespaces Resumen de la campa\~na 2026-07-13.}}{31}{table.12.2}\protected@file@percent }
|
||||
\newlabel{tab:campana-2026-07-13}{{12.2}{31}{Resumen de la campa\~na 2026-07-13}{table.12.2}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.2}Resultados resumidos}{30}{section.12.2}\protected@file@percent }
|
||||
\@writefile{lot}{\contentsline {table}{\numberline {12.1}{\ignorespaces Resumen; detalle en el Informe de Pruebas.}}{30}{table.12.1}\protected@file@percent }
|
||||
\newlabel{tab:campana-2026-07-13}{{12.1}{30}{Resumen; detalle en el Informe de Pruebas}{table.12.1}{}}
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.3}Lecci\'on operativa}{30}{section.12.3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {section}{\numberline {12.4}Pendiente respecto a este plan}{31}{section.12.4}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {A}Glosario}{32}{appendix.Alph1}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {B}Checklist de release}{33}{appendix.Alph2}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}\protected@file@percent }
|
||||
\@writefile{toc}{\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{34}{appendix.Alph3}\protected@file@percent }
|
||||
\@writefile{lof}{\addvspace {10\p@ }}
|
||||
\@writefile{lot}{\addvspace {10\p@ }}
|
||||
\gdef \@abspage@last{36}
|
||||
\gdef \@abspage@last{35}
|
||||
|
||||
+74
-76
@@ -6,79 +6,77 @@
|
||||
\BOOKMARK [1][-]{section.1.3}{\376\377\000E\000s\000t\000a\000d\000o\000\040\000a\000c\000t\000u\000a\000l\000\040\000d\000e\000l\000\040\000r\000e\000p\000o\000s\000i\000t\000o\000r\000i\000o}{chapter.1}% 6
|
||||
\BOOKMARK [1][-]{section.1.4}{\376\377\000R\000e\000f\000e\000r\000e\000n\000c\000i\000a\000s\000\040\000d\000e\000l\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.1}% 7
|
||||
\BOOKMARK [0][-]{chapter.2}{\376\377\000A\000r\000q\000u\000i\000t\000e\000c\000t\000u\000r\000a\000\040\000b\000a\000j\000o\000\040\000p\000r\000u\000e\000b\000a}{}% 8
|
||||
\BOOKMARK [1][-]{section.2.1}{\376\377\000V\000i\000s\000t\000a\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 9
|
||||
\BOOKMARK [1][-]{section.2.2}{\376\377\000I\000n\000v\000e\000n\000t\000a\000r\000i\000o\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 10
|
||||
\BOOKMARK [1][-]{section.2.3}{\376\377\000F\000l\000u\000j\000o\000\040\000c\000r\000\355\000t\000i\000c\000o\000:\000\040\000b\000o\000t\000\040\000d\000e\000\040\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.2}% 11
|
||||
\BOOKMARK [1][-]{section.2.4}{\376\377\000O\000t\000r\000o\000s\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 12
|
||||
\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000s\000t\000r\000a\000t\000e\000g\000i\000a\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000s}{}% 13
|
||||
\BOOKMARK [1][-]{section.3.1}{\376\377\000P\000i\000r\000\341\000m\000i\000d\000e}{chapter.3}% 14
|
||||
\BOOKMARK [1][-]{section.3.2}{\376\377\000C\000o\000n\000v\000e\000n\000c\000i\000\363\000n\000\040\000d\000e\000\040\000i\000d\000e\000n\000t\000i\000f\000i\000c\000a\000d\000o\000r\000e\000s}{chapter.3}% 15
|
||||
\BOOKMARK [1][-]{section.3.3}{\376\377\000E\000n\000t\000o\000r\000n\000o\000s}{chapter.3}% 16
|
||||
\BOOKMARK [1][-]{section.3.4}{\376\377\000M\000o\000c\000k\000s\000\040\000v\000s\000\040\000s\000i\000s\000t\000e\000m\000a\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.3}% 17
|
||||
\BOOKMARK [1][-]{section.3.5}{\376\377\000P\000r\000i\000n\000c\000i\000p\000i\000o\000s}{chapter.3}% 18
|
||||
\BOOKMARK [0][-]{chapter.4}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 19
|
||||
\BOOKMARK [1][-]{section.4.1}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.4}% 20
|
||||
\BOOKMARK [1][-]{section.4.2}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000A\000P\000I\000\040\000\050\000J\000o\000b\000S\000t\000o\000r\000e\000\051}{chapter.4}% 21
|
||||
\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000g\000e\000n\000t\000\040\000b\000o\000t\000\040\000\050\000P\000C\000\051}{chapter.4}% 22
|
||||
\BOOKMARK [1][-]{section.4.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t\000\040\000/\000\040\000D\000e\000s\000k\000t\000o\000p}{chapter.4}% 23
|
||||
\BOOKMARK [1][-]{section.4.5}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.4}% 24
|
||||
\BOOKMARK [0][-]{chapter.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{}% 25
|
||||
\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{chapter.5}% 26
|
||||
\BOOKMARK [1][-]{section.5.2}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.5}% 27
|
||||
\BOOKMARK [1][-]{section.5.3}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.5}% 28
|
||||
\BOOKMARK [1][-]{section.5.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t}{chapter.5}% 29
|
||||
\BOOKMARK [1][-]{section.5.5}{\376\377\000S\000m\000o\000k\000e\000\040\000m\000\355\000n\000i\000m\000o\000\040\000p\000o\000s\000t\000-\000c\000o\000m\000p\000o\000s\000e}{chapter.5}% 30
|
||||
\BOOKMARK [0][-]{chapter.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000e\000n\000d\000-\000t\000o\000-\000e\000n\000d}{}% 31
|
||||
\BOOKMARK [1][-]{section.6.1}{\376\377\000M\000a\000t\000r\000i\000z\000\040\000d\000e\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.6}% 32
|
||||
\BOOKMARK [1][-]{section.6.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000E\0002\000E\000-\0000\0005\000\040\000\050\000b\000o\000t\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\051}{chapter.6}% 33
|
||||
\BOOKMARK [2][-]{subsection.6.2.1}{\376\377\000P\000r\000e\000c\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{section.6.2}% 34
|
||||
\BOOKMARK [2][-]{subsection.6.2.2}{\376\377\000P\000a\000s\000o\000s}{section.6.2}% 35
|
||||
\BOOKMARK [2][-]{subsection.6.2.3}{\376\377\000A\000s\000s\000e\000r\000t\000s}{section.6.2}% 36
|
||||
\BOOKMARK [1][-]{section.6.3}{\376\377\000D\000e\000s\000k\000t\000o\000p}{chapter.6}% 37
|
||||
\BOOKMARK [0][-]{chapter.7}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s\000\040\000y\000\040\000c\000a\000r\000g\000a}{}% 38
|
||||
\BOOKMARK [1][-]{section.7.1}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a}{chapter.7}% 39
|
||||
\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000e\000r\000f\000i\000l\000e\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{chapter.7}% 40
|
||||
\BOOKMARK [1][-]{section.7.3}{\376\377\000E\000s\000c\000e\000n\000a\000r\000i\000o\000s\000\040\000\050\000c\000a\000t\000\341\000l\000o\000g\000o\000\040\000c\000o\000m\000p\000l\000e\000t\000o\000\051}{chapter.7}% 41
|
||||
\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000c\000r\000i\000p\000t\000s\000\040\000k\0006\000\040\000i\000n\000c\000l\000u\000i\000d\000o\000s}{chapter.7}% 42
|
||||
\BOOKMARK [1][-]{section.7.5}{\376\377\000S\000L\000O\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{chapter.7}% 43
|
||||
\BOOKMARK [2][-]{subsection.7.5.1}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000h\000o\000s\000t\000-\000c\000o\000m\000p\000a\000r\000t\000i\000d\000o\000-\0008\000g\000b\000\040\000\050\000d\000e\000f\000a\000u\000l\000t\000\051}{section.7.5}% 44
|
||||
\BOOKMARK [2][-]{subsection.7.5.2}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000l\000a\000b\000o\000r\000a\000t\000o\000r\000i\000o}{section.7.5}% 45
|
||||
\BOOKMARK [1][-]{section.7.6}{\376\377\000E\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\000e\000j\000e\000m\000p\000l\000o}{chapter.7}% 46
|
||||
\BOOKMARK [0][-]{chapter.8}{\376\377\000R\000e\000s\000i\000l\000i\000e\000n\000c\000i\000a\000\040\000y\000\040\000c\000h\000a\000o\000s}{}% 47
|
||||
\BOOKMARK [1][-]{section.8.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 48
|
||||
\BOOKMARK [1][-]{section.8.2}{\376\377\000C\000a\000s\000o\000s}{chapter.8}% 49
|
||||
\BOOKMARK [1][-]{section.8.3}{\376\377\000M\000o\000d\000o\000s\000\040\000d\000e\000\040\000f\000a\000l\000l\000o\000\040\000c\000o\000n\000o\000c\000i\000d\000o\000s\000\040\000\050\000b\000o\000t\000\051}{chapter.8}% 50
|
||||
\BOOKMARK [1][-]{section.8.4}{\376\377\000C\000r\000i\000t\000e\000r\000i\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.8}% 51
|
||||
\BOOKMARK [0][-]{chapter.9}{\376\377\000D\000a\000t\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000\040\000y\000\040\000e\000n\000t\000o\000r\000n\000o\000s}{}% 52
|
||||
\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000o\000s\000t\000\040\000d\000o\000n\000d\000e\000\040\000s\000e\000\040\000e\000j\000e\000c\000u\000t\000a\000n\000\040\000l\000a\000s\000\040\000p\000r\000u\000e\000b\000a\000s}{chapter.9}% 53
|
||||
\BOOKMARK [2][-]{subsection.9.1.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{section.9.1}% 54
|
||||
\BOOKMARK [2][-]{subsection.9.1.2}{\376\377\000S\000t\000a\000c\000k\000\040\000I\000P\000S\000E\000x\000t\000r\000a\000c\000t\000o\000r\000\040\000o\000b\000s\000e\000r\000v\000a\000d\000o\000\040\000\050\000i\000d\000l\000e\000\051}{section.9.1}% 55
|
||||
\BOOKMARK [2][-]{subsection.9.1.3}{\376\377\000C\000a\000r\000g\000a\000\040\000c\000o\000n\000c\000u\000r\000r\000e\000n\000t\000e\000\040\000d\000e\000l\000\040\000h\000o\000s\000t\000\040\000\050\000n\000o\000\040\000I\000P\000S\000\051}{section.9.1}% 56
|
||||
\BOOKMARK [2][-]{subsection.9.1.4}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000p\000r\000e\000v\000i\000o\000s\000\040\000a\000\040\000u\000n\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s}{section.9.1}% 57
|
||||
\BOOKMARK [1][-]{section.9.2}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000d\000a\000s}{chapter.9}% 58
|
||||
\BOOKMARK [1][-]{section.9.3}{\376\377\000V\000a\000r\000i\000a\000b\000l\000e\000s\000\040\000d\000e\000\040\000e\000n\000t\000o\000r\000n\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.9}% 59
|
||||
\BOOKMARK [1][-]{section.9.4}{\376\377\000S\000e\000g\000u\000r\000i\000d\000a\000d\000\040\000e\000n\000\040\000e\000n\000t\000o\000r\000n\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.9}% 60
|
||||
\BOOKMARK [0][-]{chapter.10}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s\000\040\000p\000r\000o\000f\000e\000s\000i\000o\000n\000a\000l\000e\000s\000\040\000y\000\040\000C\000I}{}% 61
|
||||
\BOOKMARK [1][-]{section.10.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000t\000o\000o\000l\000i\000n\000g}{chapter.10}% 62
|
||||
\BOOKMARK [1][-]{section.10.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000C\000I\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000\040\000\050\000n\000o\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000e\000n\000t\000r\000e\000g\000a\000\051}{chapter.10}% 63
|
||||
\BOOKMARK [1][-]{section.10.3}{\376\377\000C\000o\000m\000p\000i\000l\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.10}% 64
|
||||
\BOOKMARK [1][-]{section.10.4}{\376\377\000P\000r\000i\000o\000r\000i\000d\000a\000d\000\040\000d\000e\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000c\000i\000\363\000n\000\040\000d\000e\000\040\000t\000e\000s\000t\000s\000\040\000\050\000R\000O\000I\000\051}{chapter.10}% 65
|
||||
\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000r\000o\000n\000o\000g\000r\000a\000m\000a\000\040\000y\000\040\000c\000r\000i\000t\000e\000r\000i\000o\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{}% 66
|
||||
\BOOKMARK [1][-]{section.11.1}{\376\377\000F\000a\000s\000e\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000a\000s}{chapter.11}% 67
|
||||
\BOOKMARK [1][-]{section.11.2}{\376\377\000D\000e\000f\000i\000n\000i\000t\000i\000o\000n\000\040\000o\000f\000\040\000D\000o\000n\000e\000\040\000p\000o\000r\000\040\000r\000e\000l\000e\000a\000s\000e}{chapter.11}% 68
|
||||
\BOOKMARK [1][-]{section.11.3}{\376\377\000C\000a\000m\000p\000a\000\361\000a\000\040\000r\000e\000g\000i\000s\000t\000r\000a\000d\000a}{chapter.11}% 69
|
||||
\BOOKMARK [1][-]{section.11.4}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 70
|
||||
\BOOKMARK [1][-]{section.11.5}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 71
|
||||
\BOOKMARK [0][-]{chapter.12}{\376\377\000I\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\040\024\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003}{}% 72
|
||||
\BOOKMARK [1][-]{section.12.1}{\376\377\000M\000e\000t\000a\000d\000a\000t\000o\000s}{chapter.12}% 73
|
||||
\BOOKMARK [1][-]{section.12.2}{\376\377\000B\000a\000s\000e\000l\000i\000n\000e\000\040\000d\000e\000l\000\040\000e\000n\000t\000o\000r\000n\000o}{chapter.12}% 74
|
||||
\BOOKMARK [1][-]{section.12.3}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000p\000o\000r\000\040\000c\000a\000s\000o}{chapter.12}% 75
|
||||
\BOOKMARK [2][-]{subsection.12.3.1}{\376\377\000S\000T\000-\000G\000W\000-\0000\0001\000\040\040\024\000\040\000g\000a\000t\000e\000w\000a\000y\000\040\000h\000e\000a\000l\000t\000h}{section.12.3}% 76
|
||||
\BOOKMARK [2][-]{subsection.12.3.2}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0001\000\051}{section.12.3}% 77
|
||||
\BOOKMARK [2][-]{subsection.12.3.3}{\376\377\000S\000T\000-\000B\000O\000T\000-\0000\0001\000\040\040\024\000\040\000h\000e\000a\000r\000t\000b\000e\000a\000t\000s\000\040\000\050\000c\000o\000r\000r\000i\000d\000a\000\040\0002\000,\000\040\000r\000e\000i\000n\000t\000e\000n\000t\000o\000\051}{section.12.3}% 78
|
||||
\BOOKMARK [1][-]{section.12.4}{\376\377\000H\000a\000l\000l\000a\000z\000g\000o\000s}{chapter.12}% 79
|
||||
\BOOKMARK [1][-]{section.12.5}{\376\377\000N\000o\000\040\000e\000j\000e\000c\000u\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000c\000a\000m\000p\000a\000\361\000a}{chapter.12}% 80
|
||||
\BOOKMARK [1][-]{section.12.6}{\376\377\000F\000o\000l\000l\000o\000w\000-\000u\000p\000s}{chapter.12}% 81
|
||||
\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 82
|
||||
\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 83
|
||||
\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 84
|
||||
\BOOKMARK [1][-]{section.2.1}{\376\377\000C\000\363\000m\000o\000\040\000e\000s\000t\000\341\000\040\000d\000i\000s\000e\000\361\000a\000d\000o\000\040\000e\000l\000\040\000s\000i\000s\000t\000e\000m\000a}{chapter.2}% 9
|
||||
\BOOKMARK [1][-]{section.2.2}{\376\377\000P\000a\000t\000r\000o\000n\000e\000s\000\040\000d\000e\000\040\000d\000i\000s\000e\000\361\000o}{chapter.2}% 10
|
||||
\BOOKMARK [1][-]{section.2.3}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 11
|
||||
\BOOKMARK [1][-]{section.2.4}{\376\377\000I\000n\000v\000e\000n\000t\000a\000r\000i\000o\000\040\000d\000e\000\040\000c\000o\000m\000p\000o\000n\000e\000n\000t\000e\000s}{chapter.2}% 12
|
||||
\BOOKMARK [1][-]{section.2.5}{\376\377\000F\000l\000u\000j\000o\000\040\000c\000r\000\355\000t\000i\000c\000o\000:\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000\050\000o\000r\000q\000u\000e\000s\000t\000a\000d\000o\000r\000\040\000+\000\040\000a\000g\000e\000n\000t\000\040\000S\000e\000l\000e\000n\000i\000u\000m\000\051}{chapter.2}% 13
|
||||
\BOOKMARK [1][-]{section.2.6}{\376\377\000O\000t\000r\000o\000s\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.2}% 14
|
||||
\BOOKMARK [0][-]{chapter.3}{\376\377\000E\000s\000t\000r\000a\000t\000e\000g\000i\000a\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000s}{}% 15
|
||||
\BOOKMARK [1][-]{section.3.1}{\376\377\000P\000i\000r\000\341\000m\000i\000d\000e}{chapter.3}% 16
|
||||
\BOOKMARK [1][-]{section.3.2}{\376\377\000C\000o\000n\000v\000e\000n\000c\000i\000\363\000n\000\040\000d\000e\000\040\000i\000d\000e\000n\000t\000i\000f\000i\000c\000a\000d\000o\000r\000e\000s}{chapter.3}% 17
|
||||
\BOOKMARK [1][-]{section.3.3}{\376\377\000C\000\363\000m\000o\000\040\000l\000e\000e\000r\000\040\000u\000n\000\040\000c\000a\000s\000o\000\040\000d\000e\000l\000\040\000c\000a\000t\000\341\000l\000o\000g\000o}{chapter.3}% 18
|
||||
\BOOKMARK [1][-]{section.3.4}{\376\377\000E\000n\000t\000o\000r\000n\000o\000s}{chapter.3}% 19
|
||||
\BOOKMARK [1][-]{section.3.5}{\376\377\000M\000o\000c\000k\000s\000\040\000v\000s\000\040\000s\000i\000s\000t\000e\000m\000a\000s\000\040\000r\000e\000a\000l\000e\000s}{chapter.3}% 20
|
||||
\BOOKMARK [1][-]{section.3.6}{\376\377\000P\000r\000i\000n\000c\000i\000p\000i\000o\000s}{chapter.3}% 21
|
||||
\BOOKMARK [0][-]{chapter.4}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000u\000n\000i\000t\000a\000r\000i\000a\000s}{}% 22
|
||||
\BOOKMARK [1][-]{section.4.1}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.4}% 23
|
||||
\BOOKMARK [1][-]{section.4.2}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a\000\040\000A\000P\000I\000\040\000\050\000J\000o\000b\000S\000t\000o\000r\000e\000\051}{chapter.4}% 24
|
||||
\BOOKMARK [1][-]{section.4.3}{\376\377\000A\000g\000e\000n\000t\000\040\000b\000o\000t\000\040\000\050\000P\000C\000\051}{chapter.4}% 25
|
||||
\BOOKMARK [1][-]{section.4.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t\000\040\000/\000\040\000D\000e\000s\000k\000t\000o\000p}{chapter.4}% 26
|
||||
\BOOKMARK [1][-]{section.4.5}{\376\377\000C\000o\000m\000a\000n\000d\000o\000s\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.4}% 27
|
||||
\BOOKMARK [0][-]{chapter.5}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{}% 28
|
||||
\BOOKMARK [1][-]{section.5.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000i\000n\000t\000e\000g\000r\000a\000c\000i\000\363\000n}{chapter.5}% 29
|
||||
\BOOKMARK [1][-]{section.5.2}{\376\377\000G\000a\000t\000e\000w\000a\000y}{chapter.5}% 30
|
||||
\BOOKMARK [1][-]{section.5.3}{\376\377\000B\000o\000t\000-\000d\000e\000s\000c\000a\000r\000g\000a}{chapter.5}% 31
|
||||
\BOOKMARK [1][-]{section.5.4}{\376\377\000E\000x\000t\000r\000a\000c\000t\000i\000o\000n\000\040\000/\000\040\000C\000l\000e\000a\000n\000i\000n\000g\000\040\000/\000\040\000C\000o\000u\000n\000t}{chapter.5}% 32
|
||||
\BOOKMARK [1][-]{section.5.5}{\376\377\000S\000m\000o\000k\000e\000\040\000m\000\355\000n\000i\000m\000o\000\040\000p\000o\000s\000t\000-\000c\000o\000m\000p\000o\000s\000e}{chapter.5}% 33
|
||||
\BOOKMARK [0][-]{chapter.6}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000e\000n\000d\000-\000t\000o\000-\000e\000n\000d}{}% 34
|
||||
\BOOKMARK [1][-]{section.6.1}{\376\377\000M\000a\000t\000r\000i\000z\000\040\000d\000e\000\040\000f\000l\000u\000j\000o\000s\000\040\000d\000e\000\040\000n\000e\000g\000o\000c\000i\000o}{chapter.6}% 35
|
||||
\BOOKMARK [1][-]{section.6.2}{\376\377\000D\000e\000t\000a\000l\000l\000e\000\040\000E\0002\000E\000-\0000\0005\000\040\000\050\000b\000o\000t\000\040\000d\000e\000s\000c\000a\000r\000g\000a\000\051}{chapter.6}% 36
|
||||
\BOOKMARK [2][-]{subsection.6.2.1}{\376\377\000P\000r\000e\000c\000o\000n\000d\000i\000c\000i\000o\000n\000e\000s}{section.6.2}% 37
|
||||
\BOOKMARK [2][-]{subsection.6.2.2}{\376\377\000P\000a\000s\000o\000s}{section.6.2}% 38
|
||||
\BOOKMARK [2][-]{subsection.6.2.3}{\376\377\000A\000s\000s\000e\000r\000t\000s}{section.6.2}% 39
|
||||
\BOOKMARK [1][-]{section.6.3}{\376\377\000D\000e\000s\000k\000t\000o\000p}{chapter.6}% 40
|
||||
\BOOKMARK [0][-]{chapter.7}{\376\377\000P\000r\000u\000e\000b\000a\000s\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s\000\040\000y\000\040\000c\000a\000r\000g\000a}{}% 41
|
||||
\BOOKMARK [1][-]{section.7.1}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a}{chapter.7}% 42
|
||||
\BOOKMARK [1][-]{section.7.2}{\376\377\000P\000e\000r\000f\000i\000l\000e\000s\000\040\000d\000e\000\040\000c\000a\000r\000g\000a}{chapter.7}% 43
|
||||
\BOOKMARK [1][-]{section.7.3}{\376\377\000E\000s\000c\000e\000n\000a\000r\000i\000o\000s\000\040\000\050\000c\000a\000t\000\341\000l\000o\000g\000o\000\040\000c\000o\000m\000p\000l\000e\000t\000o\000\051}{chapter.7}% 44
|
||||
\BOOKMARK [1][-]{section.7.4}{\376\377\000S\000c\000r\000i\000p\000t\000s\000\040\000k\0006\000\040\000i\000n\000c\000l\000u\000i\000d\000o\000s}{chapter.7}% 45
|
||||
\BOOKMARK [1][-]{section.7.5}{\376\377\000S\000L\000O\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{chapter.7}% 46
|
||||
\BOOKMARK [2][-]{subsection.7.5.1}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000h\000o\000s\000t\000-\000c\000o\000m\000p\000a\000r\000t\000i\000d\000o\000-\0008\000g\000b\000\040\000\050\000d\000e\000f\000a\000u\000l\000t\000\051}{section.7.5}% 47
|
||||
\BOOKMARK [2][-]{subsection.7.5.2}{\376\377\000P\000e\000r\000f\000i\000l\000\040\000l\000a\000b\000o\000r\000a\000t\000o\000r\000i\000o}{section.7.5}% 48
|
||||
\BOOKMARK [1][-]{section.7.6}{\376\377\000E\000j\000e\000c\000u\000c\000i\000\363\000n\000\040\000e\000j\000e\000m\000p\000l\000o}{chapter.7}% 49
|
||||
\BOOKMARK [0][-]{chapter.8}{\376\377\000R\000e\000s\000i\000l\000i\000e\000n\000c\000i\000a\000\040\000y\000\040\000c\000h\000a\000o\000s}{}% 50
|
||||
\BOOKMARK [1][-]{section.8.1}{\376\377\000O\000b\000j\000e\000t\000i\000v\000o}{chapter.8}% 51
|
||||
\BOOKMARK [1][-]{section.8.2}{\376\377\000C\000a\000s\000o\000s}{chapter.8}% 52
|
||||
\BOOKMARK [1][-]{section.8.3}{\376\377\000M\000o\000d\000o\000s\000\040\000d\000e\000\040\000f\000a\000l\000l\000o\000\040\000c\000o\000n\000o\000c\000i\000d\000o\000s\000\040\000\050\000b\000o\000t\000\051}{chapter.8}% 53
|
||||
\BOOKMARK [1][-]{section.8.4}{\376\377\000C\000r\000i\000t\000e\000r\000i\000o\000\040\000g\000l\000o\000b\000a\000l}{chapter.8}% 54
|
||||
\BOOKMARK [0][-]{chapter.9}{\376\377\000D\000a\000t\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a\000\040\000y\000\040\000e\000n\000t\000o\000r\000n\000o\000s}{}% 55
|
||||
\BOOKMARK [1][-]{section.9.1}{\376\377\000H\000o\000s\000t\000\040\000d\000o\000n\000d\000e\000\040\000s\000e\000\040\000e\000j\000e\000c\000u\000t\000a\000n\000\040\000l\000a\000s\000\040\000p\000r\000u\000e\000b\000a\000s}{chapter.9}% 56
|
||||
\BOOKMARK [2][-]{subsection.9.1.1}{\376\377\000H\000a\000r\000d\000w\000a\000r\000e\000\040\000y\000\040\000s\000i\000s\000t\000e\000m\000a}{section.9.1}% 57
|
||||
\BOOKMARK [2][-]{subsection.9.1.2}{\376\377\000S\000t\000a\000c\000k\000\040\000I\000P\000S\000E\000x\000t\000r\000a\000c\000t\000o\000r\000\040\000o\000b\000s\000e\000r\000v\000a\000d\000o\000\040\000\050\000i\000d\000l\000e\000\051}{section.9.1}% 58
|
||||
\BOOKMARK [2][-]{subsection.9.1.3}{\376\377\000C\000a\000r\000g\000a\000\040\000c\000o\000n\000c\000u\000r\000r\000e\000n\000t\000e\000\040\000d\000e\000l\000\040\000h\000o\000s\000t\000\040\000\050\000n\000o\000\040\000I\000P\000S\000\051}{section.9.1}% 59
|
||||
\BOOKMARK [2][-]{subsection.9.1.4}{\376\377\000R\000e\000q\000u\000i\000s\000i\000t\000o\000s\000\040\000p\000r\000e\000v\000i\000o\000s\000\040\000a\000\040\000u\000n\000a\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\000d\000e\000\040\000e\000s\000t\000r\000\351\000s}{section.9.1}% 60
|
||||
\BOOKMARK [1][-]{section.9.2}{\376\377\000F\000i\000x\000t\000u\000r\000e\000s\000\040\000r\000e\000c\000o\000m\000e\000n\000d\000a\000d\000a\000s}{chapter.9}% 61
|
||||
\BOOKMARK [1][-]{section.9.3}{\376\377\000V\000a\000r\000i\000a\000b\000l\000e\000s\000\040\000d\000e\000\040\000e\000n\000t\000o\000r\000n\000o\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{chapter.9}% 62
|
||||
\BOOKMARK [1][-]{section.9.4}{\376\377\000S\000e\000g\000u\000r\000i\000d\000a\000d\000\040\000e\000n\000\040\000e\000n\000t\000o\000r\000n\000o\000s\000\040\000d\000e\000\040\000p\000r\000u\000e\000b\000a}{chapter.9}% 63
|
||||
\BOOKMARK [0][-]{chapter.10}{\376\377\000H\000e\000r\000r\000a\000m\000i\000e\000n\000t\000a\000s\000\040\000p\000r\000o\000f\000e\000s\000i\000o\000n\000a\000l\000e\000s\000\040\000y\000\040\000C\000I}{}% 64
|
||||
\BOOKMARK [1][-]{section.10.1}{\376\377\000S\000t\000a\000c\000k\000\040\000d\000e\000\040\000t\000o\000o\000l\000i\000n\000g}{chapter.10}% 65
|
||||
\BOOKMARK [1][-]{section.10.2}{\376\377\000P\000i\000p\000e\000l\000i\000n\000e\000\040\000C\000I\000\040\000o\000b\000j\000e\000t\000i\000v\000o\000\040\000\050\000n\000o\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000d\000o\000\040\000e\000n\000\040\000e\000s\000t\000a\000\040\000e\000n\000t\000r\000e\000g\000a\000\051}{chapter.10}% 66
|
||||
\BOOKMARK [1][-]{section.10.3}{\376\377\000C\000o\000m\000p\000i\000l\000a\000r\000\040\000e\000s\000t\000e\000\040\000d\000o\000c\000u\000m\000e\000n\000t\000o}{chapter.10}% 67
|
||||
\BOOKMARK [1][-]{section.10.4}{\376\377\000P\000r\000i\000o\000r\000i\000d\000a\000d\000\040\000d\000e\000\040\000i\000m\000p\000l\000e\000m\000e\000n\000t\000a\000c\000i\000\363\000n\000\040\000d\000e\000\040\000t\000e\000s\000t\000s\000\040\000\050\000R\000O\000I\000\051}{chapter.10}% 68
|
||||
\BOOKMARK [0][-]{chapter.11}{\376\377\000C\000r\000o\000n\000o\000g\000r\000a\000m\000a\000\040\000y\000\040\000c\000r\000i\000t\000e\000r\000i\000o\000s\000\040\000d\000e\000\040\000a\000c\000e\000p\000t\000a\000c\000i\000\363\000n}{}% 69
|
||||
\BOOKMARK [1][-]{section.11.1}{\376\377\000F\000a\000s\000e\000s\000\040\000s\000u\000g\000e\000r\000i\000d\000a\000s}{chapter.11}% 70
|
||||
\BOOKMARK [1][-]{section.11.2}{\376\377\000D\000e\000f\000i\000n\000i\000t\000i\000o\000n\000\040\000o\000f\000\040\000D\000o\000n\000e\000\040\000p\000o\000r\000\040\000r\000e\000l\000e\000a\000s\000e}{chapter.11}% 71
|
||||
\BOOKMARK [1][-]{section.11.3}{\376\377\000C\000a\000m\000p\000a\000\361\000a\000\040\000r\000e\000g\000i\000s\000t\000r\000a\000d\000a}{chapter.11}% 72
|
||||
\BOOKMARK [1][-]{section.11.4}{\376\377\000P\000l\000a\000n\000t\000i\000l\000l\000a\000\040\000d\000e\000\040\000i\000n\000f\000o\000r\000m\000e\000\040\000d\000e\000\040\000e\000j\000e\000c\000u\000c\000i\000\363\000n}{chapter.11}% 73
|
||||
\BOOKMARK [1][-]{section.11.5}{\376\377\000R\000e\000s\000p\000o\000n\000s\000a\000b\000l\000e\000s\000\040\000\050\000r\000o\000l\000e\000s\000\051}{chapter.11}% 74
|
||||
\BOOKMARK [0][-]{chapter.12}{\376\377\000E\000s\000t\000a\000d\000o\000\040\000d\000e\000\040\000c\000a\000m\000p\000a\000\361\000a\000\040\040\024\000\040\0002\0000\0002\0006\000-\0000\0007\000-\0001\0003\000\040\000\050\000+\000\040\000L\000L\000M\000\040\0001\0007\000-\000j\000u\000l\000\051}{}% 75
|
||||
\BOOKMARK [1][-]{section.12.1}{\376\377\000M\000e\000t\000a\000d\000a\000t\000o\000s}{chapter.12}% 76
|
||||
\BOOKMARK [1][-]{section.12.2}{\376\377\000R\000e\000s\000u\000l\000t\000a\000d\000o\000s\000\040\000r\000e\000s\000u\000m\000i\000d\000o\000s}{chapter.12}% 77
|
||||
\BOOKMARK [1][-]{section.12.3}{\376\377\000L\000e\000c\000c\000i\000\363\000n\000\040\000o\000p\000e\000r\000a\000t\000i\000v\000a}{chapter.12}% 78
|
||||
\BOOKMARK [1][-]{section.12.4}{\376\377\000P\000e\000n\000d\000i\000e\000n\000t\000e\000\040\000r\000e\000s\000p\000e\000c\000t\000o\000\040\000a\000\040\000e\000s\000t\000e\000\040\000p\000l\000a\000n}{chapter.12}% 79
|
||||
\BOOKMARK [0][-]{appendix.Alph1}{\376\377\000G\000l\000o\000s\000a\000r\000i\000o}{}% 80
|
||||
\BOOKMARK [0][-]{appendix.Alph2}{\376\377\000C\000h\000e\000c\000k\000l\000i\000s\000t\000\040\000d\000e\000\040\000r\000e\000l\000e\000a\000s\000e}{}% 81
|
||||
\BOOKMARK [0][-]{appendix.Alph3}{\376\377\000M\000a\000p\000a\000\040\000d\000e\000\040\000r\000u\000t\000a\000s\000\040\000r\000e\000l\000e\000v\000a\000n\000t\000e\000s}{}% 82
|
||||
|
||||
+18
-21
@@ -6,25 +6,22 @@
|
||||
|
||||
\makecover
|
||||
|
||||
\chapter*{Control de cambios}
|
||||
\begin{tabularx}{\textwidth}{@{}l l X@{}}
|
||||
\toprule
|
||||
\textbf{Versi\'on} & \textbf{Fecha} & \textbf{Descripci\'on} \\
|
||||
\midrule
|
||||
1.0.0 & 13 jul 2026 & Versi\'on inicial del plan de pruebas y estr\'es. \\
|
||||
1.1.0 & 13 jul 2026 & Entorno real de ejecuci\'on (host Ubuntu compartido 8\,GiB) y perfiles de carga. \\
|
||||
1.2.0 & 13 jul 2026 & Informe campa\~na k6: smoke, ST-GW-01 PASS, ST-BOT-01 PASS (reintento). \\
|
||||
\bottomrule
|
||||
\end{tabularx}
|
||||
|
||||
\chapter*{Resumen ejecutivo}
|
||||
Este documento define la estrategia y la matriz de pruebas del monorepo \textbf{IPSExtractor}: API Gateway, extraction/workers ARQ, cleaning, count-api (Go), bot-descarga (API + agent Selenium), PostgreSQL, Redis, MinIO y la aplicaci\'on de escritorio.
|
||||
Este documento define la \textbf{estrategia y la matriz de pruebas} del monorepo IPSExtractor:
|
||||
API Gateway, extraction/workers ARQ, cleaning, count-api, bot-descarga (API + agent Selenium),
|
||||
PostgreSQL, Redis, MinIO y la aplicaci\'on de escritorio.
|
||||
|
||||
Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no existe pipeline CI, ni suites de integraci\'on/E2E automatizadas. Este plan cierra ese gap con identificadores trazables (\texttt{UT-*}, \texttt{IT-*}, \texttt{E2E-*}, \texttt{ST-*}, \texttt{CH-*}), SLOs medibles y scripts \textbf{k6} de referencia en \texttt{docs/plan\_pruebas/scripts/}.
|
||||
|
||||
\textbf{Importante:} las pruebas de integraci\'on, humo y estr\'es documentadas contra el stack vivo se ejecutan en el host descrito en la secci\'on~\ref{sec:host-pruebas} (laptop Ubuntu 24.04, 8\,GiB RAM, Docker compartido con MLflow, Airflow, n8n, etc.). Los resultados deben interpretarse en ese contexto: un OOM o p95 alto puede deberse a contenci\'on del host, no solo a IPSExtractor.
|
||||
|
||||
\textbf{Estado campa\~na 2026-07-13:} smoke PASS; ST-GW-01 PASS; ST-BOT-01 PASS en reintento (detalle en cap.~\ref{ch:informe-2026-07-13}).
|
||||
\begin{itemize}
|
||||
\item \textbf{Objetivo:} cerrar el gap entre unitarias aisladas y un plan trazable
|
||||
(UT / IT / E2E / ST / CH) con SLOs y scripts k6.
|
||||
\item \textbf{Arquitectura:} microservicios + API Gateway + cola ARQ + claim del bot
|
||||
(cap.~\ref{ch:arquitectura}).
|
||||
\item \textbf{Entorno de referencia:} host Ubuntu 8\,GiB compartido (sec.~\ref{sec:host-pruebas});
|
||||
perfiles \texttt{shared8g} / \texttt{lab}.
|
||||
\item \textbf{Estado campa\~na:} smoke PASS; ST-GW-01 PASS; ST-BOT-01 PASS (reintento);
|
||||
ST-EXT-02 LLM medido --- detalle en el \emph{Informe de Pruebas}.
|
||||
\item \textbf{Volumen 100k/mes:} modelo y veredicto en el PDF \emph{Capacidad 100k}.
|
||||
\end{itemize}
|
||||
|
||||
\tableofcontents
|
||||
\clearpage
|
||||
@@ -57,13 +54,13 @@ Hoy el repositorio cuenta principalmente con pruebas unitarias aisladas; no exis
|
||||
\chapter{Checklist de release}
|
||||
\begin{enumerate}
|
||||
\item Unitarios verdes: bots, bot-descarga store, Vitest, \texttt{go test}.
|
||||
\item Migraciones Alembic en BD limpia (0001--0004).
|
||||
\item Migraciones Alembic en BD limpia.
|
||||
\item Smoke Compose: \texttt{GET /health/live} y \texttt{GET /health}.
|
||||
\item E2E-01 a E2E-05 ejecutados en staging (E2E-05 con agent real si hay Imaging).
|
||||
\item Al menos ST-GW-01, ST-BOT-01, ST-COUNT-01 y ST-EXT-01 con umbrales cumplidos.
|
||||
\item Informe: p95, error rate, picos RAM, jobs fallidos, costos LLM.
|
||||
\item \textbf{Entorno citado:} hostname, \texttt{free -h}, \texttt{docker stats}, perfil k6 (\texttt{shared8g}/\texttt{lab}).
|
||||
\item Sin secretos hardcodeados en compose para entornos de prueba.
|
||||
\item Informe de resultados + PDF Capacidad 100k actualizados si aplica.
|
||||
\item Entorno citado: hostname, \texttt{free -h}, \texttt{docker stats}, perfil k6.
|
||||
\item Sin secretos hardcodeados en compose de prueba.
|
||||
\end{enumerate}
|
||||
|
||||
\chapter{Mapa de rutas relevantes}
|
||||
|
||||
+53
-55
@@ -1,48 +1,51 @@
|
||||
\babel@toc {spanish}{}\relax
|
||||
\contentsline {chapter}{\numberline {1}Introducci\'on}{6}{chapter.1}%
|
||||
\contentsline {section}{\numberline {1.1}Objetivo}{6}{section.1.1}%
|
||||
\contentsline {section}{\numberline {1.2}Alcance}{6}{section.1.2}%
|
||||
\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{6}{subsection.1.2.1}%
|
||||
\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{6}{subsection.1.2.2}%
|
||||
\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{6}{section.1.3}%
|
||||
\contentsline {section}{\numberline {1.4}Referencias del sistema}{6}{section.1.4}%
|
||||
\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{8}{chapter.2}%
|
||||
\contentsline {section}{\numberline {2.1}Vista de componentes}{8}{section.2.1}%
|
||||
\contentsline {section}{\numberline {2.2}Inventario de componentes}{8}{section.2.2}%
|
||||
\contentsline {section}{\numberline {2.3}Flujo cr\'itico: bot de descarga}{8}{section.2.3}%
|
||||
\contentsline {section}{\numberline {2.4}Otros flujos de negocio}{9}{section.2.4}%
|
||||
\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{10}{chapter.3}%
|
||||
\contentsline {section}{\numberline {3.1}Pir\'amide}{10}{section.3.1}%
|
||||
\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{10}{section.3.2}%
|
||||
\contentsline {section}{\numberline {3.3}Entornos}{10}{section.3.3}%
|
||||
\contentsline {section}{\numberline {3.4}Mocks vs sistemas reales}{11}{section.3.4}%
|
||||
\contentsline {section}{\numberline {3.5}Principios}{11}{section.3.5}%
|
||||
\contentsline {chapter}{\numberline {4}Pruebas unitarias}{12}{chapter.4}%
|
||||
\contentsline {section}{\numberline {4.1}Gateway}{12}{section.4.1}%
|
||||
\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{12}{section.4.2}%
|
||||
\contentsline {section}{\numberline {4.3}Agent bot (PC)}{12}{section.4.3}%
|
||||
\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{13}{section.4.4}%
|
||||
\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{13}{section.4.5}%
|
||||
\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{14}{chapter.5}%
|
||||
\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{14}{section.5.1}%
|
||||
\contentsline {section}{\numberline {5.2}Gateway}{14}{section.5.2}%
|
||||
\contentsline {section}{\numberline {5.3}Bot-descarga}{14}{section.5.3}%
|
||||
\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{15}{section.5.4}%
|
||||
\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{15}{section.5.5}%
|
||||
\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{16}{chapter.6}%
|
||||
\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{16}{section.6.1}%
|
||||
\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{16}{section.6.2}%
|
||||
\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{16}{subsection.6.2.1}%
|
||||
\contentsline {subsection}{\numberline {6.2.2}Pasos}{17}{subsection.6.2.2}%
|
||||
\contentsline {subsection}{\numberline {6.2.3}Asserts}{17}{subsection.6.2.3}%
|
||||
\contentsline {section}{\numberline {6.3}Desktop}{17}{section.6.3}%
|
||||
\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{18}{chapter.7}%
|
||||
\contentsline {section}{\numberline {7.1}Herramienta}{18}{section.7.1}%
|
||||
\contentsline {section}{\numberline {7.2}Perfiles de carga}{18}{section.7.2}%
|
||||
\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{18}{section.7.3}%
|
||||
\contentsline {chapter}{\numberline {1}Introducci\'on}{5}{chapter.1}%
|
||||
\contentsline {section}{\numberline {1.1}Objetivo}{5}{section.1.1}%
|
||||
\contentsline {section}{\numberline {1.2}Alcance}{5}{section.1.2}%
|
||||
\contentsline {subsection}{\numberline {1.2.1}Dentro de alcance}{5}{subsection.1.2.1}%
|
||||
\contentsline {subsection}{\numberline {1.2.2}Fuera de alcance de esta versi\'on documental}{5}{subsection.1.2.2}%
|
||||
\contentsline {section}{\numberline {1.3}Estado actual del repositorio}{5}{section.1.3}%
|
||||
\contentsline {section}{\numberline {1.4}Referencias del sistema}{5}{section.1.4}%
|
||||
\contentsline {chapter}{\numberline {2}Arquitectura bajo prueba}{7}{chapter.2}%
|
||||
\contentsline {section}{\numberline {2.1}C\'omo est\'a dise\~nado el sistema}{7}{section.2.1}%
|
||||
\contentsline {section}{\numberline {2.2}Patrones de dise\~no}{7}{section.2.2}%
|
||||
\contentsline {section}{\numberline {2.3}Pipeline de negocio}{8}{section.2.3}%
|
||||
\contentsline {section}{\numberline {2.4}Inventario de componentes}{9}{section.2.4}%
|
||||
\contentsline {section}{\numberline {2.5}Flujo cr\'itico: descarga (orquestador + agent Selenium)}{9}{section.2.5}%
|
||||
\contentsline {section}{\numberline {2.6}Otros flujos de negocio}{10}{section.2.6}%
|
||||
\contentsline {chapter}{\numberline {3}Estrategia de pruebas}{11}{chapter.3}%
|
||||
\contentsline {section}{\numberline {3.1}Pir\'amide}{11}{section.3.1}%
|
||||
\contentsline {section}{\numberline {3.2}Convenci\'on de identificadores}{11}{section.3.2}%
|
||||
\contentsline {section}{\numberline {3.3}C\'omo leer un caso del cat\'alogo}{11}{section.3.3}%
|
||||
\contentsline {section}{\numberline {3.4}Entornos}{11}{section.3.4}%
|
||||
\contentsline {section}{\numberline {3.5}Mocks vs sistemas reales}{12}{section.3.5}%
|
||||
\contentsline {section}{\numberline {3.6}Principios}{12}{section.3.6}%
|
||||
\contentsline {chapter}{\numberline {4}Pruebas unitarias}{13}{chapter.4}%
|
||||
\contentsline {section}{\numberline {4.1}Gateway}{13}{section.4.1}%
|
||||
\contentsline {section}{\numberline {4.2}Bot-descarga API (\texttt {JobStore})}{13}{section.4.2}%
|
||||
\contentsline {section}{\numberline {4.3}Agent bot (PC)}{14}{section.4.3}%
|
||||
\contentsline {section}{\numberline {4.4}Extraction / Cleaning / Count / Desktop}{14}{section.4.4}%
|
||||
\contentsline {section}{\numberline {4.5}Comandos de ejecuci\'on}{14}{section.4.5}%
|
||||
\contentsline {chapter}{\numberline {5}Pruebas de integraci\'on}{15}{chapter.5}%
|
||||
\contentsline {section}{\numberline {5.1}Stack de integraci\'on}{15}{section.5.1}%
|
||||
\contentsline {section}{\numberline {5.2}Gateway}{15}{section.5.2}%
|
||||
\contentsline {section}{\numberline {5.3}Bot-descarga}{15}{section.5.3}%
|
||||
\contentsline {section}{\numberline {5.4}Extraction / Cleaning / Count}{16}{section.5.4}%
|
||||
\contentsline {section}{\numberline {5.5}Smoke m\'inimo post-compose}{16}{section.5.5}%
|
||||
\contentsline {chapter}{\numberline {6}Pruebas end-to-end}{17}{chapter.6}%
|
||||
\contentsline {section}{\numberline {6.1}Matriz de flujos de negocio}{17}{section.6.1}%
|
||||
\contentsline {section}{\numberline {6.2}Detalle E2E-05 (bot descarga)}{17}{section.6.2}%
|
||||
\contentsline {subsection}{\numberline {6.2.1}Precondiciones}{17}{subsection.6.2.1}%
|
||||
\contentsline {subsection}{\numberline {6.2.2}Pasos}{18}{subsection.6.2.2}%
|
||||
\contentsline {subsection}{\numberline {6.2.3}Asserts}{18}{subsection.6.2.3}%
|
||||
\contentsline {section}{\numberline {6.3}Desktop}{18}{section.6.3}%
|
||||
\contentsline {chapter}{\numberline {7}Pruebas de estr\'es y carga}{19}{chapter.7}%
|
||||
\contentsline {section}{\numberline {7.1}Herramienta}{19}{section.7.1}%
|
||||
\contentsline {section}{\numberline {7.2}Perfiles de carga}{19}{section.7.2}%
|
||||
\contentsline {section}{\numberline {7.3}Escenarios (cat\'alogo completo)}{19}{section.7.3}%
|
||||
\contentsline {section}{\numberline {7.4}Scripts k6 incluidos}{19}{section.7.4}%
|
||||
\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{19}{section.7.5}%
|
||||
\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{19}{subsection.7.5.1}%
|
||||
\contentsline {section}{\numberline {7.5}SLOs de aceptaci\'on}{20}{section.7.5}%
|
||||
\contentsline {subsection}{\numberline {7.5.1}Perfil host-compartido-8gb (default)}{20}{subsection.7.5.1}%
|
||||
\contentsline {subsection}{\numberline {7.5.2}Perfil laboratorio}{20}{subsection.7.5.2}%
|
||||
\contentsline {section}{\numberline {7.6}Ejecuci\'on ejemplo}{20}{section.7.6}%
|
||||
\contentsline {chapter}{\numberline {8}Resiliencia y chaos}{21}{chapter.8}%
|
||||
@@ -70,16 +73,11 @@
|
||||
\contentsline {section}{\numberline {11.3}Campa\~na registrada}{28}{section.11.3}%
|
||||
\contentsline {section}{\numberline {11.4}Plantilla de informe de ejecuci\'on}{29}{section.11.4}%
|
||||
\contentsline {section}{\numberline {11.5}Responsables (roles)}{29}{section.11.5}%
|
||||
\contentsline {chapter}{\numberline {12}Informe de ejecuci\'on --- campa\~na 2026-07-13}{30}{chapter.12}%
|
||||
\contentsline {chapter}{\numberline {12}Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}{30}{chapter.12}%
|
||||
\contentsline {section}{\numberline {12.1}Metadatos}{30}{section.12.1}%
|
||||
\contentsline {section}{\numberline {12.2}Baseline del entorno}{30}{section.12.2}%
|
||||
\contentsline {section}{\numberline {12.3}Resultados por caso}{31}{section.12.3}%
|
||||
\contentsline {subsection}{\numberline {12.3.1}ST-GW-01 --- gateway health}{31}{subsection.12.3.1}%
|
||||
\contentsline {subsection}{\numberline {12.3.2}ST-BOT-01 --- heartbeats (corrida 1)}{31}{subsection.12.3.2}%
|
||||
\contentsline {subsection}{\numberline {12.3.3}ST-BOT-01 --- heartbeats (corrida 2, reintento)}{31}{subsection.12.3.3}%
|
||||
\contentsline {section}{\numberline {12.4}Hallazgos}{31}{section.12.4}%
|
||||
\contentsline {section}{\numberline {12.5}No ejecutado en esta campa\~na}{32}{section.12.5}%
|
||||
\contentsline {section}{\numberline {12.6}Follow-ups}{32}{section.12.6}%
|
||||
\contentsline {chapter}{\numberline {A}Glosario}{33}{appendix.Alph1}%
|
||||
\contentsline {chapter}{\numberline {B}Checklist de release}{34}{appendix.Alph2}%
|
||||
\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{35}{appendix.Alph3}%
|
||||
\contentsline {section}{\numberline {12.2}Resultados resumidos}{30}{section.12.2}%
|
||||
\contentsline {section}{\numberline {12.3}Lecci\'on operativa}{30}{section.12.3}%
|
||||
\contentsline {section}{\numberline {12.4}Pendiente respecto a este plan}{31}{section.12.4}%
|
||||
\contentsline {chapter}{\numberline {A}Glosario}{32}{appendix.Alph1}%
|
||||
\contentsline {chapter}{\numberline {B}Checklist de release}{33}{appendix.Alph2}%
|
||||
\contentsline {chapter}{\numberline {C}Mapa de rutas relevantes}{34}{appendix.Alph3}%
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
\fancyhf{}
|
||||
\fancyhead[L]{\small IPSExtractor --- Plan de pruebas}
|
||||
\fancyhead[R]{\small\thepage}
|
||||
\fancyfoot[C]{\footnotesize Confidencial --- Uso interno}
|
||||
\fancyfoot[C]{\footnotesize Documento para compartir}
|
||||
\renewcommand{\headrulewidth}{0.4pt}
|
||||
\renewcommand{\footrulewidth}{0.2pt}
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
morestring=[b]'
|
||||
}
|
||||
|
||||
\usepackage{float}
|
||||
\usepackage{tikz}
|
||||
\usetikzlibrary{arrows.meta,positioning,fit,calc,shapes.geometric,shapes.multipart}
|
||||
|
||||
@@ -83,8 +84,7 @@
|
||||
pdfsubject={Pruebas unitarias, integracion, E2E, estres y resiliencia}
|
||||
}
|
||||
|
||||
\newcommand{\docversion}{1.2.0}
|
||||
\newcommand{\docdate}{13 de julio de 2026}
|
||||
\newcommand{\docdate}{17 de julio de 2026}
|
||||
\newcommand{\statusbadge}[1]{\texttt{#1}}
|
||||
|
||||
\newcommand{\makecover}{%
|
||||
@@ -95,16 +95,15 @@
|
||||
{\Huge\bfseries IPSExtractor}\\[0.35cm]
|
||||
{\Large\color{ipsgrey} Sistema de extracci\'on y an\'alisis de facturas m\'edicas}\\[0.4cm]
|
||||
{\color{ipsblue}\rule{\textwidth}{2pt}}\\[1.8cm]
|
||||
{\LARGE\bfseries Plan completo de pruebas}\\[0.3cm]
|
||||
{\LARGE\bfseries Plan de pruebas}\\[0.3cm]
|
||||
{\large Unitarias $\cdot$ Integraci\'on $\cdot$ E2E $\cdot$ Estr\'es $\cdot$ Resiliencia}\\[2.2cm]
|
||||
\begin{tabular}{@{}ll@{}}
|
||||
\textbf{Versi\'on} & \docversion \\
|
||||
\textbf{Fecha} & \docdate \\
|
||||
\textbf{Clasificaci\'on} & Uso interno \\
|
||||
\textbf{Herramientas} & pytest, Vitest, go test, k6, Docker Compose, latexmk \\
|
||||
\textbf{Audiencia} & Documento para compartir \\
|
||||
\textbf{Herramientas} & pytest, Vitest, go test, k6, Docker Compose \\
|
||||
\end{tabular}
|
||||
\vfill
|
||||
{\small Generado a partir de la arquitectura real del monorepo IPSExtractor.}\\[0.3cm]
|
||||
{\small Estrategia y matriz de pruebas. Resultados en el Informe; volumen en Capacidad 100k.}\\[0.3cm]
|
||||
{\color{ipsblue}\rule{0.5\textwidth}{0.6pt}}
|
||||
\end{titlepage}
|
||||
}
|
||||
|
||||
@@ -47,11 +47,31 @@ k6 run -e PROFILE=shared8g -e DOWNLOAD_JOB_ID=<id> k6_bot_events.js
|
||||
k6 run -e PROFILE=shared8g k6_smoke_suite.js
|
||||
# Solo si free -h muestra ≥3GiB disponibles:
|
||||
k6 run -e PROFILE=shared8g k6_count_batches.js
|
||||
# o Python (PDFs reales, Windows-friendly):
|
||||
.\docs\plan_pruebas\scripts\st_count_01_bench.ps1 -Profile shared8g
|
||||
|
||||
# Latencia count-api con PDFs reales:
|
||||
bash docs/plan_pruebas/scripts/measure_count_latency.sh
|
||||
|
||||
# Latencia LLM ST-EXT-02 (mismo schema/campos, gpt-5.4-mini):
|
||||
bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh
|
||||
# Windows: .\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10
|
||||
|
||||
# Cola ARQ ST-EXT-01 (LLM mock, sin coste OpenAI):
|
||||
# 1) Mock (otra terminal) + extraction con ASYNC_QUEUE=true y EXTRACTOR_BASE_URL→mock
|
||||
bash docs/plan_pruebas/scripts/llm_mock_server.sh
|
||||
# 2) Bench (shared8g ≈30 docs / lab ≈200):
|
||||
bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh
|
||||
PROFILE=lab ARQ_BENCH_LIMIT=100 bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh
|
||||
# Windows:
|
||||
# .\docs\plan_pruebas\scripts\llm_mock_server.ps1
|
||||
# .\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -Profile shared8g -Limit 30
|
||||
# .\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -StartMock -Limit 20
|
||||
```
|
||||
|
||||
Modelo de capacidad 100k/mes: [`estimate_capacity.md`](estimate_capacity.md). Informe PDF: [`../InformePruebas_IPSExtractor.pdf`](../InformePruebas_IPSExtractor.pdf).
|
||||
|
||||
PDFs de prueba: [`../data/`](../data/) (`500000284335937.pdf`, `...940.pdf`, `...954.pdf`).
|
||||
|
||||
Tras el bench LLM: resultados en [`../informe/sections/13_llm_latency.tex`](../informe/sections/13_llm_latency.tex)
|
||||
(CSV `informes/llm_latency_20260717.csv`). Schema: `informes/st_ext_02_schema.json`.
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""E2E-07 — Crear job de descarga con worker offline debe devolver HTTP 409.
|
||||
|
||||
Pasos:
|
||||
1. POST /jobs (Desktop padre vía gateway → extraction)
|
||||
2. POST /bot-descarga/jobs con worker_id que NO está online (sin heartbeat)
|
||||
3. Assert status == 409
|
||||
|
||||
Uso:
|
||||
python e2e_07_worker_offline.py
|
||||
python e2e_07_worker_offline.py --base-url http://127.0.0.1:8189
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("ERROR: pip install requests", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
# Excel mínimo (ZIP/OOXML vacío no hace falta: el 409 ocurre antes de validar contenido)
|
||||
_MIN_XLSX = (
|
||||
b"PK\x03\x04\x14\x00\x00\x00\x08\x00\x00\x00!\x00"
|
||||
b"e2e07-dummy-xlsx-content-for-multipart"
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description="E2E-07 worker offline → 409")
|
||||
p.add_argument("--base-url", default="http://127.0.0.1:8189")
|
||||
p.add_argument(
|
||||
"--token",
|
||||
default="",
|
||||
help="BOT_API_TOKEN (default: env BOT_API_TOKEN o dev-token-change-me)",
|
||||
)
|
||||
p.add_argument(
|
||||
"--worker-id",
|
||||
default="e2e07-offline-pc",
|
||||
help="Worker que no debe estar online",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
import os
|
||||
|
||||
token = args.token or os.getenv("BOT_API_TOKEN", "dev-token-change-me")
|
||||
base = args.base_url.rstrip("/")
|
||||
headers_bot = {"Authorization": f"Bearer {token}"}
|
||||
worker_id = args.worker_id
|
||||
out: dict = {
|
||||
"test_id": "E2E-07",
|
||||
"base_url": base,
|
||||
"worker_id": worker_id,
|
||||
"steps": [],
|
||||
"verdict": "FAIL",
|
||||
}
|
||||
|
||||
# 0) Health
|
||||
t0 = time.perf_counter()
|
||||
live = requests.get(f"{base}/health/live", timeout=10)
|
||||
out["steps"].append(
|
||||
{"step": "health_live", "status": live.status_code, "ms": round((time.perf_counter() - t0) * 1000, 1)}
|
||||
)
|
||||
if live.status_code != 200:
|
||||
out["error"] = "gateway /health/live no OK"
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 1
|
||||
|
||||
# 1) Job Desktop padre
|
||||
t0 = time.perf_counter()
|
||||
parent = requests.post(
|
||||
f"{base}/jobs",
|
||||
json={"title": "E2E-07 worker offline"},
|
||||
timeout=15,
|
||||
)
|
||||
out["steps"].append(
|
||||
{
|
||||
"step": "create_parent_job",
|
||||
"status": parent.status_code,
|
||||
"ms": round((time.perf_counter() - t0) * 1000, 1),
|
||||
"body": _safe_json(parent),
|
||||
}
|
||||
)
|
||||
if parent.status_code not in (200, 201):
|
||||
out["error"] = "no se pudo crear job Desktop padre"
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 1
|
||||
parent_id = parent.json().get("id")
|
||||
if not parent_id:
|
||||
out["error"] = "respuesta padre sin id"
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 1
|
||||
out["parent_job_id"] = parent_id
|
||||
|
||||
# 2) Asegurar que el worker NO está online (no registrar / no heartbeat)
|
||||
t0 = time.perf_counter()
|
||||
workers = requests.get(f"{base}/bot-descarga/workers", headers=headers_bot, timeout=10)
|
||||
online = False
|
||||
if workers.status_code == 200:
|
||||
for w in workers.json():
|
||||
if w.get("id") == worker_id and w.get("status") in ("online", "busy"):
|
||||
online = True
|
||||
out["steps"].append(
|
||||
{
|
||||
"step": "check_worker_not_online",
|
||||
"status": workers.status_code,
|
||||
"ms": round((time.perf_counter() - t0) * 1000, 1),
|
||||
"worker_online": online,
|
||||
}
|
||||
)
|
||||
if online:
|
||||
out["error"] = (
|
||||
f"worker '{worker_id}' está online; usa otro --worker-id o apaga el agent"
|
||||
)
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 1
|
||||
|
||||
# 3) POST create download job → esperar 409
|
||||
t0 = time.perf_counter()
|
||||
files = {"file": ("e2e07.xlsx", BytesIO(_MIN_XLSX), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")}
|
||||
data = {
|
||||
"worker_id": worker_id,
|
||||
"job_id": parent_id,
|
||||
"title": "E2E-07 offline",
|
||||
}
|
||||
create = requests.post(
|
||||
f"{base}/bot-descarga/jobs",
|
||||
headers=headers_bot,
|
||||
data=data,
|
||||
files=files,
|
||||
timeout=30,
|
||||
)
|
||||
elapsed = round((time.perf_counter() - t0) * 1000, 1)
|
||||
detail = _safe_json(create)
|
||||
out["steps"].append(
|
||||
{
|
||||
"step": "create_job_offline_worker",
|
||||
"status": create.status_code,
|
||||
"ms": elapsed,
|
||||
"body": detail,
|
||||
}
|
||||
)
|
||||
out["expected"] = 409
|
||||
out["got"] = create.status_code
|
||||
|
||||
if create.status_code == 409:
|
||||
out["verdict"] = "PASS"
|
||||
out["meaning"] = (
|
||||
"El orquestador bot-descarga rechaza crear job si el agent Selenium "
|
||||
"no está online (sin heartbeat). Correcto."
|
||||
)
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 0
|
||||
|
||||
out["verdict"] = "FAIL"
|
||||
out["error"] = f"se esperaba HTTP 409, se obtuvo {create.status_code}"
|
||||
_write(out)
|
||||
print(json.dumps(out, indent=2, ensure_ascii=False))
|
||||
return 1
|
||||
|
||||
|
||||
def _safe_json(resp: requests.Response):
|
||||
try:
|
||||
return resp.json()
|
||||
except Exception:
|
||||
return (resp.text or "")[:500]
|
||||
|
||||
|
||||
def _write(out: dict) -> None:
|
||||
dest = Path(__file__).resolve().parents[1] / "informes"
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
path = dest / f"e2e_07_{stamp}.json"
|
||||
path.write_text(json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
# también latest
|
||||
(dest / "e2e_07_latest.json").write_text(
|
||||
json.dumps(out, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -14,12 +14,96 @@ No sustituye una corrida real de 100k PDFs. Usa supuestos conservadores + eviden
|
||||
| Etapa | facturas/h estimadas | vs 139/h | vs 570/h |
|
||||
|-------|----------------------|----------|----------|
|
||||
| Gateway (orquestación) | >>10 000 (health ~57 rps) | OK | OK |
|
||||
| count-api OCR | 20–80 (CPU/RAM contención) | RIESGO | NO |
|
||||
| extraction LLM | 60–200 (si cuota LLM; 30–60s/PDF, pocos workers) | ÁMBAR | NO |
|
||||
| bot Selenium (1 agent) | 30–120 | RIESGO | NO |
|
||||
| count-api OCR | **≈100–325** (ST-COUNT-01 2026-07-17: p50 35,8 s @3 conc.; 9/9 ok) | ÁMBAR/OK | NO |
|
||||
| extraction LLM | **≈506** (ST-EXT-02 2026-07-17: p50 7,114 s, gpt-5.4-mini, 23 PDFs) | OK | ÁMBAR (1w); OK≈1012/h con 2w sin cuota |
|
||||
| Agent Selenium (1 agent PC) | **≈422** (medido: 633 PDFs / 90 min) | OK vs 139/h | NO vs 570/h |
|
||||
| cleaning CSV | 500–2000 | OK | ÁMBAR |
|
||||
| Pipeline completo (este host) | <50 | NO | NO |
|
||||
| Pipeline completo (este host) | **≈100–325** (= min etapas; cuello OCR; **no** medido E2E) | JUSTO/NO vs 139 | NO vs 570 |
|
||||
|
||||
## Bench cola ARQ (ST-EXT-01) — LLM mock
|
||||
|
||||
Mide enqueue + drenado de backlog **sin coste OpenAI**. Requiere mock + `ASYNC_QUEUE=true`.
|
||||
|
||||
```bash
|
||||
# Terminal 1 — mock OpenAI-compatible
|
||||
bash docs/plan_pruebas/scripts/llm_mock_server.sh
|
||||
# Apuntar extraction/workers:
|
||||
# EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1
|
||||
# ASYNC_QUEUE=true
|
||||
# WORKER_REPLICAS=1 # shared8g; lab → 2
|
||||
|
||||
# Terminal 2 — bench
|
||||
bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh
|
||||
PROFILE=lab ARQ_BENCH_LIMIT=100 bash docs/plan_pruebas/scripts/st_ext_01_arq_bench.sh
|
||||
|
||||
# Windows
|
||||
.\docs\plan_pruebas\scripts\llm_mock_server.ps1
|
||||
.\docs\plan_pruebas\scripts\st_ext_01_arq_bench.ps1 -Profile shared8g -Limit 30
|
||||
```
|
||||
|
||||
Salida: `informes/st_ext_01_arq_YYYYMMDD.csv` + `.summary.json`. Schema: `st_ext_01_schema.json`.
|
||||
|
||||
**Resultados 2026-07-17 (shared8g, Limit=30):** mode=async; 30/30 queued; drained en **22,5 s**; total **23,9 s**; throughput cola ≈ **4 528 docs/h** (mock ≈50 ms — no es SLO LLM). Producción: `ASYNC_QUEUE=true`.
|
||||
|
||||
## Bench OCR carga (ST-COUNT-01)
|
||||
|
||||
```bash
|
||||
# Python (recomendado si no hay k6):
|
||||
python docs/plan_pruebas/scripts/st_count_01_bench.py --profile shared8g
|
||||
# Windows:
|
||||
.\docs\plan_pruebas\scripts\st_count_01_bench.ps1 -Profile shared8g
|
||||
|
||||
# k6 (campo multipart corregido a "files"):
|
||||
k6 run -e PROFILE=shared8g k6_count_batches.js
|
||||
```
|
||||
|
||||
**Resultados 2026-07-17:** 9/9 ok; p50 **35,8 s** @3 concurrentes; throughput obs. ≈ **325 fact/h**; sin 5xx/OOM.
|
||||
|
||||
## Bench LLM (ST-EXT-02) — mismo schema/campos
|
||||
|
||||
Congela `fields` en `informes/st_ext_02_schema.json` y los reutiliza en toda la corrida
|
||||
(y en re-runs) para que latencia/coste sean comparables. Modelo default: **gpt-5.4-mini**.
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh
|
||||
LLM_BENCH_LIMIT=10 bash docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh
|
||||
|
||||
# Windows
|
||||
.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1
|
||||
.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Limit 10 -Model gpt-5.4-mini
|
||||
# Forzar redescubrimiento de schema:
|
||||
.\docs\plan_pruebas\scripts\st_ext_02_llm_bench.ps1 -Rediscover
|
||||
```
|
||||
|
||||
Salida:
|
||||
- `docs/plan_pruebas/informes/llm_latency_YYYYMMDD.csv`
|
||||
- `docs/plan_pruebas/informes/llm_latency_YYYYMMDD.summary.json`
|
||||
- `docs/plan_pruebas/informes/st_ext_02_schema.json` (campos fijos)
|
||||
|
||||
Coste de referencia: USD **0,0257**/doc → **USD 2 570** por 100 000 que pasan conteo.
|
||||
|
||||
**Resultados 2026-07-17:** 23/23 ok; p50 **7,114** s; p95 **8,582** s; ≈**506** fact/h (1 worker).
|
||||
Volcados en `informe/sections/13_llm_latency.tex` y fila LLM de `08_capacidad_100k.tex`.
|
||||
|
||||
## Escenario dedicado (condicional)
|
||||
|
||||
Con host ≥16–32 GiB solo IPS, OCR pool, 2+ workers extraction, cuota LLM, N agents bot: pipeline completo puede acercarse a 139–570/h según dimensionamiento.
|
||||
Con host ≥16–32 GiB solo IPS, OCR pool, 2+ workers extraction, cuota LLM, N agents Selenium: pipeline completo puede acercarse a 139–570/h según dimensionamiento (y p50 LLM medido).
|
||||
|
||||
## Coste LLM — 100 000 facturas que pasan el filtro de conteo
|
||||
|
||||
Supuesto: el conteo filtra el lote y **100 000 PDFs** llegan a extracción.
|
||||
|
||||
| Concepto | Valor |
|
||||
|----------|-------|
|
||||
| Modelo de extracción | **gpt-5.4 mini** |
|
||||
| Coste por documento | **USD 0,0257** |
|
||||
| Volumen (pasan filtro conteo) | 100 000 |
|
||||
| **Coste estimado extracción/mes** | **USD 2 570** |
|
||||
|
||||
Fórmula: `100_000 × 0.0257 = 2_570`.
|
||||
|
||||
Notas:
|
||||
- No incluye agent Selenium, orquestación `bot-descarga`, OCR de conteo, infra ni reintentos.
|
||||
- Si el filtro de conteo deja pasar menos de 100k, el coste baja linealmente (`n_válidos × 0.0257`).
|
||||
- ST-EXT-02 2026-07-17 usó el unitario 0.0257 como estimación; contrastar con tokens reales si se instrumenta usage.
|
||||
|
||||
@@ -52,11 +52,13 @@ const tinyPdf = encoding.b64decode(
|
||||
|
||||
export default function () {
|
||||
const bin = __ENV.PDF_B64 ? encoding.b64decode(__ENV.PDF_B64) : tinyPdf;
|
||||
// count-api espera el campo multipart "files" (puede ser múltiple).
|
||||
const data = {
|
||||
file: http.file(bin, 'sample.pdf', 'application/pdf'),
|
||||
files: http.file(bin, 'sample.pdf', 'application/pdf'),
|
||||
};
|
||||
const res = http.post(`${BASE_URL}/count/v1/count`, data);
|
||||
check(res, {
|
||||
'status 2xx': (r) => r.status >= 200 && r.status < 300,
|
||||
'not 5xx': (r) => r.status < 500,
|
||||
});
|
||||
sleep(0.1);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Arranca mock OpenAI para ST-EXT-01 (Windows).
|
||||
param(
|
||||
[int]$Port = 8099,
|
||||
[double]$DelayMs = 50
|
||||
)
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..")
|
||||
$py = Join-Path $Root "docs\plan_pruebas\scripts\llm_mock_server.py"
|
||||
Write-Host "LLM mock → python $py --port $Port --delay-ms $DelayMs"
|
||||
python $py --port $Port --delay-ms $DelayMs
|
||||
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Mock OpenAI-compatible para ST-EXT-01 (cola ARQ sin coste LLM real).
|
||||
|
||||
Endpoints:
|
||||
GET /v1/models
|
||||
POST /v1/chat/completions
|
||||
|
||||
Responde JSON fijo con delay configurable (default 50 ms).
|
||||
|
||||
Ejemplo:
|
||||
python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50
|
||||
|
||||
Apuntar extraction/workers (Docker):
|
||||
EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1
|
||||
NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1
|
||||
EXTRACTOR_API_KEY=mock-key
|
||||
ASYNC_QUEUE=true
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
MOCK_SCHEMA = {
|
||||
"numero_factura": "str",
|
||||
"fecha": "str",
|
||||
"nit_prestador": "str",
|
||||
"razon_social": "str",
|
||||
"valor_total": "number",
|
||||
}
|
||||
|
||||
MOCK_EXTRACTION = {
|
||||
"numero_factura": "MOCK-0001",
|
||||
"fecha": "2026-01-15",
|
||||
"nit_prestador": "900000000",
|
||||
"razon_social": "IPS MOCK ST-EXT-01",
|
||||
"valor_total": 1000,
|
||||
}
|
||||
|
||||
|
||||
class Stats:
|
||||
def __init__(self) -> None:
|
||||
self.lock = threading.Lock()
|
||||
self.completions = 0
|
||||
self.models = 0
|
||||
|
||||
def inc(self, kind: str) -> None:
|
||||
with self.lock:
|
||||
if kind == "completions":
|
||||
self.completions += 1
|
||||
else:
|
||||
self.models += 1
|
||||
|
||||
|
||||
STATS = Stats()
|
||||
|
||||
|
||||
def make_handler(delay_ms: float):
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, fmt: str, *args) -> None: # quieter
|
||||
pass
|
||||
|
||||
def _json(self, code: int, payload: dict) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path.rstrip("/")
|
||||
if path in ("/v1/models", "/models"):
|
||||
STATS.inc("models")
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"object": "list",
|
||||
"data": [
|
||||
{"id": "mock-llm", "object": "model", "owned_by": "st-ext-01"},
|
||||
{"id": "gpt-5.4-mini", "object": "model", "owned_by": "st-ext-01"},
|
||||
],
|
||||
},
|
||||
)
|
||||
return
|
||||
if path in ("/health", "/v1/health"):
|
||||
self._json(200, {"status": "ok", "completions": STATS.completions})
|
||||
return
|
||||
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
|
||||
|
||||
def do_POST(self) -> None: # noqa: N802
|
||||
path = urlparse(self.path).path.rstrip("/")
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
try:
|
||||
req = json.loads(raw.decode("utf-8") or "{}")
|
||||
except Exception:
|
||||
req = {}
|
||||
|
||||
if path not in ("/v1/chat/completions", "/chat/completions"):
|
||||
self._json(404, {"error": {"message": f"not found: {path}", "type": "not_found"}})
|
||||
return
|
||||
|
||||
if delay_ms > 0:
|
||||
time.sleep(delay_ms / 1000.0)
|
||||
|
||||
# Heurística: schema discovery suele pedir "campos" / schema; extracción pide factura.
|
||||
messages = req.get("messages") or []
|
||||
blob = json.dumps(messages, ensure_ascii=False).lower()
|
||||
if "schema" in blob or "campos" in blob or "field" in blob or "descub" in blob:
|
||||
content = json.dumps(MOCK_SCHEMA, ensure_ascii=False)
|
||||
else:
|
||||
content = json.dumps(MOCK_EXTRACTION, ensure_ascii=False)
|
||||
|
||||
model = req.get("model") or "mock-llm"
|
||||
STATS.inc("completions")
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"id": f"chatcmpl-mock-{STATS.completions}",
|
||||
"object": "chat.completion",
|
||||
"created": int(time.time()),
|
||||
"model": model,
|
||||
"choices": [
|
||||
{
|
||||
"index": 0,
|
||||
"message": {"role": "assistant", "content": content},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"usage": {
|
||||
"prompt_tokens": 100,
|
||||
"completion_tokens": 50,
|
||||
"total_tokens": 150,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return Handler
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description="Mock OpenAI API for ST-EXT-01")
|
||||
p.add_argument("--host", default="0.0.0.0")
|
||||
p.add_argument("--port", type=int, default=int(__import__("os").environ.get("LLM_MOCK_PORT", "8099")))
|
||||
p.add_argument("--delay-ms", type=float, default=float(__import__("os").environ.get("LLM_MOCK_DELAY_MS", "50")))
|
||||
args = p.parse_args()
|
||||
|
||||
httpd = ThreadingHTTPServer((args.host, args.port), make_handler(args.delay_ms))
|
||||
print(f"[llm-mock] http://{args.host}:{args.port}/v1 delay={args.delay_ms}ms")
|
||||
print(f"[llm-mock] health: http://127.0.0.1:{args.port}/health")
|
||||
print("[llm-mock] Ctrl+C para detener")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\n[llm-mock] stopped")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Arranca mock OpenAI para ST-EXT-01.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
PORT="${LLM_MOCK_PORT:-8099}"
|
||||
DELAY="${LLM_MOCK_DELAY_MS:-50}"
|
||||
exec python3 "$ROOT/docs/plan_pruebas/scripts/llm_mock_server.py" --port "$PORT" --delay-ms "$DELAY" "$@"
|
||||
@@ -0,0 +1,11 @@
|
||||
# Alias → ST-EXT-02
|
||||
param(
|
||||
[string]$BaseUrl = "http://127.0.0.1:8189",
|
||||
[string]$DataDir = "",
|
||||
[string]$OutCsv = "",
|
||||
[int]$Limit = 0,
|
||||
[string]$Model = "gpt-5.4-mini"
|
||||
)
|
||||
& (Join-Path $PSScriptRoot "st_ext_02_llm_bench.ps1") `
|
||||
-BaseUrl $BaseUrl -DataDir $DataDir -OutCsv $OutCsv -Limit $Limit -Model $Model
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Alias de compatibilidad → ST-EXT-02 (st_ext_02_llm_bench.py)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import runpy
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.argv[0] = str(Path(__file__).with_name("st_ext_02_llm_bench.py"))
|
||||
runpy.run_path(str(Path(__file__).with_name("st_ext_02_llm_bench.py")), run_name="__main__")
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Alias → ST-EXT-02
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
exec bash "$ROOT/docs/plan_pruebas/scripts/st_ext_02_llm_bench.sh" "$@"
|
||||
@@ -0,0 +1,20 @@
|
||||
# ST-COUNT-01 — carga count-api (Windows).
|
||||
param(
|
||||
[string]$BaseUrl = "http://127.0.0.1:8189",
|
||||
[string]$Profile = "shared8g",
|
||||
[int]$Duration = 0,
|
||||
[double]$Rate = 0,
|
||||
[int]$MaxWorkers = 0
|
||||
)
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..")
|
||||
$py = Join-Path $Root "docs\plan_pruebas\scripts\st_count_01_bench.py"
|
||||
|
||||
$argsList = @("--base-url", $BaseUrl, "--profile", $Profile)
|
||||
if ($Duration -gt 0) { $argsList += @("--duration", "$Duration") }
|
||||
if ($Rate -gt 0) { $argsList += @("--rate", "$Rate") }
|
||||
if ($MaxWorkers -gt 0) { $argsList += @("--max-workers", "$MaxWorkers") }
|
||||
|
||||
Write-Host "ST-COUNT-01 → python $py $($argsList -join ' ')"
|
||||
python $py @argsList
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,279 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ST-COUNT-01 — carga concurrente hacia count-api (vía gateway).
|
||||
|
||||
Equivalente práctico al k6 k6_count_batches.js para hosts sin k6 instalado.
|
||||
Perfiles:
|
||||
shared8g → ~1 req/s durante 60s, concurrencia máxima 3 (conservador)
|
||||
lab → ~3 req/s durante 120s, concurrencia máxima 8
|
||||
|
||||
Usa PDFs reales de docs/plan_pruebas/data/ (rota entre ellos).
|
||||
|
||||
Ejemplos:
|
||||
python docs/plan_pruebas/scripts/st_count_01_bench.py
|
||||
python docs/plan_pruebas/scripts/st_count_01_bench.py --profile shared8g
|
||||
PROFILE=lab python docs/plan_pruebas/scripts/st_count_01_bench.py --duration 90
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Requiere 'requests': pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data"
|
||||
DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes"
|
||||
|
||||
PROFILES = {
|
||||
"shared8g": {"rate": 1.0, "duration_s": 60, "max_workers": 3},
|
||||
"lab": {"rate": 3.0, "duration_s": 120, "max_workers": 8},
|
||||
}
|
||||
|
||||
|
||||
def list_pdfs(data_dir: Path) -> list[Path]:
|
||||
if not data_dir.is_dir():
|
||||
raise SystemExit(f"No existe DATA_DIR={data_dir}")
|
||||
pdfs = sorted({p.resolve() for p in list(data_dir.glob("*.pdf")) + list(data_dir.glob("*.PDF"))})
|
||||
if not pdfs:
|
||||
raise SystemExit(f"No hay PDFs en {data_dir}")
|
||||
return pdfs
|
||||
|
||||
|
||||
def percentile(vals: list[float], p: float) -> float:
|
||||
if not vals:
|
||||
return float("nan")
|
||||
s = sorted(vals)
|
||||
if len(s) == 1:
|
||||
return s[0]
|
||||
k = (len(s) - 1) * (p / 100.0)
|
||||
f = int(k)
|
||||
c = min(f + 1, len(s) - 1)
|
||||
if f == c:
|
||||
return s[f]
|
||||
return s[f] + (s[c] - s[f]) * (k - f)
|
||||
|
||||
|
||||
def count_one(base_url: str, pdf: Path, timeout_s: float) -> dict:
|
||||
t0 = time.perf_counter()
|
||||
status = "error"
|
||||
http_code = 0
|
||||
service_count = ""
|
||||
err = ""
|
||||
try:
|
||||
with pdf.open("rb") as fh:
|
||||
r = requests.post(
|
||||
f"{base_url}/count/v1/count",
|
||||
files={"files": (pdf.name, fh, "application/pdf")},
|
||||
timeout=timeout_s,
|
||||
)
|
||||
http_code = r.status_code
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if r.status_code >= 500:
|
||||
status = f"http_{r.status_code}"
|
||||
err = (r.text or "")[:200].replace("\n", " ")
|
||||
elif r.status_code >= 400:
|
||||
status = f"http_{r.status_code}"
|
||||
err = str(body.get("error") or r.text)[:200]
|
||||
else:
|
||||
results = body.get("results") or []
|
||||
if results:
|
||||
service_count = results[0].get("service_count", "")
|
||||
status = str(results[0].get("status") or "ok")
|
||||
else:
|
||||
status = "ok_empty"
|
||||
except requests.Timeout:
|
||||
status = "timeout"
|
||||
except Exception as ex:
|
||||
status = "exception"
|
||||
err = str(ex)[:200]
|
||||
|
||||
return {
|
||||
"file": pdf.name,
|
||||
"wall_s": round(time.perf_counter() - t0, 3),
|
||||
"http_code": http_code,
|
||||
"status": status,
|
||||
"service_count": service_count,
|
||||
"error": err,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="ST-COUNT-01 count-api load bench")
|
||||
p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189"))
|
||||
p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("COUNT_BENCH_PDF_DIR") or DEFAULT_DATA))
|
||||
p.add_argument(
|
||||
"--profile",
|
||||
choices=sorted(PROFILES.keys()),
|
||||
default=os.environ.get("PROFILE") or "shared8g",
|
||||
)
|
||||
p.add_argument("--duration", type=float, default=float(os.environ.get("COUNT_BENCH_DURATION_S") or "0"))
|
||||
p.add_argument("--rate", type=float, default=float(os.environ.get("COUNT_BENCH_RATE") or "0"))
|
||||
p.add_argument("--max-workers", type=int, default=int(os.environ.get("COUNT_BENCH_MAX_WORKERS") or "0"))
|
||||
p.add_argument("--timeout", type=float, default=float(os.environ.get("COUNT_BENCH_TIMEOUT_S") or "120"))
|
||||
p.add_argument(
|
||||
"--out-csv",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.environ.get("COUNT_BENCH_CSV")
|
||||
or (DEFAULT_INFORMES / f"st_count_01_{date.today().strftime('%Y%m%d')}.csv")
|
||||
),
|
||||
)
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
base_url = str(args.base_url).rstrip("/")
|
||||
cfg = PROFILES[args.profile]
|
||||
rate = args.rate if args.rate > 0 else cfg["rate"]
|
||||
duration_s = args.duration if args.duration > 0 else cfg["duration_s"]
|
||||
max_workers = args.max_workers if args.max_workers > 0 else cfg["max_workers"]
|
||||
interval = 1.0 / rate if rate > 0 else 1.0
|
||||
|
||||
try:
|
||||
h = requests.get(f"{base_url}/health/live", timeout=5)
|
||||
print(f"health/live -> {h.status_code}")
|
||||
except Exception as ex:
|
||||
print(f"AVISO: health/live ({ex})", file=sys.stderr)
|
||||
|
||||
pdfs = list_pdfs(args.data_dir)
|
||||
print(f"BASE_URL={base_url}")
|
||||
print(f"PROFILE={args.profile} rate={rate}/s duration={duration_s}s max_workers={max_workers}")
|
||||
print(f"DATA_DIR={args.data_dir} ({len(pdfs)} PDFs)")
|
||||
print(f"OUT={args.out_csv}")
|
||||
print()
|
||||
|
||||
rows: list[dict] = []
|
||||
lock = threading.Lock()
|
||||
idx = {"i": 0}
|
||||
stop_at = time.perf_counter() + duration_s
|
||||
next_slot = time.perf_counter()
|
||||
|
||||
def submit_one(ex: ThreadPoolExecutor, futures: set):
|
||||
nonlocal next_slot
|
||||
now = time.perf_counter()
|
||||
if now < next_slot:
|
||||
time.sleep(next_slot - now)
|
||||
next_slot = max(next_slot + interval, time.perf_counter())
|
||||
with lock:
|
||||
pdf = pdfs[idx["i"] % len(pdfs)]
|
||||
idx["i"] += 1
|
||||
n = idx["i"]
|
||||
print(f"[launch {n}] {pdf.name} ...", flush=True)
|
||||
futures.add(ex.submit(count_one, base_url, pdf, args.timeout))
|
||||
|
||||
args.out_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
fieldnames = ["file", "wall_s", "http_code", "status", "service_count", "error"]
|
||||
t0 = time.perf_counter()
|
||||
futures: set = set()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as ex:
|
||||
while time.perf_counter() < stop_at:
|
||||
# No saturar más allá de max_workers en vuelo
|
||||
while len(futures) >= max_workers:
|
||||
done = {f for f in futures if f.done()}
|
||||
for f in done:
|
||||
row = f.result()
|
||||
rows.append(row)
|
||||
print(
|
||||
f" done wall_s={row['wall_s']} code={row['http_code']} status={row['status']}",
|
||||
flush=True,
|
||||
)
|
||||
futures.remove(f)
|
||||
if len(futures) >= max_workers:
|
||||
time.sleep(0.05)
|
||||
if time.perf_counter() >= stop_at:
|
||||
break
|
||||
submit_one(ex, futures)
|
||||
|
||||
for f in as_completed(futures):
|
||||
row = f.result()
|
||||
rows.append(row)
|
||||
print(
|
||||
f" done wall_s={row['wall_s']} code={row['http_code']} status={row['status']}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
wall_total = round(time.perf_counter() - t0, 3)
|
||||
|
||||
with args.out_csv.open("w", encoding="utf-8", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=fieldnames)
|
||||
w.writeheader()
|
||||
for row in rows:
|
||||
w.writerow(row)
|
||||
|
||||
ok = [r for r in rows if r["http_code"] and 200 <= int(r["http_code"]) < 300]
|
||||
fail = [r for r in rows if r not in ok]
|
||||
times = [float(r["wall_s"]) for r in ok]
|
||||
meta = {
|
||||
"scenario": "ST-COUNT-01",
|
||||
"profile": args.profile,
|
||||
"rate_per_s": rate,
|
||||
"duration_s_planned": duration_s,
|
||||
"max_workers": max_workers,
|
||||
"n_total": len(rows),
|
||||
"n_ok": len(ok),
|
||||
"n_fail": len(fail),
|
||||
"wall_total_s": wall_total,
|
||||
"achieved_rate_per_s": round(len(rows) / wall_total, 3) if wall_total else 0,
|
||||
"csv": str(args.out_csv),
|
||||
"finished_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
if times:
|
||||
meta["wall_s_avg"] = round(statistics.mean(times), 3)
|
||||
meta["wall_s_p50"] = round(percentile(times, 50), 3)
|
||||
meta["wall_s_p95"] = round(percentile(times, 95), 3)
|
||||
meta["wall_s_min"] = round(min(times), 3)
|
||||
meta["wall_s_max"] = round(max(times), 3)
|
||||
# Throughput efectivo si se procesara en serie con p50 (referencia conservadora)
|
||||
meta["throughput_serial_p50_per_h"] = round(3600 / meta["wall_s_p50"], 1) if meta["wall_s_p50"] else None
|
||||
meta["throughput_parallel_est_per_h"] = (
|
||||
round(meta["throughput_serial_p50_per_h"] * max_workers, 1)
|
||||
if meta.get("throughput_serial_p50_per_h")
|
||||
else None
|
||||
)
|
||||
|
||||
meta_path = args.out_csv.with_suffix(".summary.json")
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print()
|
||||
print("=== Resumen ST-COUNT-01 (count-api carga) ===")
|
||||
print(f"CSV: {args.out_csv}")
|
||||
print(f"Summary: {meta_path}")
|
||||
print(f"ok={len(ok)} fail={len(fail)} total={len(rows)} wall_total={wall_total}s")
|
||||
print(f"achieved_rate ~ {meta['achieved_rate_per_s']}/s (plan {rate}/s)")
|
||||
if times:
|
||||
print(
|
||||
f"wall_s avg={meta['wall_s_avg']:.3f} p50={meta['wall_s_p50']:.3f} "
|
||||
f"p95={meta['wall_s_p95']:.3f} min={meta['wall_s_min']:.3f} max={meta['wall_s_max']:.3f}"
|
||||
)
|
||||
print(
|
||||
f"Throughput ref. serie(p50) ~ {meta['throughput_serial_p50_per_h']} fact/h; "
|
||||
f"x{max_workers} workers ~ {meta['throughput_parallel_est_per_h']} fact/h (ideal)"
|
||||
)
|
||||
oom_hint = any("timeout" in r["status"] or str(r["http_code"]).startswith("5") for r in rows)
|
||||
if oom_hint:
|
||||
print("AVISO: timeouts/5xx — posible contencion RAM/CPU; revisa docker stats.", file=sys.stderr)
|
||||
# PASS si hay ok y tasa de fallo < 15% (umbral checks k6 rate>0.85)
|
||||
fail_rate = len(fail) / max(len(rows), 1)
|
||||
return 0 if len(ok) > 0 and fail_rate <= 0.15 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# ST-COUNT-01 — carga count-api.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}"
|
||||
export PROFILE="${PROFILE:-shared8g}"
|
||||
exec python3 "$ROOT/docs/plan_pruebas/scripts/st_count_01_bench.py" "$@"
|
||||
@@ -0,0 +1,27 @@
|
||||
# ST-EXT-01 — bench cola ARQ con LLM mock (Windows).
|
||||
param(
|
||||
[string]$BaseUrl = "http://127.0.0.1:8189",
|
||||
[string]$DataDir = "",
|
||||
[string]$OutCsv = "",
|
||||
[string]$SchemaFile = "",
|
||||
[string]$Profile = "shared8g",
|
||||
[int]$Limit = 0,
|
||||
[string]$Model = "mock-llm",
|
||||
[ValidateSet("one", "many")]
|
||||
[string]$IngestMode = "one",
|
||||
[switch]$StartMock
|
||||
)
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..")
|
||||
$py = Join-Path $Root "docs\plan_pruebas\scripts\st_ext_01_arq_bench.py"
|
||||
|
||||
$argsList = @("--base-url", $BaseUrl, "--profile", $Profile, "--model", $Model, "--ingest-mode", $IngestMode)
|
||||
if ($DataDir) { $argsList += @("--data-dir", $DataDir) }
|
||||
if ($OutCsv) { $argsList += @("--out-csv", $OutCsv) }
|
||||
if ($SchemaFile) { $argsList += @("--schema-file", $SchemaFile) }
|
||||
if ($Limit -gt 0) { $argsList += @("--limit", "$Limit") }
|
||||
if ($StartMock) { $argsList += "--start-mock" }
|
||||
|
||||
Write-Host "ST-EXT-01 → python $py $($argsList -join ' ')"
|
||||
python $py @argsList
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,561 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ST-EXT-01 — Bench de cola ARQ con LLM mock (sin coste OpenAI).
|
||||
|
||||
Mide enqueue + drenado de backlog (no latencia LLM real; eso es ST-EXT-02).
|
||||
|
||||
Prerrequisitos:
|
||||
1. Mock LLM en marcha:
|
||||
python docs/plan_pruebas/scripts/llm_mock_server.py --port 8099 --delay-ms 50
|
||||
2. Extraction/workers apuntando al mock y con cola async:
|
||||
EXTRACTOR_BASE_URL=http://host.docker.internal:8099/v1
|
||||
NORMALIZER_BASE_URL=http://host.docker.internal:8099/v1
|
||||
EXTRACTOR_API_KEY=mock-key
|
||||
ASYNC_QUEUE=true
|
||||
WORKER_REPLICAS=1 # shared8g; lab → 2
|
||||
3. Gateway en BASE_URL (default http://127.0.0.1:8189)
|
||||
|
||||
Perfiles (--profile):
|
||||
shared8g → default --limit 30 (plan: 20–50)
|
||||
lab → default --limit 200 (plan: 100–500)
|
||||
|
||||
Flujo:
|
||||
1. Carga schema congelado (informes/st_ext_01_schema.json)
|
||||
2. POST /tasks/extraction con N documentos
|
||||
3. Encola N PDFs (ingest o ingest-many) midiendo enqueue_s
|
||||
4. Sondea GET /tasks/{id} hasta processed >= N (drain_s)
|
||||
5. Escribe CSV + summary JSON
|
||||
|
||||
Ejemplos:
|
||||
python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py
|
||||
python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py --profile lab --limit 100
|
||||
python docs/plan_pruebas/scripts/st_ext_01_arq_bench.py --start-mock --limit 20
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Requiere 'requests': pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SCRIPTS = Path(__file__).resolve().parent
|
||||
DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data"
|
||||
DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes"
|
||||
DEFAULT_SCHEMA = DEFAULT_INFORMES / "st_ext_01_schema.json"
|
||||
MOCK_SCRIPT = SCRIPTS / "llm_mock_server.py"
|
||||
|
||||
PROFILE_DEFAULTS = {
|
||||
"shared8g": {"limit": 30, "workers_hint": 1},
|
||||
"lab": {"limit": 200, "workers_hint": 2},
|
||||
}
|
||||
|
||||
|
||||
def list_pdfs(data_dir: Path) -> list[Path]:
|
||||
if not data_dir.is_dir():
|
||||
raise SystemExit(f"No existe DATA_DIR={data_dir}")
|
||||
pdfs = sorted(data_dir.glob("*.pdf")) + sorted(data_dir.glob("*.PDF"))
|
||||
seen: set[str] = set()
|
||||
unique: list[Path] = []
|
||||
for p in pdfs:
|
||||
key = p.name.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(p)
|
||||
if not unique:
|
||||
raise SystemExit(f"No hay PDFs en {data_dir}")
|
||||
return unique
|
||||
|
||||
|
||||
def expand_jobs(pdfs: list[Path], n: int) -> list[tuple[Path, str]]:
|
||||
"""Reutiliza PDFs con radicado único si n > len(pdfs)."""
|
||||
jobs: list[tuple[Path, str]] = []
|
||||
for i in range(n):
|
||||
pdf = pdfs[i % len(pdfs)]
|
||||
rad = f"{pdf.stem}_{i:04d}"
|
||||
jobs.append((pdf, rad))
|
||||
return jobs
|
||||
|
||||
|
||||
def load_schema(path: Path) -> dict:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
fields = list(data.get("fields") or [])
|
||||
if not fields:
|
||||
raise SystemExit(f"Schema sin fields: {path}")
|
||||
if "alias_map" not in data:
|
||||
data["alias_map"] = {f: f.split("/")[-1] for f in fields}
|
||||
print(f"[ST-EXT-01] Schema: {path} ({len(fields)} campos)")
|
||||
return data
|
||||
|
||||
|
||||
def create_task(base_url: str, jobs: list[tuple[Path, str]], freeze: dict, model: str) -> str:
|
||||
fields = list(freeze["fields"])
|
||||
alias_map = dict(freeze.get("alias_map") or {f: f.split("/")[-1] for f in fields})
|
||||
structure_mode = str(freeze.get("structure_mode") or "general")
|
||||
documents = [
|
||||
{"radicado": rad, "filename": pdf.name, "doc_type": None} for pdf, rad in jobs
|
||||
]
|
||||
body = {
|
||||
"source": "desktop",
|
||||
"fields": fields,
|
||||
"alias_map": alias_map,
|
||||
"structure_mode": structure_mode,
|
||||
"retry_on_empty": False,
|
||||
"documents": documents,
|
||||
"document_count": len(documents),
|
||||
"model": model,
|
||||
}
|
||||
r = requests.post(f"{base_url}/tasks/extraction", json=body, timeout=60)
|
||||
if r.status_code >= 400:
|
||||
raise SystemExit(f"tasks/extraction HTTP {r.status_code}: {r.text[:500]}")
|
||||
task = r.json()
|
||||
task_id = task.get("id")
|
||||
if not task_id:
|
||||
raise SystemExit(f"Sin task id: {task}")
|
||||
print(f"[ST-EXT-01] Tarea {task_id} — {len(jobs)} docs, model={model}")
|
||||
return str(task_id)
|
||||
|
||||
|
||||
def task_snapshot(base_url: str, task_id: str) -> dict:
|
||||
r = requests.get(f"{base_url}/tasks/{urllib.parse.quote(task_id)}", timeout=30)
|
||||
r.raise_for_status()
|
||||
data = r.json()
|
||||
result = data.get("result") or {}
|
||||
processed = int(result.get("processed") or 0)
|
||||
row_count = int(result.get("row_count") or 0)
|
||||
|
||||
# Fuente de verdad más fiable ante carrera en result.processed:
|
||||
# contar filas persistidas vía /tasks/{id}/rows.
|
||||
rows_n = 0
|
||||
try:
|
||||
rr = requests.get(
|
||||
f"{base_url}/tasks/{urllib.parse.quote(task_id)}/rows",
|
||||
params={"limit": 5000, "offset": 0},
|
||||
timeout=30,
|
||||
)
|
||||
if rr.status_code < 400:
|
||||
body = rr.json()
|
||||
if isinstance(body, list):
|
||||
rows_n = len(body)
|
||||
elif isinstance(body, dict):
|
||||
rows_n = int(body.get("count") or len(body.get("rows") or []))
|
||||
except Exception:
|
||||
rows_n = 0
|
||||
|
||||
progress = max(processed, row_count, rows_n)
|
||||
return {
|
||||
"status": str(data.get("status") or "unknown"),
|
||||
"processed": progress,
|
||||
"processed_raw": processed,
|
||||
"row_count": max(row_count, rows_n),
|
||||
"total": int(result.get("total") or 0),
|
||||
"errors": list(result.get("errors") or []),
|
||||
}
|
||||
|
||||
|
||||
def enqueue_one(
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
pdf: Path,
|
||||
radicado: str,
|
||||
timeout_s: float,
|
||||
) -> dict:
|
||||
t0 = time.perf_counter()
|
||||
http_code = 0
|
||||
status = "error"
|
||||
queued = False
|
||||
err_msg = ""
|
||||
try:
|
||||
with pdf.open("rb") as fh:
|
||||
r = requests.post(
|
||||
f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest",
|
||||
files={"file": (pdf.name, fh, "application/pdf")},
|
||||
data={"radicado": radicado, "filename": pdf.name},
|
||||
timeout=timeout_s,
|
||||
)
|
||||
http_code = r.status_code
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if r.status_code >= 400:
|
||||
status = f"http_{r.status_code}"
|
||||
err_msg = (r.text or "")[:200].replace("\n", " ")
|
||||
elif body.get("queued") is True or body.get("queued") == 1:
|
||||
queued = True
|
||||
status = "queued"
|
||||
else:
|
||||
# sync: respuesta ya incluye progreso
|
||||
task = body.get("task") or body
|
||||
result = (task.get("result") if isinstance(task, dict) else None) or {}
|
||||
processed = int(result.get("processed") or 0)
|
||||
status = "sync_ok" if processed > 0 or r.status_code < 300 else "sync_no_progress"
|
||||
errors = result.get("errors") or []
|
||||
if errors:
|
||||
err_msg = str(errors[-1])[:200]
|
||||
except requests.Timeout:
|
||||
status = "timeout"
|
||||
except Exception as ex:
|
||||
status = "exception"
|
||||
err_msg = str(ex)[:200]
|
||||
|
||||
return {
|
||||
"radicado": radicado,
|
||||
"file": pdf.name,
|
||||
"enqueue_s": round(time.perf_counter() - t0, 3),
|
||||
"http_code": http_code,
|
||||
"status": status,
|
||||
"queued": "1" if queued else "0",
|
||||
"error": err_msg,
|
||||
}
|
||||
|
||||
|
||||
def enqueue_many_batch(
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
batch: list[tuple[Path, str]],
|
||||
timeout_s: float,
|
||||
) -> list[dict]:
|
||||
"""POST /ingest-many; un row por PDF del lote."""
|
||||
t0 = time.perf_counter()
|
||||
files = []
|
||||
open_handles = []
|
||||
try:
|
||||
for pdf, _rad in batch:
|
||||
fh = pdf.open("rb")
|
||||
open_handles.append(fh)
|
||||
files.append(("files", (pdf.name, fh, "application/pdf")))
|
||||
data = {
|
||||
"radicados": "|".join(rad for _, rad in batch),
|
||||
"filenames": "|".join(pdf.name for pdf, _ in batch),
|
||||
}
|
||||
r = requests.post(
|
||||
f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest-many",
|
||||
files=files,
|
||||
data=data,
|
||||
timeout=timeout_s,
|
||||
)
|
||||
finally:
|
||||
for fh in open_handles:
|
||||
fh.close()
|
||||
|
||||
wall = round(time.perf_counter() - t0, 3)
|
||||
per = round(wall / max(len(batch), 1), 3)
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
|
||||
queued_flag = False
|
||||
status = "error"
|
||||
err_msg = ""
|
||||
if r.status_code >= 400:
|
||||
status = f"http_{r.status_code}"
|
||||
err_msg = (r.text or "")[:200].replace("\n", " ")
|
||||
elif isinstance(body.get("queued"), int) or body.get("queued") is True:
|
||||
queued_flag = True
|
||||
status = "queued"
|
||||
else:
|
||||
status = "sync_batch"
|
||||
|
||||
rows = []
|
||||
for pdf, rad in batch:
|
||||
rows.append(
|
||||
{
|
||||
"radicado": rad,
|
||||
"file": pdf.name,
|
||||
"enqueue_s": per,
|
||||
"http_code": r.status_code,
|
||||
"status": status,
|
||||
"queued": "1" if queued_flag else "0",
|
||||
"error": err_msg,
|
||||
}
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def wait_drain(
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
target: int,
|
||||
poll_timeout_s: float,
|
||||
poll_interval_s: float,
|
||||
) -> dict:
|
||||
t0 = time.perf_counter()
|
||||
deadline = t0 + poll_timeout_s
|
||||
last = {"status": "unknown", "processed": 0, "total": target, "errors": []}
|
||||
samples: list[tuple[float, int]] = []
|
||||
while time.perf_counter() < deadline:
|
||||
last = task_snapshot(base_url, task_id)
|
||||
elapsed = time.perf_counter() - t0
|
||||
samples.append((elapsed, last["processed"]))
|
||||
if last["processed"] >= target:
|
||||
break
|
||||
if last["status"] in ("failed", "cancelled"):
|
||||
break
|
||||
print(
|
||||
f" … processed={last['processed']}/{target} status={last['status']} "
|
||||
f"elapsed={elapsed:.1f}s",
|
||||
flush=True,
|
||||
)
|
||||
time.sleep(poll_interval_s)
|
||||
drain_s = round(time.perf_counter() - t0, 3)
|
||||
return {
|
||||
"drain_s": drain_s,
|
||||
"processed": last["processed"],
|
||||
"task_status": last["status"],
|
||||
"errors_tail": (last.get("errors") or [])[-3:],
|
||||
"samples": samples,
|
||||
"drained": last["processed"] >= target,
|
||||
}
|
||||
|
||||
|
||||
def start_mock(port: int, delay_ms: float) -> subprocess.Popen:
|
||||
if not MOCK_SCRIPT.is_file():
|
||||
raise SystemExit(f"No existe mock: {MOCK_SCRIPT}")
|
||||
print(f"[ST-EXT-01] Arrancando mock LLM :{port} delay={delay_ms}ms")
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(MOCK_SCRIPT), "--port", str(port), "--delay-ms", str(delay_ms)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
# Esperar health
|
||||
url = f"http://127.0.0.1:{port}/health"
|
||||
for _ in range(30):
|
||||
try:
|
||||
r = requests.get(url, timeout=1)
|
||||
if r.status_code == 200:
|
||||
print(f"[ST-EXT-01] Mock listo → {url}")
|
||||
return proc
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(0.2)
|
||||
if proc.poll() is not None:
|
||||
raise SystemExit(f"Mock salió con código {proc.returncode}")
|
||||
raise SystemExit("Mock no respondió /health a tiempo")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="ST-EXT-01 ARQ bench (LLM mock)")
|
||||
p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189"))
|
||||
p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("ARQ_BENCH_PDF_DIR") or DEFAULT_DATA))
|
||||
p.add_argument(
|
||||
"--out-csv",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.environ.get("ARQ_BENCH_CSV")
|
||||
or (DEFAULT_INFORMES / f"st_ext_01_arq_{date.today().strftime('%Y%m%d')}.csv")
|
||||
),
|
||||
)
|
||||
p.add_argument(
|
||||
"--schema-file",
|
||||
type=Path,
|
||||
default=Path(os.environ.get("ARQ_BENCH_SCHEMA_FILE") or DEFAULT_SCHEMA),
|
||||
)
|
||||
p.add_argument(
|
||||
"--profile",
|
||||
choices=sorted(PROFILE_DEFAULTS.keys()),
|
||||
default=os.environ.get("PROFILE") or "shared8g",
|
||||
)
|
||||
p.add_argument("--limit", type=int, default=int(os.environ.get("ARQ_BENCH_LIMIT") or "0"))
|
||||
p.add_argument("--model", default=os.environ.get("ARQ_BENCH_MODEL") or "mock-llm")
|
||||
p.add_argument(
|
||||
"--ingest-mode",
|
||||
choices=["one", "many"],
|
||||
default=os.environ.get("ARQ_BENCH_INGEST_MODE") or "one",
|
||||
help="one=POST /ingest por PDF; many=POST /ingest-many por lote",
|
||||
)
|
||||
p.add_argument("--batch-size", type=int, default=int(os.environ.get("ARQ_BENCH_BATCH_SIZE") or "10"))
|
||||
p.add_argument("--poll-timeout", type=float, default=float(os.environ.get("ARQ_BENCH_POLL_TIMEOUT_S") or "600"))
|
||||
p.add_argument("--poll-interval", type=float, default=float(os.environ.get("ARQ_BENCH_POLL_INTERVAL_S") or "1.0"))
|
||||
p.add_argument("--enqueue-timeout", type=float, default=float(os.environ.get("ARQ_BENCH_ENQUEUE_TIMEOUT_S") or "120"))
|
||||
p.add_argument("--start-mock", action="store_true", help="Arranca llm_mock_server.py en este proceso")
|
||||
p.add_argument("--mock-port", type=int, default=int(os.environ.get("LLM_MOCK_PORT") or "8099"))
|
||||
p.add_argument("--mock-delay-ms", type=float, default=float(os.environ.get("LLM_MOCK_DELAY_MS") or "50"))
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
base_url = str(args.base_url).rstrip("/")
|
||||
profile = PROFILE_DEFAULTS[args.profile]
|
||||
limit = args.limit if args.limit > 0 else int(profile["limit"])
|
||||
|
||||
mock_proc: subprocess.Popen | None = None
|
||||
if args.start_mock:
|
||||
mock_proc = start_mock(args.mock_port, args.mock_delay_ms)
|
||||
|
||||
try:
|
||||
try:
|
||||
h = requests.get(f"{base_url}/health/live", timeout=5)
|
||||
print(f"health/live → {h.status_code}")
|
||||
except Exception as ex:
|
||||
print(f"AVISO: no responde {base_url}/health/live ({ex})", file=sys.stderr)
|
||||
|
||||
pdfs = list_pdfs(args.data_dir)
|
||||
jobs = expand_jobs(pdfs, limit)
|
||||
freeze = load_schema(args.schema_file)
|
||||
|
||||
print(f"BASE_URL={base_url}")
|
||||
print(f"PROFILE={args.profile} LIMIT={limit} (plan workers≈{profile['workers_hint']})")
|
||||
print(f"DATA_DIR={args.data_dir} ({len(pdfs)} PDFs base, {len(jobs)} jobs)")
|
||||
print(f"OUT={args.out_csv}")
|
||||
print(f"INGEST={args.ingest_mode} MODEL={args.model}")
|
||||
print()
|
||||
print(
|
||||
"AVISO: ST-EXT-01 requiere ASYNC_QUEUE=true + workers + EXTRACTOR_BASE_URL→mock. "
|
||||
"Si ves status=sync_* el stack está en modo síncrono.",
|
||||
flush=True,
|
||||
)
|
||||
print()
|
||||
|
||||
task_id = create_task(base_url, jobs, freeze, args.model)
|
||||
|
||||
args.out_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
fieldnames = ["radicado", "file", "enqueue_s", "http_code", "status", "queued", "error"]
|
||||
rows: list[dict] = []
|
||||
|
||||
t_enq0 = time.perf_counter()
|
||||
with args.out_csv.open("w", encoding="utf-8", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=fieldnames)
|
||||
w.writeheader()
|
||||
|
||||
if args.ingest_mode == "many":
|
||||
bs = max(1, args.batch_size)
|
||||
for i in range(0, len(jobs), bs):
|
||||
batch = jobs[i : i + bs]
|
||||
print(f"[enqueue {i + 1}-{i + len(batch)}/{len(jobs)}] ingest-many …", flush=True)
|
||||
batch_rows = enqueue_many_batch(
|
||||
base_url, task_id, batch, args.enqueue_timeout
|
||||
)
|
||||
for row in batch_rows:
|
||||
rows.append(row)
|
||||
w.writerow(row)
|
||||
fh.flush()
|
||||
if any(str(r["status"]).startswith("http_") for r in batch_rows):
|
||||
print(
|
||||
"AVISO: error HTTP en lote — abortando enqueue restante.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
break
|
||||
else:
|
||||
consecutive_fail = 0
|
||||
for i, (pdf, rad) in enumerate(jobs, 1):
|
||||
print(f"[enqueue {i}/{len(jobs)}] {rad} ({pdf.name}) …", flush=True)
|
||||
row = enqueue_one(base_url, task_id, pdf, rad, args.enqueue_timeout)
|
||||
rows.append(row)
|
||||
w.writerow(row)
|
||||
fh.flush()
|
||||
print(
|
||||
f" enqueue_s={row['enqueue_s']} code={row['http_code']} "
|
||||
f"status={row['status']} queued={row['queued']}",
|
||||
flush=True,
|
||||
)
|
||||
if str(row["status"]).startswith("http_") or row["status"] in (
|
||||
"error",
|
||||
"timeout",
|
||||
"exception",
|
||||
):
|
||||
consecutive_fail += 1
|
||||
if consecutive_fail >= 3:
|
||||
print(
|
||||
"AVISO: 3 fallos consecutivos de enqueue — abortando.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
break
|
||||
else:
|
||||
consecutive_fail = 0
|
||||
|
||||
enqueue_wall_s = round(time.perf_counter() - t_enq0, 3)
|
||||
n_queued = sum(1 for r in rows if r["queued"] == "1")
|
||||
n_fail = sum(1 for r in rows if str(r["status"]).startswith("http_") or r["status"] in ("error", "timeout", "exception"))
|
||||
mode = "async" if n_queued > 0 else "sync_or_unknown"
|
||||
|
||||
print()
|
||||
print(f"[ST-EXT-01] Enqueue wall={enqueue_wall_s}s queued={n_queued}/{len(rows)} mode={mode}")
|
||||
print(f"[ST-EXT-01] Drenando hasta processed>={len(jobs)} (timeout={args.poll_timeout}s)…", flush=True)
|
||||
|
||||
drain = wait_drain(base_url, task_id, len(jobs), args.poll_timeout, args.poll_interval)
|
||||
total_s = round(enqueue_wall_s + drain["drain_s"], 3)
|
||||
processed = int(drain["processed"])
|
||||
thr_h = round(processed / (total_s / 3600.0), 1) if total_s > 0 and processed else 0.0
|
||||
|
||||
enqueue_times = [float(r["enqueue_s"]) for r in rows if r.get("enqueue_s") is not None]
|
||||
meta = {
|
||||
"scenario": "ST-EXT-01",
|
||||
"profile": args.profile,
|
||||
"task_id": task_id,
|
||||
"model": args.model,
|
||||
"mode": mode,
|
||||
"ingest_mode": args.ingest_mode,
|
||||
"n_target": len(jobs),
|
||||
"n_queued": n_queued,
|
||||
"n_enqueue_fail": n_fail,
|
||||
"processed": processed,
|
||||
"drained": drain["drained"],
|
||||
"task_status": drain["task_status"],
|
||||
"enqueue_wall_s": enqueue_wall_s,
|
||||
"drain_s": drain["drain_s"],
|
||||
"total_s": total_s,
|
||||
"throughput_docs_per_h": thr_h,
|
||||
"workers_hint": profile["workers_hint"],
|
||||
"schema_file": str(args.schema_file),
|
||||
"csv": str(args.out_csv),
|
||||
"errors_tail": drain.get("errors_tail") or [],
|
||||
"finished_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
if enqueue_times:
|
||||
meta["enqueue_s_avg"] = round(statistics.mean(enqueue_times), 3)
|
||||
meta["enqueue_s_p50"] = round(sorted(enqueue_times)[len(enqueue_times) // 2], 3)
|
||||
|
||||
meta_path = args.out_csv.with_suffix(".summary.json")
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print()
|
||||
print("=== Resumen ST-EXT-01 (cola ARQ / LLM mock) ===")
|
||||
print(f"CSV: {args.out_csv}")
|
||||
print(f"Summary: {meta_path}")
|
||||
print(f"task_id: {task_id}")
|
||||
print(f"mode: {mode} (ASYNC_QUEUE esperado=true)")
|
||||
print(f"profile: {args.profile} target={len(jobs)} processed={processed} drained={drain['drained']}")
|
||||
print(f"enqueue_s: wall={enqueue_wall_s} avg={meta.get('enqueue_s_avg', 'n/a')}")
|
||||
print(f"drain_s: {drain['drain_s']} task_status={drain['task_status']}")
|
||||
print(f"total_s: {total_s}")
|
||||
print(f"throughput: ≈ {thr_h} docs/h (processed/total_s)")
|
||||
if mode != "async":
|
||||
print(
|
||||
"AVISO: no se detectó queued=true — revisa ASYNC_QUEUE=true, Redis y workers.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
if not drain["drained"]:
|
||||
print(
|
||||
"AVISO: backlog no drenado a tiempo — revisa WORKER_REPLICAS, mock LLM y logs.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print()
|
||||
print("Checklist plan: shared8g 20–50 docs / 1 worker; lab 100–500 / WORKER_REPLICAS=2")
|
||||
return 0 if drain["drained"] and n_fail == 0 else 1
|
||||
finally:
|
||||
if mock_proc is not None and mock_proc.poll() is None:
|
||||
mock_proc.terminate()
|
||||
try:
|
||||
mock_proc.wait(timeout=5)
|
||||
except Exception:
|
||||
mock_proc.kill()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# ST-EXT-01 — bench cola ARQ con LLM mock.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}"
|
||||
export PROFILE="${PROFILE:-shared8g}"
|
||||
export ARQ_BENCH_MODEL="${ARQ_BENCH_MODEL:-mock-llm}"
|
||||
exec python3 "$ROOT/docs/plan_pruebas/scripts/st_ext_01_arq_bench.py" "$@"
|
||||
@@ -0,0 +1,27 @@
|
||||
# ST-EXT-02 — bench LLM con schema/campos congelados (Windows).
|
||||
# Mismo schema para todos los PDFs; reutiliza st_ext_02_schema.json en re-runs.
|
||||
param(
|
||||
[string]$BaseUrl = "http://127.0.0.1:8189",
|
||||
[string]$DataDir = "",
|
||||
[string]$OutCsv = "",
|
||||
[string]$SchemaFile = "",
|
||||
[int]$Limit = 0,
|
||||
[string]$Model = "gpt-5.4-mini",
|
||||
[switch]$Rediscover
|
||||
)
|
||||
|
||||
$Root = Resolve-Path (Join-Path $PSScriptRoot "..\..\..")
|
||||
$py = Join-Path $Root "docs\plan_pruebas\scripts\st_ext_02_llm_bench.py"
|
||||
|
||||
$argsList = @("--base-url", $BaseUrl, "--model", $Model)
|
||||
if ($DataDir) { $argsList += @("--data-dir", $DataDir) }
|
||||
if ($OutCsv) { $argsList += @("--out-csv", $OutCsv) }
|
||||
if ($SchemaFile) { $argsList += @("--schema-file", $SchemaFile) }
|
||||
if ($Limit -gt 0) { $argsList += @("--limit", "$Limit") }
|
||||
if ($Rediscover) { $argsList += "--rediscover" }
|
||||
|
||||
$env:LLM_BENCH_COST_PER_DOC = if ($env:LLM_BENCH_COST_PER_DOC) { $env:LLM_BENCH_COST_PER_DOC } else { "0.0257" }
|
||||
|
||||
Write-Host "ST-EXT-02 → python $py $($argsList -join ' ')"
|
||||
python $py @argsList
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,418 @@
|
||||
#!/usr/bin/env python3
|
||||
"""ST-EXT-02 — Bench de latencia LLM con el mismo schema/campos para todos los PDFs.
|
||||
|
||||
Garantiza comparabilidad de latencia y coste:
|
||||
- Descubre schema UNA vez (o carga un JSON congelado).
|
||||
- Congela fields + alias_map en informes/st_ext_02_schema.json.
|
||||
- Usa exactamente esos campos en la tarea y en cada ingest.
|
||||
- Modelo por defecto: gpt-5.4-mini.
|
||||
- Estima coste @ USD 0.0257/doc (referencia 100k que pasan conteo).
|
||||
|
||||
Flujo:
|
||||
1. POST /extraction/schemas/upload (solo si no hay schema congelado)
|
||||
2. Guarda/carga schema JSON (mismos campos)
|
||||
3. POST /tasks/extraction (documents[], fields fijos)
|
||||
4. Por cada PDF: POST /tasks/{id}/ingest → wall_s
|
||||
|
||||
Ejemplos:
|
||||
python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py
|
||||
python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py --limit 10
|
||||
python docs/plan_pruebas/scripts/st_ext_02_llm_bench.py --schema-file docs/plan_pruebas/informes/st_ext_02_schema.json
|
||||
LLM_BENCH_LIMIT=5 .\\docs\\plan_pruebas\\scripts\\st_ext_02_llm_bench.ps1
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
print("Requiere 'requests': pip install requests", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
DEFAULT_DATA = ROOT / "docs" / "plan_pruebas" / "data"
|
||||
DEFAULT_INFORMES = ROOT / "docs" / "plan_pruebas" / "informes"
|
||||
DEFAULT_SCHEMA = DEFAULT_INFORMES / "st_ext_02_schema.json"
|
||||
COST_PER_DOC_USD = float(os.environ.get("LLM_BENCH_COST_PER_DOC", "0.0257"))
|
||||
DEFAULT_MODEL = os.environ.get("LLM_BENCH_MODEL") or "gpt-5.4-mini"
|
||||
|
||||
|
||||
def percentile(sorted_vals: list[float], p: float) -> float:
|
||||
if not sorted_vals:
|
||||
return float("nan")
|
||||
if len(sorted_vals) == 1:
|
||||
return sorted_vals[0]
|
||||
k = (len(sorted_vals) - 1) * (p / 100.0)
|
||||
f = int(k)
|
||||
c = min(f + 1, len(sorted_vals) - 1)
|
||||
if f == c:
|
||||
return sorted_vals[f]
|
||||
return sorted_vals[f] + (sorted_vals[c] - sorted_vals[f]) * (k - f)
|
||||
|
||||
|
||||
def list_pdfs(data_dir: Path, limit: int) -> list[Path]:
|
||||
if not data_dir.is_dir():
|
||||
raise SystemExit(f"No existe DATA_DIR={data_dir}")
|
||||
pdfs = sorted(data_dir.glob("*.pdf")) + sorted(data_dir.glob("*.PDF"))
|
||||
seen: set[str] = set()
|
||||
unique: list[Path] = []
|
||||
for p in pdfs:
|
||||
key = p.name.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique.append(p)
|
||||
if not unique:
|
||||
raise SystemExit(f"No hay PDFs en {data_dir}")
|
||||
if limit > 0:
|
||||
unique = unique[:limit]
|
||||
return unique
|
||||
|
||||
|
||||
def flatten_schema_payload(payload: dict) -> dict[str, str]:
|
||||
schema = payload.get("schema") or {}
|
||||
if schema:
|
||||
return {str(k): str(v) for k, v in schema.items()}
|
||||
flat: dict[str, str] = {}
|
||||
for type_name, fields in (payload.get("schema_by_type") or {}).items():
|
||||
for path, kind in (fields or {}).items():
|
||||
flat[f"{type_name}/{path}"] = str(kind)
|
||||
return flat
|
||||
|
||||
|
||||
def discover_schema(base_url: str, sample: Path, structure_mode: str, max_fields: int) -> dict:
|
||||
print(f"[ST-EXT-02] Descubriendo schema con muestra: {sample.name}")
|
||||
with sample.open("rb") as fh:
|
||||
r = requests.post(
|
||||
f"{base_url}/extraction/schemas/upload",
|
||||
files={"file": (sample.name, fh, "application/pdf")},
|
||||
data={"structure_mode": structure_mode},
|
||||
timeout=180,
|
||||
)
|
||||
if r.status_code >= 400:
|
||||
raise SystemExit(f"extraction/schemas/upload HTTP {r.status_code}: {r.text[:500]}")
|
||||
payload = r.json()
|
||||
schema_map = flatten_schema_payload(payload)
|
||||
fields = sorted(schema_map.keys())
|
||||
if not fields:
|
||||
raise SystemExit(f"Schema vacío: {json.dumps(payload)[:400]}")
|
||||
if max_fields > 0:
|
||||
fields = fields[:max_fields]
|
||||
schema_map = {k: schema_map[k] for k in fields}
|
||||
freeze = {
|
||||
"version": 1,
|
||||
"scenario": "ST-EXT-02",
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sample_file": sample.name,
|
||||
"structure_mode": structure_mode,
|
||||
"model_hint": DEFAULT_MODEL,
|
||||
"cost_per_doc_usd": COST_PER_DOC_USD,
|
||||
"fields": fields,
|
||||
"alias_map": {f: f.split("/")[-1] for f in fields},
|
||||
"schema": schema_map,
|
||||
}
|
||||
print(f" Campos congelados ({len(fields)}): {', '.join(fields[:8])}{'…' if len(fields) > 8 else ''}")
|
||||
return freeze
|
||||
|
||||
|
||||
def load_schema(path: Path) -> dict:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
fields = list(data.get("fields") or [])
|
||||
if not fields:
|
||||
raise SystemExit(f"Schema sin fields: {path}")
|
||||
if "alias_map" not in data:
|
||||
data["alias_map"] = {f: f.split("/")[-1] for f in fields}
|
||||
print(f"[ST-EXT-02] Schema cargado: {path} ({len(fields)} campos, mismos para todos los PDFs)")
|
||||
print(f" Campos: {', '.join(fields[:8])}{'…' if len(fields) > 8 else ''}")
|
||||
return data
|
||||
|
||||
|
||||
def save_schema(path: Path, freeze: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(freeze, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[ST-EXT-02] Schema guardado → {path}")
|
||||
|
||||
|
||||
def resolve_schema(
|
||||
*,
|
||||
base_url: str,
|
||||
pdfs: list[Path],
|
||||
schema_file: Path,
|
||||
structure_mode: str,
|
||||
max_fields: int,
|
||||
rediscover: bool,
|
||||
) -> dict:
|
||||
if schema_file.is_file() and not rediscover:
|
||||
return load_schema(schema_file)
|
||||
freeze = discover_schema(base_url, pdfs[0], structure_mode, max_fields)
|
||||
save_schema(schema_file, freeze)
|
||||
return freeze
|
||||
|
||||
|
||||
def create_task(
|
||||
base_url: str,
|
||||
pdfs: list[Path],
|
||||
freeze: dict,
|
||||
model: str | None,
|
||||
) -> str:
|
||||
fields = list(freeze["fields"])
|
||||
alias_map = dict(freeze.get("alias_map") or {f: f.split("/")[-1] for f in fields})
|
||||
structure_mode = str(freeze.get("structure_mode") or "general")
|
||||
documents = [{"radicado": p.stem, "filename": p.name, "doc_type": None} for p in pdfs]
|
||||
body = {
|
||||
"source": "desktop",
|
||||
"fields": fields,
|
||||
"alias_map": alias_map,
|
||||
"structure_mode": structure_mode,
|
||||
"retry_on_empty": False,
|
||||
"documents": documents,
|
||||
"document_count": len(documents),
|
||||
"model": model,
|
||||
}
|
||||
r = requests.post(f"{base_url}/tasks/extraction", json=body, timeout=60)
|
||||
if r.status_code >= 400:
|
||||
raise SystemExit(f"tasks/extraction HTTP {r.status_code}: {r.text[:500]}")
|
||||
task = r.json()
|
||||
task_id = task.get("id")
|
||||
if not task_id:
|
||||
raise SystemExit(f"Sin task id: {task}")
|
||||
print(f"[ST-EXT-02] Tarea {task_id} — {len(pdfs)} docs, {len(fields)} campos fijos, model={model}")
|
||||
return str(task_id)
|
||||
|
||||
|
||||
def task_processed(base_url: str, task_id: str) -> int:
|
||||
r = requests.get(f"{base_url}/tasks/{task_id}", timeout=30)
|
||||
r.raise_for_status()
|
||||
result = r.json().get("result") or {}
|
||||
return int(result.get("processed") or 0)
|
||||
|
||||
|
||||
def wait_processed(
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
before: int,
|
||||
poll_timeout_s: float,
|
||||
poll_interval_s: float,
|
||||
) -> tuple[str, int]:
|
||||
deadline = time.time() + poll_timeout_s
|
||||
processed = before
|
||||
while time.time() < deadline:
|
||||
r = requests.get(f"{base_url}/tasks/{task_id}", timeout=30)
|
||||
if r.status_code >= 400:
|
||||
return f"poll_http_{r.status_code}", processed
|
||||
data = r.json()
|
||||
status = str(data.get("status") or "unknown")
|
||||
result = data.get("result") or {}
|
||||
processed = int(result.get("processed") or 0)
|
||||
errors = result.get("errors") or []
|
||||
if processed > before:
|
||||
return "ok", processed
|
||||
if status in ("failed", "cancelled"):
|
||||
err = errors[-1] if errors else status
|
||||
return f"task_{err}", processed
|
||||
time.sleep(poll_interval_s)
|
||||
return "timeout", processed
|
||||
|
||||
|
||||
def ingest_one(
|
||||
base_url: str,
|
||||
task_id: str,
|
||||
pdf: Path,
|
||||
poll_timeout_s: float,
|
||||
poll_interval_s: float,
|
||||
) -> dict:
|
||||
before = task_processed(base_url, task_id)
|
||||
t0 = time.perf_counter()
|
||||
http_code = 0
|
||||
status = "error"
|
||||
queued = False
|
||||
err_msg = ""
|
||||
try:
|
||||
with pdf.open("rb") as fh:
|
||||
r = requests.post(
|
||||
f"{base_url}/tasks/{urllib.parse.quote(task_id)}/ingest",
|
||||
files={"file": (pdf.name, fh, "application/pdf")},
|
||||
data={"radicado": pdf.stem, "filename": pdf.name},
|
||||
timeout=poll_timeout_s + 30,
|
||||
)
|
||||
http_code = r.status_code
|
||||
try:
|
||||
body = r.json()
|
||||
except Exception:
|
||||
body = {}
|
||||
if r.status_code >= 400:
|
||||
status = f"http_{r.status_code}"
|
||||
err_msg = (r.text or "")[:200].replace("\n", " ")
|
||||
elif body.get("queued"):
|
||||
queued = True
|
||||
status, _ = wait_processed(base_url, task_id, before, poll_timeout_s, poll_interval_s)
|
||||
else:
|
||||
task = body.get("task") or body
|
||||
result = (task.get("result") if isinstance(task, dict) else None) or {}
|
||||
processed = int(result.get("processed") or 0)
|
||||
status = "ok" if (processed > before or r.status_code < 300) else "no_progress"
|
||||
errors = result.get("errors") or []
|
||||
if errors:
|
||||
err_msg = str(errors[-1])[:200]
|
||||
except requests.Timeout:
|
||||
status = "timeout"
|
||||
except Exception as ex:
|
||||
status = "exception"
|
||||
err_msg = str(ex)[:200]
|
||||
|
||||
wall_s = round(time.perf_counter() - t0, 3)
|
||||
cost_est = round(COST_PER_DOC_USD, 6) if status == "ok" else ""
|
||||
return {
|
||||
"file": pdf.name,
|
||||
"wall_s": wall_s,
|
||||
"http_code": http_code,
|
||||
"status": status,
|
||||
"queued": "1" if queued else "0",
|
||||
"cost_usd_est": cost_est,
|
||||
"error": err_msg,
|
||||
}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="ST-EXT-02 LLM bench (mismo schema/campos)")
|
||||
p.add_argument("--base-url", default=os.environ.get("BASE_URL", "http://127.0.0.1:8189"))
|
||||
p.add_argument("--data-dir", type=Path, default=Path(os.environ.get("LLM_BENCH_PDF_DIR") or DEFAULT_DATA))
|
||||
p.add_argument(
|
||||
"--out-csv",
|
||||
type=Path,
|
||||
default=Path(
|
||||
os.environ.get("LLM_LATENCY_CSV")
|
||||
or (DEFAULT_INFORMES / f"llm_latency_{date.today().strftime('%Y%m%d')}.csv")
|
||||
),
|
||||
)
|
||||
p.add_argument(
|
||||
"--schema-file",
|
||||
type=Path,
|
||||
default=Path(os.environ.get("LLM_BENCH_SCHEMA_FILE") or DEFAULT_SCHEMA),
|
||||
help="JSON congelado con fields (mismo schema para toda la corrida y re-runs)",
|
||||
)
|
||||
p.add_argument("--rediscover", action="store_true", help="Ignora schema congelado y vuelve a descubrir")
|
||||
p.add_argument("--limit", type=int, default=int(os.environ.get("LLM_BENCH_LIMIT") or "0"))
|
||||
p.add_argument("--max-fields", type=int, default=int(os.environ.get("LLM_BENCH_MAX_FIELDS") or "20"))
|
||||
p.add_argument("--model", default=DEFAULT_MODEL)
|
||||
p.add_argument("--structure-mode", default=os.environ.get("LLM_BENCH_STRUCTURE_MODE") or "general")
|
||||
p.add_argument("--poll-timeout", type=float, default=float(os.environ.get("LLM_BENCH_POLL_TIMEOUT_S") or "300"))
|
||||
p.add_argument("--poll-interval", type=float, default=float(os.environ.get("LLM_BENCH_POLL_INTERVAL_S") or "1.5"))
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
base_url = str(args.base_url).rstrip("/")
|
||||
|
||||
try:
|
||||
h = requests.get(f"{base_url}/health/live", timeout=5)
|
||||
print(f"health/live → {h.status_code}")
|
||||
except Exception as ex:
|
||||
print(f"AVISO: no responde {base_url}/health/live ({ex})", file=sys.stderr)
|
||||
|
||||
pdfs = list_pdfs(args.data_dir, args.limit)
|
||||
print(f"BASE_URL={base_url}")
|
||||
print(f"DATA_DIR={args.data_dir}")
|
||||
print(f"OUT={args.out_csv}")
|
||||
print(f"SCHEMA={args.schema_file}")
|
||||
print(f"PDFs={len(pdfs)} MODEL={args.model} COST/doc=${COST_PER_DOC_USD}")
|
||||
print()
|
||||
|
||||
freeze = resolve_schema(
|
||||
base_url=base_url,
|
||||
pdfs=pdfs,
|
||||
schema_file=args.schema_file,
|
||||
structure_mode=args.structure_mode,
|
||||
max_fields=args.max_fields,
|
||||
rediscover=args.rediscover,
|
||||
)
|
||||
# Asegurar que todos los PDFs usan exactamente freeze["fields"]
|
||||
task_id = create_task(base_url, pdfs, freeze, args.model)
|
||||
|
||||
args.out_csv.parent.mkdir(parents=True, exist_ok=True)
|
||||
fieldnames = ["file", "wall_s", "http_code", "status", "queued", "cost_usd_est", "error"]
|
||||
rows: list[dict] = []
|
||||
with args.out_csv.open("w", encoding="utf-8", newline="") as fh:
|
||||
w = csv.DictWriter(fh, fieldnames=fieldnames)
|
||||
w.writeheader()
|
||||
for i, pdf in enumerate(pdfs, 1):
|
||||
print(f"[{i}/{len(pdfs)}] {pdf.name} …", flush=True)
|
||||
row = ingest_one(base_url, task_id, pdf, args.poll_timeout, args.poll_interval)
|
||||
rows.append(row)
|
||||
w.writerow(row)
|
||||
fh.flush()
|
||||
print(
|
||||
f" wall_s={row['wall_s']} code={row['http_code']} status={row['status']} "
|
||||
f"cost_est={row['cost_usd_est']}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
ok_times = sorted(float(r["wall_s"]) for r in rows if r["status"] == "ok")
|
||||
n_ok = len(ok_times)
|
||||
n_fail = len(rows) - n_ok
|
||||
cost_run = round(n_ok * COST_PER_DOC_USD, 4)
|
||||
cost_100k = round(100_000 * COST_PER_DOC_USD, 2)
|
||||
|
||||
meta = {
|
||||
"scenario": "ST-EXT-02",
|
||||
"task_id": task_id,
|
||||
"model": args.model,
|
||||
"schema_file": str(args.schema_file),
|
||||
"fields": freeze.get("fields"),
|
||||
"n_fields": len(freeze.get("fields") or []),
|
||||
"n_ok": n_ok,
|
||||
"n_fail": n_fail,
|
||||
"cost_per_doc_usd": COST_PER_DOC_USD,
|
||||
"cost_run_usd": cost_run,
|
||||
"cost_100k_usd": cost_100k,
|
||||
"csv": str(args.out_csv),
|
||||
"finished_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
if ok_times:
|
||||
meta["wall_s_avg"] = round(statistics.mean(ok_times), 3)
|
||||
meta["wall_s_p50"] = round(percentile(ok_times, 50), 3)
|
||||
meta["wall_s_p95"] = round(percentile(ok_times, 95), 3)
|
||||
meta["throughput_1w_per_h"] = round(3600 / meta["wall_s_p50"], 1) if meta["wall_s_p50"] else None
|
||||
|
||||
meta_path = args.out_csv.with_suffix(".summary.json")
|
||||
meta_path.write_text(json.dumps(meta, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
print()
|
||||
print("=== Resumen ST-EXT-02 (mismo schema/campos) ===")
|
||||
print(f"CSV: {args.out_csv}")
|
||||
print(f"Schema: {args.schema_file} ({meta['n_fields']} campos fijos)")
|
||||
print(f"Summary: {meta_path}")
|
||||
print(f"task_id: {task_id}")
|
||||
print(f"model: {args.model}")
|
||||
print(f"ok={n_ok} fail={n_fail} total={len(rows)}")
|
||||
if ok_times:
|
||||
print(
|
||||
f"wall_s avg={meta['wall_s_avg']:.3f} p50={meta['wall_s_p50']:.3f} "
|
||||
f"p95={meta['wall_s_p95']:.3f} min={ok_times[0]:.3f} max={ok_times[-1]:.3f}"
|
||||
)
|
||||
if meta.get("throughput_1w_per_h"):
|
||||
print(f"Throughput 1 worker ≈ {meta['throughput_1w_per_h']:.0f} fact/h (p50)")
|
||||
print(f"Throughput 2 workers ≈ {2 * meta['throughput_1w_per_h']:.0f} fact/h (sin cuota)")
|
||||
else:
|
||||
print("Sin muestras ok — revisa API key, workers y logs de extraction.")
|
||||
print(f"Coste corrida (ok×{COST_PER_DOC_USD}): USD {cost_run}")
|
||||
print(f"Coste 100k que pasan conteo: USD {cost_100k}")
|
||||
print()
|
||||
print("PENDIENTE docs: volcar p50/p95 en informe/sections/13_llm_latency.tex")
|
||||
print(" y fila LLM en sections/08_capacidad_100k.tex")
|
||||
return 0 if n_ok > 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
# ST-EXT-02 — bench LLM con schema/campos congelados (gpt-5.4-mini por defecto).
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
export BASE_URL="${BASE_URL:-http://127.0.0.1:8189}"
|
||||
export LLM_BENCH_MODEL="${LLM_BENCH_MODEL:-gpt-5.4-mini}"
|
||||
export LLM_BENCH_COST_PER_DOC="${LLM_BENCH_COST_PER_DOC:-0.0257}"
|
||||
exec python3 "$ROOT/docs/plan_pruebas/scripts/st_ext_02_llm_bench.py" "$@"
|
||||
@@ -1,11 +1,48 @@
|
||||
\chapter{Arquitectura bajo prueba}
|
||||
\label{ch:arquitectura}
|
||||
|
||||
\section{Vista de componentes}
|
||||
El host solo expone el Gateway en el puerto \textbf{8189}. El resto de APIs viven en la red Docker. El agent del bot corre en PCs externas y se comunica por HTTP + PutObject a MinIO.
|
||||
\section{C\'omo est\'a dise\~nado el sistema}
|
||||
IPSExtractor sigue una arquitectura de \textbf{microservicios} detrás de un
|
||||
\textbf{API Gateway}. El host solo expone el puerto \textbf{8189}; el resto vive
|
||||
en la red Docker. Los agents Selenium corren en PCs externas (HTTP + PutObject a MinIO).
|
||||
|
||||
\noindent\textbf{Nombres:} \texttt{bot-descarga} = orquestador (API); \textbf{agent Selenium} = PC que descarga.
|
||||
ST-BOT-* mide el orquestador; $\sim$422 fact/h es del agent.
|
||||
|
||||
\input{diagrams/arquitectura}
|
||||
|
||||
\section{Patrones de dise\~no}
|
||||
\begin{description}[style=nextline,leftmargin=0.8cm,labelwidth=3.2cm]
|
||||
\item[API Gateway]
|
||||
\'Unica puerta p\'ublica. Proxy a extraction, cleaning, count-api y bot-descarga.
|
||||
Centraliza CORS, health y timeouts (JSON 30\,s; uploads del orquestador 600\,s).
|
||||
\emph{Implicaci\'on de prueba:} ST-GW-* miden el gateway, no el OCR ni el LLM.
|
||||
\item[Bounded contexts]
|
||||
Cada servicio tiene un dominio claro: OCR (count-api), LLM (extraction+workers),
|
||||
orquestaci\'on de descarga (\texttt{bot-descarga}), limpieza CSV (cleaning).
|
||||
\emph{Implicaci\'on:} fallos y capacidad se prueban por servicio, no solo E2E.
|
||||
\item[Cola as\'incrona (ARQ)]
|
||||
Con \texttt{ASYNC\_QUEUE=true}, el ingest encola en Redis y los workers drenan.
|
||||
Con \texttt{false} (default), el API procesa en el mismo request (sync).
|
||||
\emph{Implicaci\'on:} ST-EXT-01 exige async + workers; ST-EXT-02 puede correr sync.
|
||||
\item[Claim / lease]
|
||||
Un job \texttt{pending} solo lo toma un agent Selenium (claim at\'omico v\'ia \texttt{bot-descarga}). Heartbeat
|
||||
mantiene al worker online; sin heartbeat, no se asignan jobs.
|
||||
\emph{Implicaci\'on:} ST-BOT-01/02 miden el orquestador (heartbeats y contenci\'on de claim).
|
||||
\end{description}
|
||||
|
||||
\input{diagrams/patrones}
|
||||
|
||||
\section{Pipeline de negocio}
|
||||
\input{diagrams/pipeline_negocio}
|
||||
|
||||
\begin{enumerate}
|
||||
\item \textbf{Descarga:} agent Selenium obtiene PDFs $\rightarrow$ MinIO.
|
||||
\item \textbf{Conteo:} OCR/ONNX valida el documento (filtro de calidad).
|
||||
\item \textbf{Extracci\'on:} LLM rellena campos $\rightarrow$ PostgreSQL.
|
||||
\item \textbf{Limpieza:} reglas/CSV sobre el lote.
|
||||
\end{enumerate}
|
||||
|
||||
\section{Inventario de componentes}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
@@ -14,35 +51,35 @@ El host solo expone el Gateway en el puerto \textbf{8189}. El resto de APIs vive
|
||||
\textbf{Componente} & \textbf{Puerto} & \textbf{Responsabilidad} \\
|
||||
\midrule
|
||||
\texttt{api\_gateway} & Host 8189 & Proxy HTTP, health agregado, CORS \\
|
||||
\texttt{extraction} & 8001 & Jobs/tasks, LLM, ingest PDFs \\
|
||||
\texttt{extraction} & 8001 & Tasks, LLM, ingest PDFs \\
|
||||
\texttt{worker} (ARQ) & --- & Procesamiento async v\'ia Redis \\
|
||||
\texttt{cleaning} & 8002 & Limpieza CSV / reglas \\
|
||||
\texttt{count-api} & 8080 & OCR + ONNX, stateless \\
|
||||
\texttt{bot-descarga} & 8200 & Control remoto de bots \\
|
||||
\texttt{bot-descarga} & 8200 & Orquestador: jobs, claim, heartbeats, events \\
|
||||
PostgreSQL 16 & 5432 & Persistencia compartida \\
|
||||
Redis 7 & 6379 & Cola ARQ + rate limiter \\
|
||||
Desktop & local & Cliente Electron/React \\
|
||||
Agent bot & local PC & Selenium + MinIO upload \\
|
||||
MinIO/S3 & externo & Excel y PDFs del bot \\
|
||||
Agent Selenium & local PC & Descarga PDFs (Selenium/Imaging) + MinIO upload \\
|
||||
MinIO/S3 & externo & Excel y PDFs del pipeline de descarga \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Componentes del sistema bajo prueba.}
|
||||
\end{table}
|
||||
|
||||
\section{Flujo cr\'itico: bot de descarga}
|
||||
\section{Flujo cr\'itico: descarga (orquestador + agent Selenium)}
|
||||
\input{diagrams/flujo_bot}
|
||||
|
||||
Secuencia resumida:
|
||||
\begin{enumerate}
|
||||
\item Agent: \texttt{register} + loop \texttt{heartbeat} / \texttt{claim\_pending} ($\sim$8\,s).
|
||||
\item Agent Selenium: \texttt{register} + loop \texttt{heartbeat} / \texttt{claim\_pending} ($\sim$8\,s).
|
||||
\item Desktop: \texttt{POST /bot-descarga/jobs} (Excel, \texttt{worker\_id}, \texttt{job\_id}).
|
||||
\item API: valida job padre y worker online; sube Excel a MinIO; estado \texttt{pending}.
|
||||
\item Agent: claim $\rightarrow$ \texttt{RUNNING}; descarga Excel; Selenium/Imaging; PutObject PDFs.
|
||||
\item Agent Selenium: claim $\rightarrow$ \texttt{RUNNING}; descarga Excel; Selenium/Imaging; PutObject PDFs.
|
||||
\item Eventos \texttt{POST /events}; cierre \texttt{PATCH /jobs/\{id\}} (\texttt{completed}/\texttt{failed}).
|
||||
\end{enumerate}
|
||||
|
||||
Layout MinIO:
|
||||
\begin{lstlisting}[language={},caption={Prefijos MinIO del bot}]
|
||||
\begin{lstlisting}[language={},caption={Prefijos MinIO del pipeline de descarga}]
|
||||
uploads/{download_job_id}/input.xlsx
|
||||
downloads/{download_job_id}/{nit}/{radicado}.pdf
|
||||
downloads/{download_job_id}/_manifest.json
|
||||
@@ -51,6 +88,6 @@ downloads/{download_job_id}/_manifest.json
|
||||
\section{Otros flujos de negocio}
|
||||
\begin{description}
|
||||
\item[Conteo] Desktop $\rightarrow$ \texttt{POST /count/v1/count} $\rightarrow$ \texttt{POST /count-jobs} $\rightarrow$ validaci\'on humana.
|
||||
\item[Extracci\'on] \texttt{from-count} o ingest de PDFs $\rightarrow$ workers ARQ $\rightarrow$ filas en BD.
|
||||
\item[Extracci\'on] \texttt{from-count} o ingest de PDFs $\rightarrow$ (ARQ) workers $\rightarrow$ filas en BD.
|
||||
\item[Limpieza] Job cleaning desde \texttt{extraction\_batch\_id} o CSV upload.
|
||||
\end{description}
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
\label{ch:estrategia}
|
||||
|
||||
\section{Pir\'amide}
|
||||
Cada capa responde a una pregunta distinta: correctitud de dominio,
|
||||
integraci\'on entre servicios, cierre del flujo de negocio, resistencia a carga
|
||||
y degradaci\'on ante fallos.
|
||||
|
||||
\begin{table}[h]
|
||||
\centering
|
||||
\begin{tabular}{@{}llp{4.5cm}l@{}}
|
||||
\begin{tabular}{@{}llp{4.2cm}l@{}}
|
||||
\toprule
|
||||
\textbf{Capa} & \textbf{Qu\'e valida} & \textbf{D\'onde} & \textbf{Frecuencia} \\
|
||||
\midrule
|
||||
@@ -21,13 +25,18 @@ Chaos & Ca\'idas de dependencias & Staging & Mensual \\
|
||||
|
||||
\section{Convenci\'on de identificadores}
|
||||
\begin{itemize}
|
||||
\item \texttt{UT-<SERVICIO>-<NN>} --- unitarias
|
||||
\item \texttt{IT-<SERVICIO>-<NN>} --- integraci\'on
|
||||
\item \texttt{UT-<SERVICIO>-<NN>} --- unitarias (riesgo de regresi\'on de dominio)
|
||||
\item \texttt{IT-<SERVICIO>-<NN>} --- integraci\'on (contratos HTTP + infra)
|
||||
\item \texttt{E2E-<NN>} --- end-to-end de negocio
|
||||
\item \texttt{ST-<AREA>-<NN>} --- estr\'es / carga
|
||||
\item \texttt{ST-<AREA>-<NN>} --- estr\'es / carga (capacidad y SLO)
|
||||
\item \texttt{CH-<NN>} --- chaos / resiliencia
|
||||
\end{itemize}
|
||||
|
||||
\section{C\'omo leer un caso del cat\'alogo}
|
||||
Para cada ID: \textbf{caso} (qu\'e se hace) + \textbf{criterio} (c\'omo pasa) +
|
||||
\textbf{riesgo cubierto} (por qu\'e importa). Los resultados medidos viven en el
|
||||
\emph{Informe de Pruebas}; el modelo de volumen en \emph{Capacidad 100k}.
|
||||
|
||||
\section{Entornos}
|
||||
\begin{description}
|
||||
\item[local] Mocks de LLM, Imaging y MinIO (moto/LocalStack).
|
||||
@@ -58,6 +67,6 @@ Chrome & Mock & Mock & N/A & Real \\
|
||||
\begin{enumerate}
|
||||
\item Un caso = un objetivo verificable (assert expl\'icito).
|
||||
\item Datos de prueba versionados (fixtures), no datos de producci\'on.
|
||||
\item Estr\'es solo en staging aislado; umbrales escritos antes de ejecutar.
|
||||
\item Fallos externos (LLM/Imaging) se aislan con mocks en CI.
|
||||
\item Interpretar p95 altos junto con \texttt{free -h} / \texttt{docker stats} en shared8g.
|
||||
\item Documentar entorno en cada informe de ejecuci\'on.
|
||||
\end{enumerate}
|
||||
|
||||
@@ -1,33 +1,36 @@
|
||||
\chapter{Pruebas unitarias}
|
||||
\label{ch:unitarias}
|
||||
|
||||
Las unitarias cubren \textbf{regresiones de dominio} sin levantar el stack completo.
|
||||
Un fallo aqu\'i suele ser m\'as barato de corregir que un fallo E2E.
|
||||
|
||||
\section{Gateway}
|
||||
\begin{longtable}{@{}p{2.4cm}p{4.2cm}p{5.5cm}@{}}
|
||||
\begin{longtable}{@{}p{2.2cm}p{3.5cm}p{4.0cm}p{3.0cm}@{}}
|
||||
\caption{Unitarias gateway.}\\
|
||||
\toprule
|
||||
\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} \\
|
||||
\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} & \textbf{Riesgo} \\
|
||||
\midrule
|
||||
\endhead
|
||||
UT-GW-01 & Clasificaci\'on path upload vs JSON & Timeouts 600\,s vs 30\,s correctos \\
|
||||
UT-GW-02 & Health live sin upstream & Respuesta inmediata \texttt{ok} \\
|
||||
UT-GW-03 & Health agregado degradado & Si extraction/cleaning fallan $\rightarrow$ \texttt{degraded} \\
|
||||
UT-GW-01 & Path upload vs JSON & Timeouts 600\,s vs 30\,s & Uploads cortados \\
|
||||
UT-GW-02 & Health live sin upstream & \texttt{ok} inmediato & Orquestaci\'on ciega \\
|
||||
UT-GW-03 & Health agregado degradado & Upstream down $\rightarrow$ \texttt{degraded} & Falsos ``todo ok'' \\
|
||||
\bottomrule
|
||||
\end{longtable}
|
||||
|
||||
\section{Bot-descarga API (\texttt{JobStore})}
|
||||
Cobertura existente en \texttt{test\_store.py}; ampliar con:
|
||||
\begin{longtable}{@{}p{2.6cm}p{4.0cm}p{5.5cm}@{}}
|
||||
Cobertura en \texttt{test\_store.py}; ampliar con:
|
||||
\begin{longtable}{@{}p{2.4cm}p{3.4cm}p{3.8cm}p{3.0cm}@{}}
|
||||
\caption{Unitarias bot-descarga API.}\\
|
||||
\toprule
|
||||
\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} \\
|
||||
\textbf{ID} & \textbf{Caso} & \textbf{Criterio de \'exito} & \textbf{Riesgo} \\
|
||||
\midrule
|
||||
\endhead
|
||||
UT-BOT-01 & Register + heartbeat & Worker online; offline tras $>$45\,s \\
|
||||
UT-BOT-02 & Claim at\'omico & Un solo worker obtiene el job \\
|
||||
UT-BOT-03 & Cancel pending & Estado \texttt{cancelled} inmediato \\
|
||||
UT-BOT-04 & Cancel running & \texttt{cancel\_requested=true} \\
|
||||
UT-BOT-05 & Auth Bearer & Token inv\'alido $\rightarrow$ 401 \\
|
||||
UT-BOT-06 & Keys MinIO & Prefijos uploads/downloads correctos \\
|
||||
UT-BOT-01 & Register + heartbeat & Online; offline $>$45\,s & Jobs a PCs muertos \\
|
||||
UT-BOT-02 & Claim at\'omico & Un solo worker & Doble descarga \\
|
||||
UT-BOT-03 & Cancel pending & \texttt{cancelled} & Jobs fantasma \\
|
||||
UT-BOT-04 & Cancel running & \texttt{cancel\_requested} & Selenium sin freno \\
|
||||
UT-BOT-05 & Auth Bearer & 401 si inv\'alido & Acceso indebido \\
|
||||
UT-BOT-06 & Keys MinIO & Prefijos correctos & PDFs perdidos \\
|
||||
\bottomrule
|
||||
\end{longtable}
|
||||
|
||||
|
||||
@@ -27,25 +27,28 @@ ST-EXT-01 & 20--50 tasks; 1 r\'eplica worker & 100--500 tasks; 2 r\'eplicas \\
|
||||
Con el zoo Docker actual (MLflow $\sim$1{,}86\,GiB + Airflow + n8n, etc.) \textbf{no} ejecutar ST-COUNT-01 ni ST-EXT agresivo sin liberar memoria primero.
|
||||
|
||||
\section{Escenarios (cat\'alogo completo)}
|
||||
\begin{longtable}{@{}p{2.4cm}p{3.2cm}p{3.5cm}p{3.2cm}@{}}
|
||||
\caption{Escenarios de estr\'es.}\\
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\small
|
||||
\begin{tabular}{@{}llll@{}}
|
||||
\toprule
|
||||
\textbf{ID} & \textbf{Objetivo} & \textbf{Carga (laboratorio)} & \textbf{Observar} \\
|
||||
\textbf{ID} & \textbf{Objetivo} & \textbf{Carga (lab)} & \textbf{Observar / riesgo} \\
|
||||
\midrule
|
||||
\endhead
|
||||
ST-GW-01 & Gateway concurrente & 50--100 VUs \texttt{/health/live} & p95, 5xx \\
|
||||
ST-BOT-01 & Heartbeats masivos & 20--50 workers / 8\,s & PG, offline falsos \\
|
||||
ST-BOT-02 & Claim contention & 10 workers, cola jobs & Un claim/job \\
|
||||
ST-BOT-03 & Event storm & Hasta 5k events/job & Escritura PG \\
|
||||
ST-BOT-04 & Excel grande & 10--50\,MB upload & Timeout 600\,s \\
|
||||
ST-MINIO-01 & PutObject paralelo & N PCs & Errores S3 \\
|
||||
ST-COUNT-01 & OCR paralelo & 5--20 lotes $\times$10 & RAM, OOM \\
|
||||
ST-EXT-01 & Cola ARQ & 100--500 tasks (LLM mock) & Backlog \\
|
||||
ST-EXT-02 & LLM real & 20--50 PDFs & 429, costos \\
|
||||
ST-DES-01 & Polling desktop & 10 clientes poll & CPU gateway \\
|
||||
ST-DB-01 & PG saturaci\'on & Mezcla ST-BOT+EXT & Locks \\
|
||||
ST-GW-01 & Gateway concurrente & 50--100 VUs live & p95, 5xx --- orquestaci\'on lenta \\
|
||||
ST-BOT-01 & Heartbeats masivos & 20--50 workers & Offline falsos --- PCs muertos \\
|
||||
ST-BOT-02 & Claim contention & 10 workers & Un claim/job --- doble descarga \\
|
||||
ST-BOT-03 & Event storm & Hasta 5k events & Escritura PG --- perdida eventos \\
|
||||
ST-BOT-04 & Excel grande & 10--50\,MB & Timeout 600\,s --- upload cortado \\
|
||||
ST-MINIO-01 & PutObject paralelo & N PCs & Errores S3 --- PDFs perdidos \\
|
||||
ST-COUNT-01 & OCR paralelo & 5--20 lotes x10 & RAM/OOM --- cuello 100k \\
|
||||
ST-EXT-01 & Cola ARQ & 100--500 (mock) & Backlog --- workers insuficientes \\
|
||||
ST-EXT-02 & LLM real & 20--50 PDFs & 429, coste --- cuota LLM \\
|
||||
ST-DES-01 & Polling desktop & 10 clientes & CPU GW --- UI lenta \\
|
||||
ST-DB-01 & PG saturaci\'on & Mezcla BOT+EXT & Locks --- deadlocks \\
|
||||
\bottomrule
|
||||
\end{longtable}
|
||||
\end{tabular}
|
||||
\caption{Escenarios de estr\'es (objetivo y riesgo).}
|
||||
\end{table}
|
||||
|
||||
\section{Scripts k6 incluidos}
|
||||
\begin{table}[h]
|
||||
|
||||
@@ -1,94 +1,42 @@
|
||||
\chapter{Informe de ejecuci\'on --- campa\~na 2026-07-13}
|
||||
\chapter{Estado de campa\~na --- 2026-07-13 (+ LLM 17-jul)}
|
||||
\label{ch:informe-2026-07-13}
|
||||
|
||||
Este cap\'itulo es un \textbf{resumen de estado} del plan. Las m\'etricas completas,
|
||||
gr\'aficas e interpretaci\'on est\'an en el \emph{Informe de Pruebas}
|
||||
(\texttt{InformePruebas\_IPSExtractor.pdf}). El modelo de volumen est\'a en
|
||||
\emph{Capacidad 100k}.
|
||||
|
||||
\section{Metadatos}
|
||||
Host \texttt{ubuntu} (ASUS X441UAK), Ubuntu 24.04, 8\,GiB, perfil \texttt{shared8g},
|
||||
gateway \texttt{http://127.0.0.1:8189}.
|
||||
|
||||
\section{Resultados resumidos}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}lp{9cm}@{}}
|
||||
\toprule
|
||||
\textbf{Campo} & \textbf{Valor} \\
|
||||
\midrule
|
||||
Fecha & 13 de julio de 2026 \\
|
||||
Host & \texttt{ubuntu} (ASUS X441UAK), Ubuntu 24.04.4 LTS \\
|
||||
CPU / RAM & Intel i5-7200U; 7{,}6\,GiB (Docker limit $\approx$ 7{,}64\,GiB) \\
|
||||
Perfil k6 & \texttt{shared8g} \\
|
||||
Gateway & \texttt{http://127.0.0.1:8189} \\
|
||||
Herramienta & k6 (Grafana), Docker 29.1.3 \\
|
||||
Rama / repo & \texttt{main} --- IPSExtractor \\
|
||||
Ejecutor & erik (host staging compartido) \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Metadatos de la campa\~na.}
|
||||
\end{table}
|
||||
|
||||
\section{Baseline del entorno}
|
||||
Medici\'on previa a las cargas:
|
||||
|
||||
\begin{itemize}
|
||||
\item Memoria: total 7{,}6\,GiB; usados $\approx$4{,}4\,GiB; disponibles $\approx$3{,}3\,GiB; \textbf{swap 2{,}1\,GiB en uso}.
|
||||
\item Contenedores IPS: todos \texttt{Up} / healthy (api, bot-descarga, extraction, workers, count-api, cleaning, db, redis) --- $\sim$17\,h uptime.
|
||||
\item Vecino dominante: \texttt{mlflow-server} $\approx$1{,}84\,GiB; tambi\'en n8n, MinIO, postgres compartido, etc.
|
||||
\item \texttt{GET /health/live}: \texttt{ok}, $\approx$11\,ms.
|
||||
\item \texttt{GET /health}: \texttt{ok} (extraction, cleaning, counting, bot\_descarga); $\approx$194\,ms.
|
||||
\end{itemize}
|
||||
|
||||
\section{Resultados por caso}
|
||||
\begin{table}[ht]
|
||||
\centering
|
||||
\begin{tabular}{@{}llp{6.2cm}@{}}
|
||||
\begin{tabular}{@{}llp{6.5cm}@{}}
|
||||
\toprule
|
||||
\textbf{ID} & \textbf{Resultado} & \textbf{Detalle} \\
|
||||
\midrule
|
||||
Smoke health & \textbf{PASS} & Live + agregado OK; servicios healthy \\
|
||||
ST-GW-01 & \textbf{PASS} & 6884 req; fail 0\,\%; p95 $=$ 40{,}85\,ms (SLO $<$500\,ms) \\
|
||||
ST-BOT-01 (1) & \textbf{PARCIAL} & 100\,\% HTTP 200; p95 $=$ 2{,}28\,s (SLO $<$800\,ms) \textbf{FAIL} latencia; max 23\,s \\
|
||||
ST-BOT-01 (2) & \textbf{PASS} & 100\,\% HTTP 200; p95 $=$ 85{,}55\,ms; max 98\,ms \\
|
||||
Smoke health & PASS & Live + agregado OK \\
|
||||
ST-GW-01 & PASS & p95 40{,}85\,ms; fail 0\,\% \\
|
||||
ST-BOT-01 \#1 & PARCIAL & Funcional OK; p95 latencia FAIL (contenci\'on) \\
|
||||
ST-BOT-01 \#2 & PASS & p95 85{,}55\,ms tras aliviar host \\
|
||||
ST-EXT-02 & PASS & 23/23; p50 7{,}1\,s $\approx$506 fact/h (17-jul) \\
|
||||
ST-EXT-01 & PASS & 30/30 async; drain 22{,}5\,s; cola $\approx$4\,5k docs/h mock \\
|
||||
ST-COUNT-01 & PASS & 9/9; p50 35{,}8\,s @3 conc.; $\approx$325 fact/h obs. \\
|
||||
\bottomrule
|
||||
\end{tabular}
|
||||
\caption{Resumen de la campa\~na 2026-07-13.}
|
||||
\caption{Resumen; detalle en el Informe de Pruebas.}
|
||||
\label{tab:campana-2026-07-13}
|
||||
\end{table}
|
||||
|
||||
\subsection{ST-GW-01 --- gateway health}
|
||||
\begin{itemize}
|
||||
\item Script: \texttt{k6\_gateway\_health.js}, \texttt{PROFILE=shared8g}.
|
||||
\item Duraci\'on: $\approx$2\,min; hasta 25 VUs.
|
||||
\item Checks: 13768/13768 (100\,\%).
|
||||
\item \texttt{http\_req\_failed}: 0\,\%; p95 $=$ 40{,}85\,ms; avg $=$ 12{,}55\,ms.
|
||||
\item Thresholds: todos en verde.
|
||||
\end{itemize}
|
||||
\section{Lecci\'on operativa}
|
||||
Un p95 alto en heartbeats con 100\,\% HTTP 200 suele ser \textbf{contenci\'on del host}
|
||||
(swap + MLflow), no un bug del claim. Siempre adjuntar \texttt{free -h} y
|
||||
\texttt{docker stats} al interpretar ST-BOT-*.
|
||||
ST-EXT-01 confirma que la cola ARQ drena bien con mock; el throughput de cola
|
||||
no sustituye ST-EXT-02 (LLM real).
|
||||
ST-COUNT-01 muestra degradaci\'on fuerte del OCR bajo paralelismo (2\,s $\rightarrow$ 36\,s).
|
||||
|
||||
\subsection{ST-BOT-01 --- heartbeats (corrida 1)}
|
||||
\begin{itemize}
|
||||
\item Script: \texttt{k6\_bot\_heartbeats.js}, 5 VUs, 2\,min, token \texttt{dev-token-change-me}.
|
||||
\item Checks: 75/75 (100\,\%) --- todos los heartbeats HTTP 200.
|
||||
\item Latencia: avg 511\,ms; med 57\,ms; p95 2{,}28\,s; max 23{,}12\,s.
|
||||
\item Threshold \texttt{p(95)<800}: \textbf{cruzado} (k6 exit con error de thresholds).
|
||||
\item Interpretaci\'on: API correcta; picos atribuibles a contenci\'on del host (swap + MLflow/HDD), no a error de negocio.
|
||||
\end{itemize}
|
||||
|
||||
\subsection{ST-BOT-01 --- heartbeats (corrida 2, reintento)}
|
||||
\begin{itemize}
|
||||
\item Mismo script y perfil, tras aliviar presi\'on del host.
|
||||
\item Checks: 75/75 (100\,\%).
|
||||
\item Latencia: avg 57\,ms; med 56\,ms; p95 85{,}55\,ms; max 98\,ms.
|
||||
\item Thresholds: todos en verde.
|
||||
\item Conclusi\'on: el FAIL de latencia de la corrida 1 \textbf{no se reproduce} con menos contenci\'on; confirma causa ambiental.
|
||||
\end{itemize}
|
||||
|
||||
\section{Hallazgos}
|
||||
\begin{enumerate}
|
||||
\item IPSExtractor (gateway + bot-descarga heartbeat) cumple SLO funcional y de latencia del perfil \texttt{shared8g} cuando el host no est\'a saturado.
|
||||
\item Con swap activo ($\sim$2\,GiB) y MLflow $\sim$1{,}8\,GiB, pueden aparecer outliers de varios segundos en endpoints ligeros; deben reportarse como riesgo de \emph{plataforma compartida}, no como regresi\'on de c\'odigo sin evidencia adicional.
|
||||
\item El health agregado es usable ($\sim$200\,ms en esta sesi\'on); no usarlo como SLO de liveness (usar \texttt{/health/live}).
|
||||
\end{enumerate}
|
||||
|
||||
\section{No ejecutado en esta campa\~na}
|
||||
ST-BOT-03 (events), ST-COUNT-01, ST-EXT-*, E2E Imaging, chaos. Motivo: swap/contenci\'on residual; se aplazan a una ventana con $\geq$3\,GiB libres o MLflow/Airflow detenidos.
|
||||
|
||||
\section{Follow-ups}
|
||||
\begin{itemize}
|
||||
\item Repetir ST-BOT-01 peri\'odicamente; si p95 $>$800\,ms, adjuntar \texttt{free -h} y \texttt{docker stats}.
|
||||
\item Ventana dedicada (parar MLflow) antes de ST-COUNT-01.
|
||||
\item Opcional: excluir requests de \texttt{setup()} del threshold de latencia en k6 (registros de workers) para no mezclar cold-start con steady-state.
|
||||
\end{itemize}
|
||||
\section{Pendiente respecto a este plan}
|
||||
ST-BOT-03, ST-COUNT-01 perfil lab, ST-EXT-01 perfil lab (100--500), E2E Imaging/VPN, chaos.
|
||||
|
||||
Reference in New Issue
Block a user