semillero-INVIMA/benchmarks/scripts/generate-report.js

239 lines
11 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const {
benchmarkDir,
config,
readCsv,
resultsDir,
writeJson
} = require('./benchmark-lib');
const exists = (name) => fs.existsSync(path.join(resultsDir, name));
const readSummary = (name) => (exists(name) ? readCsv(path.join(resultsDir, name)) : []);
const number = (value) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : null;
};
const fmt = (value, suffix = '') => {
const parsed = number(value);
return parsed === null ? 'No disponible' : `${parsed.toLocaleString('es-CO', { maximumFractionDigits: 4 })}${suffix}`;
};
const markdownTable = (rows, columns) => {
if (!rows.length) return 'No hay datos disponibles.\n';
const header = `| ${columns.map((col) => col.title).join(' |')} |`;
const sep = `| ${columns.map(() => '---').join(' |')} |`;
const body = rows.map((row) => `| ${columns.map((col) => row[col.key] ?? '').join(' |')} |`);
return [header, sep, ...body].join('\n');
};
const criteria = (row) => {
const group = row.query_group || '';
const avg = number(row.latency_avg_ms);
const p95 = number(row.latency_p95_ms);
const p99 = number(row.latency_p99_ms);
const cv = number(row.cv_percent);
if (group === 'busqueda_textual') {
return avg <= 3000 && p95 <= 5000 && p99 <= 8000 ? 'Cumple latencia textual' : 'No cumple latencia textual';
}
return avg <= 1000 && p95 <= 2000 && p99 <= 3000 && cv <= 30
? 'Cumple latencia estructurada'
: 'No cumple latencia estructurada';
};
const main = () => {
const baseline = readSummary('baseline_summary.csv');
const optimized = readSummary('optimized_summary.csv');
const concurrency = readSummary('concurrency_summary.csv');
const generatedAt = new Date().toISOString();
const baselineRows = baseline.map((row) => ({
consulta: row.query_id,
grupo: row.query_group,
n: row.n,
promedio_ms: fmt(row.latency_avg_ms),
p95_ms: fmt(row.latency_p95_ms),
p99_ms: fmt(row.latency_p99_ms),
cv: fmt(row.cv_percent, ' %'),
fallos: row.failures,
throughput_s: fmt(row.throughput_per_second),
criterio: criteria(row)
}));
const optimizedRows = optimized.map((row) => ({
consulta: row.query_id,
grupo: row.query_group,
n: row.n,
promedio_ms: fmt(row.latency_avg_ms),
p95_ms: fmt(row.latency_p95_ms),
p99_ms: fmt(row.latency_p99_ms),
cv: fmt(row.cv_percent, ' %'),
fallos: row.failures,
throughput_s: fmt(row.throughput_per_second),
criterio: criteria(row)
}));
const groupSummary = (rows) => {
const map = new Map();
for (const row of rows) {
const key = row.query_group || 'sin_grupo';
if (!map.has(key)) map.set(key, []);
map.get(key).push(row);
}
return Array.from(map.entries()).map(([group, items]) => {
const totalN = items.reduce((acc, item) => acc + Number(item.n || 0), 0);
const weightedAvg =
totalN > 0
? items.reduce((acc, item) => acc + Number(item.latency_avg_ms || 0) * Number(item.n || 0), 0) / totalN
: null;
const p95Max = Math.max(...items.map((item) => Number(item.latency_p95_ms || 0)));
const failures = items.reduce((acc, item) => acc + Number(item.failures || 0), 0);
return { group, totalN, weightedAvg, p95Max, failures };
});
};
const baselineGroupSummary = groupSummary(baseline);
const measuredTotal = baseline.reduce((acc, row) => acc + Number(row.n || 0), 0);
const expectedSequential = config.queries.length * config.measuredRepetitions;
const baselineArticleText = baselineGroupSummary.length
? baselineGroupSummary
.map(
(item) =>
`Para el grupo ${item.group}, la validacion disponible contiene ${item.totalN} operaciones, con latencia promedio de ${fmt(
item.weightedAvg,
' ms'
)}, p95 de ${fmt(item.p95Max, ' ms')} y ${item.failures} fallos.`
)
.join(' ')
: 'No existen resultados de linea base medidos en los CSV.';
const report = `# Informe de rendimiento INVIMA
Generado: ${generatedAt}
## 1. Infraestructura
Infraestructura minima de referencia declarada: 4 GB RAM, 2 vCPU compartidas, 80 GB de almacenamiento, 4 TB de transferencia mensual y red nominal de hasta 40 Gbps de entrada / 4 Gbps de salida. Las versiones reales del entorno se registran con \`npm run benchmark:resources\` en \`benchmarks/results/resource_snapshot.json\`.
## 2. Condiciones de prueba
El protocolo completo usa ${config.warmupRepetitions} repeticiones de calentamiento por consulta, ${config.measuredRepetitions} repeticiones medidas por cada una de las siete consultas y ${config.concurrencyRequestsPerLevel} solicitudes por cada nivel de concurrencia (${config.concurrencyLevels.join(', ')} usuarios virtuales). El calentamiento no se incluye en metricas.
## 3. Estado del servidor antes de iniciar
Ver \`benchmarks/results/resource_snapshot.json\`. Si el archivo no existe, ejecute \`npm run benchmark:resources\`.
## 4. Consultas evaluadas
${markdownTable(config.queries.map((query) => ({ id: query.id, nombre: query.name, grupo: query.group, endpoint: query.endpoint })), [
{ key: 'id', title: 'ID' },
{ key: 'nombre', title: 'Consulta' },
{ key: 'grupo', title: 'Grupo' },
{ key: 'endpoint', title: 'Endpoint asociado' }
])}
## 5. Metodologia
Cada ejecucion usa \`EXPLAIN (ANALYZE, BUFFERS, VERBOSE, FORMAT JSON)\`, registra latencia total de la operacion SQL, tiempos de planeacion y ejecucion de PostgreSQL, filas estimadas y reales, buffers, estado de exito o fallo, CPU/memoria del proceso de benchmark y conexiones activas a PostgreSQL. Los campos de HTTP quedan como \`SQL_ONLY\` porque estas siete consultas fueron identificadas como mediciones SQL representativas; los endpoints asociados se documentan para trazabilidad.
## 6. Resultados por consulta - linea base
${markdownTable(baselineRows, [
{ key: 'consulta', title: 'Consulta' },
{ key: 'grupo', title: 'Grupo' },
{ key: 'n', title: 'n' },
{ key: 'promedio_ms', title: 'Promedio ms' },
{ key: 'p95_ms', title: 'p95 ms' },
{ key: 'p99_ms', title: 'p99 ms' },
{ key: 'cv', title: 'CV' },
{ key: 'fallos', title: 'Fallos' },
{ key: 'throughput_s', title: 'Throughput/s' },
{ key: 'criterio', title: 'Criterio' }
])}
## 7. Resultados de concurrencia
${markdownTable(concurrency.map((row) => ({
nivel: row.concurrency_level,
consulta: row.query_id || 'agregado',
n: row.n,
promedio_ms: fmt(row.latency_avg_ms),
p95_ms: fmt(row.latency_p95_ms),
fallos: row.failures,
throughput_s: fmt(row.throughput_per_second)
})), [
{ key: 'nivel', title: 'Usuarios' },
{ key: 'consulta', title: 'Consulta' },
{ key: 'n', title: 'n' },
{ key: 'promedio_ms', title: 'Promedio ms' },
{ key: 'p95_ms', title: 'p95 ms' },
{ key: 'fallos', title: 'Fallos' },
{ key: 'throughput_s', title: 'Throughput/s' }
])}
## 8. Recursos
Los resultados crudos incluyen CPU del proceso, memoria RSS, operaciones de lectura/escritura reportadas por Node.js y conexiones activas. Para CPU/RAM del servidor y PostgreSQL se debe complementar con herramientas del sistema operativo si se requiere granularidad por proceso.
## 9. EXPLAIN ANALYZE
Cada fila cruda conserva planeacion, ejecucion, filas estimadas/reales y buffers. Para conservar el plan JSON completo, use una corrida puntual con \`BENCHMARK_REPETITIONS=1\` o exporte desde PostgreSQL si se requiere auditoria completa de planes.
## 10. Optimizaciones aplicadas
Las optimizaciones reversibles estan en \`benchmarks/migrations/optimization_up.sql\` y \`benchmarks/migrations/optimization_down.sql\`. Incluyen indices B-tree para orden/filtros frecuentes, indices GIN trigram para los campos usados por \`ILIKE '%marcapasos%'\`, aumento de estadisticas por columna y \`ANALYZE\`.
## 11. Comparacion antes/despues
${optimized.length ? markdownTable(optimizedRows, [
{ key: 'consulta', title: 'Consulta' },
{ key: 'grupo', title: 'Grupo' },
{ key: 'n', title: 'n' },
{ key: 'promedio_ms', title: 'Promedio ms' },
{ key: 'p95_ms', title: 'p95 ms' },
{ key: 'p99_ms', title: 'p99 ms' },
{ key: 'cv', title: 'CV' },
{ key: 'fallos', title: 'Fallos' },
{ key: 'throughput_s', title: 'Throughput/s' },
{ key: 'criterio', title: 'Criterio' }
]) : 'No hay resultados optimizados disponibles. Ejecute `npm run benchmark:optimized` despues de `npm run benchmark:optimize`.\n'}
## 12. Criterios cumplidos y no cumplidos
No se declara cumplimiento sin resultados crudos. Cuando existan CSV completos, esta seccion debe leerse junto con las columnas \`criterio\`, \`failure_rate_percent\`, \`latency_p95_ms\` y \`latency_p99_ms\`.
## 13. Limitaciones
- Las siete consultas representan mediciones SQL; el tiempo HTTP real requiere ejecutar el backend, autenticar y medir endpoints con token.
- La busqueda textual real combina FTS con varios \`ILIKE\`, lo cual puede ser costoso.
- Ejecutar 1 200 operaciones completas puede tardar mucho si las busquedas textuales mantienen tiempos del orden de decenas de segundos.
- EA, MSE, precision y exhaustividad no calculables por ausencia de un conjunto de referencia validado.
## 14. Recomendaciones
- Ejecutar primero una prueba corta con \`BENCHMARK_REPETITIONS=1 BENCHMARK_CONCURRENCY_REQUESTS=7\`.
- Ejecutar el protocolo completo en una ventana controlada.
- Revisar planes de \`query_06\` y \`query_07\` antes y despues de los indices trigram.
- Considerar una refactorizacion futura de busqueda textual para usar una columna \`tsvector\` persistida o una estrategia de union controlada, sin eliminar campos validos.
`;
fs.writeFileSync(path.join(benchmarkDir, 'INFORME_RENDIMIENTO.md'), report, 'utf8');
const article = `# Resumen para articulo
El sistema de benchmarking definido para la plataforma INVIMA evalua siete consultas representativas sobre PostgreSQL, con ${config.warmupRepetitions} repeticiones de calentamiento y ${config.measuredRepetitions} repeticiones medidas por consulta, ademas de pruebas concurrentes con ${config.concurrencyLevels.join(', ')} usuarios virtuales. Los resultados crudos se almacenan en CSV y JSON para preservar trazabilidad.
Estado actual: se encontraron ${measuredTotal} operaciones secuenciales medidas en los CSV de linea base. El protocolo completo espera ${expectedSequential} operaciones secuenciales medidas, por lo que cualquier interpretacion debe distinguir la validacion corta de la ejecucion completa. ${baselineArticleText}
Cuando se ejecute el protocolo completo, esta seccion debe reportar numero total de operaciones, latencia promedio por grupo de consulta, percentil 95, throughput, tasa de fallos y comportamiento bajo concurrencia. Si se aplican las optimizaciones reversibles, se debe comparar la linea base contra la medicion optimizada y reportar mejora absoluta y porcentual. No se declara cumplimiento de criterios con una corrida incompleta.
EA, MSE, precision y exhaustividad no calculables por ausencia de un conjunto de referencia validado. El archivo \`benchmarks/ground_truth.example.json\` define la estructura sugerida para incorporar una referencia manual en una etapa posterior.
`;
fs.writeFileSync(path.join(benchmarkDir, 'RESUMEN_ARTICULO.md'), article, 'utf8');
writeJson(path.join(resultsDir, 'report_metadata.json'), { generatedAt, baselineRows: baseline.length, optimizedRows: optimized.length, concurrencyRows: concurrency.length });
};
main();