239 lines
11 KiB
JavaScript
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();
|