Saltar al contenido

Rendimiento

El rendimiento fue una preocupacion transversal abordada en el Milestone 15, aplicando optimizaciones en todos los paquetes. El GoFHIR Validator usa varias estrategias para minimizar allocations, reducir computacion y maximizar el throughput sin sacrificar la correctitud.

Estrategias de Optimizacion

Object Pooling con sync.Pool

El validador usa sync.Pool para reutilizar objetos frecuentemente creados, particularmente los structs Result y Stats que se crean en cada llamada de validacion. Esto elimina miles de allocations en el heap en escenarios de procesamiento por lotes.

var resultPool = sync.Pool{
    New: func() interface{} {
        return &Result{
            Issues: make([]issue.Issue, 0, 16),
        }
    },
}

func acquireResult() *Result {
    r := resultPool.Get().(*Result)
    r.Issues = r.Issues[:0] // Resetear slice, mantener backing array
    return r
}

func releaseResult(r *Result) {
    resultPool.Put(r)
}

Cache de Compilacion de Regex

Los tipos primitivos FHIR se validan contra expresiones regulares derivadas de sus StructureDefinitions. Como los mismos patrones se usan en miles de elementos, los regexes compilados se cachean para evitar llamadas repetidas a regexp.Compile.

var regexCache sync.Map

func getCompiledRegex(pattern string) (*regexp.Regexp, error) {
    if cached, ok := regexCache.Load(pattern); ok {
        return cached.(*regexp.Regexp), nil
    }
    compiled, err := regexp.Compile("^" + pattern + "$")
    if err != nil {
        return nil, err
    }
    regexCache.Store(pattern, compiled)
    return compiled, nil
}

Cache de Indices de Elementos

Cada StructureDefinition contiene un snapshot con potencialmente cientos de entradas ElementDefinition. El validador construye un indice path-a-elemento en el primer acceso y lo cachea para busquedas posteriores, convirtiendo escaneos lineales O(n) en busquedas O(1) en un mapa.

type elementIndex struct {
    byPath map[string]*ElementDefinition
    once   sync.Once
}

func (idx *elementIndex) Get(path string) *ElementDefinition {
    idx.once.Do(func() {
        idx.byPath = make(map[string]*ElementDefinition, len(idx.elements))
        for i := range idx.elements {
            idx.byPath[idx.elements[i].Path] = &idx.elements[i]
        }
    })
    return idx.byPath[path]
}

Especificaciones Embebidas

Las definiciones base de FHIR R4, R4B y R5 estan embebidas directamente en el binario usando el paquete embed de Go via pkg/specs. Esto elimina I/O de disco al inicio y hace que el binario del validador sea completamente autocontenido.

//go:embed data/r4/*.json
var r4Specs embed.FS

Esto significa que validator.New() puede cargar todos los StructureDefinitions base, ValueSets y CodeSystems desde memoria sin tocar el sistema de archivos.

Resultados de Benchmarks

Todos los benchmarks se ejecutaron con go test -bench=. -benchmem ./pkg/validator/ en una maquina de desarrollo estandar.

EscenarioMejoraAllocations
Patient Minimo4.0x mas rapido86% menos
Patient con Datos1.3x mas rapido43% menos
Validacion Paralela8.5x mas rapido74% menos
Procesamiento por Lotes2.1x mas rapido72% menos

La columna “Mejora” compara el validador optimizado (M15) contra la linea base pre-optimizacion. La columna “Allocations” muestra la reduccion en allocations del heap por operacion.

Ejecutar Benchmarks

Para ejecutar la suite completa de benchmarks:

go test -bench=. -benchmem ./pkg/validator/

Para ejecutar un benchmark especifico:

go test -bench=BenchmarkValidatePatient -benchmem ./pkg/validator/

Para comparar resultados de benchmarks entre ejecuciones, usa benchstat:

# Ejecutar benchmarks antes de los cambios
go test -bench=. -benchmem -count=10 ./pkg/validator/ > old.txt

# Hacer cambios, luego ejecutar de nuevo
go test -bench=. -benchmem -count=10 ./pkg/validator/ > new.txt

# Comparar
benchstat old.txt new.txt

Profiling de Memoria

Para identificar hotspots de allocations:

# Generar un perfil de memoria
go test -bench=BenchmarkValidatePatient -memprofile mem.prof ./pkg/validator/

# Analizar con pprof
go tool pprof -http=:8080 mem.prof

Para generar un perfil de CPU:

# Generar un perfil de CPU
go test -bench=BenchmarkValidatePatient -cpuprofile cpu.prof ./pkg/validator/

# Analizar
go tool pprof -http=:8080 cpu.prof

Mejores Practicas

Reutilizar la Instancia del Validador

Crear un validador carga e indexa StructureDefinitions. Crealo una vez y reutilizalo en todas las validaciones:

// CORRECTO: Crear una vez, reutilizar muchas veces
v, err := validator.New()
if err != nil {
    log.Fatal(err)
}

for _, resource := range resources {
    result := v.Validate(resource)
    // procesar resultado
}
// INCORRECTO: Crear un nuevo validador para cada recurso
for _, resource := range resources {
    v, _ := validator.New() // Costoso! Recarga todos los SDs
    result := v.Validate(resource)
}

Usar Validacion por Lotes

Al validar multiples recursos, pasalos como lote en lugar de validar uno a la vez. Esto habilita optimizaciones internas como reutilizacion de pools y menor presion en el GC:

results := v.ValidateBatch(resources)

Deshabilitar Fases No Utilizadas

Si sabes que ciertas fases de validacion no son necesarias para tu caso de uso, deshabilitalas para ahorrar tiempo de procesamiento:

// Omitir evaluacion de constraints FHIRPath y slicing si no se necesitan
v, err := validator.New(
    validator.WithDisabledPhases("constraint", "slicing"),
)

Deshabilitar Terminologia Sin Conexion

Si no hay un servidor de terminologia disponible, deshabilita la validacion de terminologia explicitamente para evitar retrasos por timeout:

v, err := validator.New(
    validator.WithTerminologyDisabled(),
)

O via la CLI:

gofhir-validator -tx n/a patient.json
Las estrategias de optimizacion descritas aqui se aplican automaticamente. No necesitas configurar pooling, cache ni indexacion – estan integradas en el validador. Las mejores practicas anteriores se enfocan en como usar el validador para aprovechar al maximo estas optimizaciones integradas.
Última actualización