Imported from Luqueee/kivgraph (
benchmarks/AGENTS.md). Install upstream withnpx skills add Luqueee/kivgraph --skill benchmarks. Copyright stays with the author.
Instrucciones de los benchmarks (benchmarks/)
Estas reglas se suman a las de AGENTS.md en la raíz del repositorio, que se
leen siempre. Una instrucción de este archivo puede añadir restricciones; nunca
puede relajar un contrato de integridad, compatibilidad o verificación
declarado en la raíz.
- Los benchmarks viven en
benchmarks/<nombre>/, conresults.jsonyreport.md. Deben conservar comando, commit, entorno, dataset, semilla, métricas y limitaciones. - Los benchmarks de observabilidad deben separar la ruta local, el proveedor
noopy cualquier proveedor SDK configurado explícitamente; no se deben presentar como un único coste de producción. - El benchmark end-to-end del visor se versiona en
benchmarks/web-viewer/; el harness falla cerrado ante una métrica fuera de límite y no emiteWEB_VIEWER_PERFORMANCE_PASSsi el corpus o GPU no coinciden con la referencia declarada. benchmarks/tool-honesty/no mide aristas: mide qué afirma una tool cuando su respuesta está vacía, conduciendo el binario real por MCP contra un corpus con puntos ciegos a propósito. Los repositorios limpios son la mitad del diseño: sin ellos, un veredicto constanteLOWER_BOUNDpasaría todas las comprobaciones. Los dos lenguajes van en un solo corpus para poder comprobar que el veredicto no se contagia entre ellos, y cada brazo declara su propio ámbito ciego: la pasada se niega si alguno perdió el suyo. El ámbito se lee degraph_status, no del fixture. El brazo Rust se salta declarándose cuando falta su toolchain, y preservaRUSTUP_HOMEporque unHOMEaislado deja arustupsin toolchains.benchmarks/edit-frequencyno mide una reconstrucción contra otra: mide qué le cuesta el grafo a un agente que edita, que es la carga por la que la issue#106reabrió el ADR 0057. Sus dos brazos se encuentran en una sola cifra -- cuántas preguntas compra una reconstrucción-- porque es la única unidad que comparten: editar no le cuesta nada al brazo de búsqueda, y preguntar no le cuesta casi nada al grafo. Medido:17,150 spor pase tras editar un fichero contra0,101 spor pregunta buscada y leída, o sea169,5preguntas por reconstrucción, y82,7 %del pase es publicar.- El pase de calentamiento se mide, se publica y no entra en ninguna
mediana. Es el que llena la caché de hechos, y un agente que edita no lo
corre nunca: mezclarlo en la mediana informa de un coste que nadie paga. La
diferencia no es marginal --
123,220 sen frío contra17,150en caliente. - Y la caché de hechos está clavada a la huella del binario que indexa, así
que una corrida lanzada con
go runrecompila a otra ruta y no puede acertar ni su propio calentamiento. Las corridas publicadas se hacen con un binario construido, y el informe lo dice en su orden de reproducción. Un harness que no lo hiciera mediría un pase frío y lo llamaría caliente. - El brazo de búsqueda declara qué buscador corrió.
rgno siempre está en elPATH-- los hosts de agente lo empaquetan dentro de su propio ejecutable-- y la alternativagrepnecesita las exclusiones escritas a mano, porque ripgrep no baja a un árbol de dependencias instaladas y ungrepque sí bajara cronometraría una búsqueda que ninguna sesión hace. El buscador más lento favorece al grafo, así que la cifra publicada es una cota inferior y el informe la nombra así. benchmarks/snapshot-heaptampoco mide páginas residentes: separa, en lo que cuesta cargar un snapshot publicado, los bytes que un lector conserva de los que la carga asigna y tira. Toma el perfil con el snapshot vivo, que es la única forma de atribuirlo: el benchmark del paquete escribe el suyo cuando ya es inalcanzable y no atribuye ni un byte.- Y las dos mitades no son la misma cifra en
Private_Dirty, que es lo que este archivo decía. Sólo la que se conserva lo es en régimen estacionario:benchmarks/load-cost-residentretiró60,5 MBde la mitad transitoria y el residente por servidor no se movió (71,76 MBcontra71,22, tres pares de tres). Bajar lo asignado compra tiempo hasta la primera respuesta; los bytes por proceso se bajan moviendo una estructura al fichero mapeado, y sólo eso. Quien escriba una cifra desnapshot-heapen una ficha de memoria residente está citando la magnitud equivocada. benchmarks/load-cost-residentcorre en un contenedor Linux y no es el host de referencia: lo que hace comparables sus unidades es el page size de4096bytes, y lo que hace comparables sus dos brazos es que corrieron en la misma VM contra el mismo fichero. No sobrescribe los artefactos deshared-snapshot, y no afirma ningún límite de latencia.benchmarks/daemon-costresponde qué cuesta un proceso sirviendo a N clientes contra N procesos sirviendo a uno. Lo que publica como respuesta es la pendiente por cliente, no ningún total: un brazo que ahorrara a dos clientes y no a ocho parecería una victoria en cualquier fila suelta. Mide el recuento de un cliente aunque un demonio no comparta nada allí, porque es donde su coste fijo sería visible sin nada que amortizarlo -- y ahí resultó que empata, desmintiendo la predicción del ADR 0065.daemon-costmide las dos puertas del demonio a tres cargas y publica un artefacto por combinación:results-idle.jsonyresults-http-idle.jsonsin ninguna llamada,results.jsonyresults-http.jsoncon8, yresults-http-sustained.jsoncon2.000. El transporte y los recuentos entran en el digest; sin eso las corridas colisionan en una identidad y una cifra de socket puede citarse como si fuera alcanzable. El esquema esdaemon-cost-v3.- La carga cero no es un extremo teórico: es la mediana.
48de51servidores reales no reciben ninguna llamada, así que-calls 0mide el caso que predomina. Ahí el arranque resultó ser el coste --33 MBpor cliente sin contestar nada, contra40contestando-- y eso se arregló: el ADR 0067 movió la lectura del grafo a la primera consulta, y la cifra ociosa vigente es10 MBpor cliente contra39contestando y66bajo tráfico sostenido. Es también la única carga en la que los cuatro puntos del barrido miden lo mismo por cliente, porque-calls Nreparte N llamadas entre los clientes que haya. - Un árbol sucio no publica un commit a secas.
commitlleva-dirtycuando hay cambios sin commitear y-unknowncuando no se pudo saber, y las dos variantes se declaran enlimitations. Es el caso normal -- las cifras que justifican un cambio se miden antes de commitearlo-- y sin el sufijo el artefacto atribuye sus números a un código que no ejecutó. Las corridas publicadas se hacen desde un árbol limpio. - Un guardia no puede ser carga. El
graph_statusque prueba que los dos brazos sirven la misma generación corre después del muestreo, no antes: nada obliga a que preceda a los bytes, y preguntando primero la carga cero era imposible de medir. Falla y descarta igual. Ese movimiento es lo que subió el esquema dev2av3: un ficherov2incluye esa llamada en sus bytes. - Lo que no se midió no se publica como cero. Los percentiles, el
new_client_msy los ratios de latencia son punteros y desaparecen del fichero cuando la corrida no preguntó nada; el resumen imprime--. Unp50_ms: 0se lee como una respuesta instantánea y unp99_ratio: 0como un demonio infinitamente más rápido. Lo que sí se mide a toda carga esnew_client_connect_ms, que es el campo con el que dos cargas se comparan. - Un probe que sólo existe en el camino de un servidor real -- los de
startServeryconnect-- no lo caza ningún test local: borrarlo no rompe nada en un portátil. Por eso la corrida se niega a publicar un fichero ocioso que haya cronometrado algún primer answer, y ese rechazo sí tiene test. - La carga se cuenta, no se elige, y es la variable que decidió el resultado
dos veces. El event log de un
serveregistra cada llamada de tool, así que la carga de una sesión real se recuenta de un log de uso -- la orden está en el informe. Medido: mediana de una llamada por sesión y48de51servidores sin ninguna. Las2.000llamadas del caso sostenido son tres órdenes de magnitud por encima de eso, y ahí HTTP parecía costar12,5 MBpor cliente cuando a carga real cuesta menos de1 MB, igual que el socket. Un benchmark que mide la carga equivocada no es impreciso: contesta otra pregunta, y en este caso subestimaba el ahorro. - El caso sostenido se conserva en
results-http-sustained.jsonporque es un techo útil, y su cifra no se transporta entre corpus:4,9–5,9 MBpor cliente sobre117.499símbolos contra12,8sobre108.737, reproducido dos veces en corpus de ese tamaño. Esa dependencia del corpus es la firma de un coste en bytes retenidos -- el buffer de reanudación de10 MiBque el SDK da a cada sesión-- y no de un coste por sesión. Citar el techo como si fuera el coste es el error que este benchmark ya cometió. - No es un brazo de
shared-snapshoty no debe convertirse en uno: los brazos de aquél se definen por si el fichero de snapshot está, y su gate mide mapear contra derivar. Un tercer brazo dejaría su comparación sin significado. - Una cifra por símbolo se lee de la pasada que la produjo, nunca cruzada entre
corpus. Las corridas vigentes de
daemon-costusan los108.737símbolos deworkspaceen su generación000001; las de117.499están en el historial, no en estas tablas.workspacees un workspace en uso, así que un reindexado posterior no reproduce el recuento anterior: el corpus se declara por pasada.
Corpus y auditorías
- Los corpus sintéticos de aceptación de gran escala se generan en una ruta privada y nunca sustituyen ni modifican repositorios indexados. Para LadybugDB, la reproducibilidad debe distinguir entre hechos lógicos (conteos, schema e integridad) y bytes físicos del archivo nativo.
- Una auditoría de exactitud debe separar
false exact edgesde aristas colgantes: compara fixtures con ground truth para las primeras y ejecuta las invariantes canónicas de extremos, evidencia y procedencia para las segundas. - Un informe
ACCEPT_KIVGRAPH_WITH_LIMITSdebe enumerar plataforma, toolchains, corpus, transporte, garantías, métricas y riesgos residuales; no puede convertir una limitación conocida en un PASS implícito.
dist/ y los repositorios indexados nunca se usan como entrada: se generan
copias o fixtures privados.
Un límite de latencia no se afirma en go test ./...
Un SLO es una propiedad de la máquina que lo mide, y go test ./... corre donde
sea: en un runner compartido con dos trabajos más al lado. Afirmarlo ahí produce
un fallo que no describe el código.
Ocurrió: la release v0.2.0 cayó con find_cross_repo_consumers en p95 de
11,6 ms contra un límite de 5 ms, sobre el mismo commit cuyo CI acababa de
pasar en otro runner, y con cinco de cinco pasadas locales por debajo del
límite. El código no había cambiado entre las dos observaciones.
Así que el límite se comprueba cuando alguien lo pide -KIVGRAPH_BENCH_SLO=1,
que es lo que significa una puerta de benchmark- y se informa el resto del
tiempo. Lo que sí se afirma siempre es que la medición ocurrió: un harness que
dejara de evaluar sus comprobaciones pasaría igual, y eso es peor que un límite
incumplido.
La misma regla vale para cualquier gate que dependa del entorno -rust-src
instalado, una GPU, un corpus- y por el mismo motivo: la ausencia se declara y se
salta, nunca se convierte en un FAIL que apunta al sitio equivocado.