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();