Skip to content

Commit

Permalink
Improve performance of rdl.Validate via validator cache (#29)
Browse files Browse the repository at this point in the history
* Add specific validator tests

* Add code-generator

* Prepare for adding validator cache

* Implement validator cache

* Run benchmarks after successful build
  • Loading branch information
evantorrie authored and boynton committed May 6, 2017
1 parent 1b0ac88 commit b83b97e
Show file tree
Hide file tree
Showing 7 changed files with 381 additions and 11 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
*~
src
Makefile
rdl/_gen
*.iml
.idea
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ language: go

go: 1.6

before_install: # generate any other needed files
- go generate ./...

before_script:
- go vet ./...

after_success: # run benchmarks
- go test -v -bench . -run ^$ ./...
30 changes: 30 additions & 0 deletions rdl/codegen/codegen_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package codegen

import (
"github.com/ardielle/ardielle-go/rdl"
TestA "github.com/ardielle/ardielle-go/rdl/_gen/A"
"testing"
)

//go:generate go run generator.go

func BenchmarkA(b *testing.B) {
rdl.ValidatorUseCache(false)
for i := 0; i < b.N; i++ {
_ = rdl.Validate(TestA.CodegenSchema(), "StringStruct", TestA.Example)
}
}

func BenchmarkB(b *testing.B) {
rdl.ValidatorUseCache(true)
for i := 0; i < b.N; i++ {
_ = rdl.Validate(TestA.CodegenSchema(), "StringStruct", TestA.Example)
}
}

func TestCodeGenModel(test *testing.T) {
v := rdl.Validate(TestA.CodegenSchema(), "StringStruct", TestA.Example)
if !v.Valid {
test.Errorf("Validation error: %v, validation", v)
}
}
4 changes: 4 additions & 0 deletions rdl/codegen/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Stub to satisfy go 1.6 complaint about no buildable source files
//
// Future documentation placeholder
package codegen
178 changes: 178 additions & 0 deletions rdl/codegen/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// +build ignore

package main

import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"time"
)

type params struct {
Timestamp time.Time
Name string
StringTypeNames []string
}

const numTypes = 500

// Quick example that demonstrates a benchmark between different implementations
// of Validator
func main() {
p := params{Timestamp: time.Now(), Name: "codegen"}
for i := 0; i < numTypes; i++ {
p.StringTypeNames = append(p.StringTypeNames, fmt.Sprintf("Test%03d", i))
}

baseDir, err := os.Getwd()
die(err)
baseDir = filepath.Join(baseDir, "..")

fmt.Printf("Generating test files in %v\n", filepath.Join(baseDir, "_gen"))
apath := filepath.Join(baseDir, "_gen", "A")

_ = os.MkdirAll(apath, 0755)

instantiateTemplate(modelTemplate, p, filepath.Join(apath, "codegen_model.go"))
instantiateTemplate(schemaTemplate, p, filepath.Join(apath, "codegen_schema.go"))
instantiateTemplate(dataTemplate, p, filepath.Join(apath, "codegen_data.go"))
}

func instantiateTemplate(tmpl *template.Template, p params, fn string) {
f, err := os.Create(fn)
die(err)
defer f.Close()
die(tmpl.Execute(f, p))
}

func die(err error) {
if err != nil {
log.Fatal(err)
}
}

var funcMap = template.FuncMap{
"ToLower": strings.ToLower,
"Title": strings.Title,
}

var modelTemplate = template.Must(template.New("").Funcs(funcMap).Parse(`// go generate
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by robots at
// {{ .Timestamp }}
package {{ .Name }}
import (
"encoding/json"
"fmt"
"github.com/ardielle/ardielle-go/rdl"
)
var _ = rdl.Version
var _ = json.Marshal
var _ = fmt.Printf
{{ range .StringTypeNames }}
type {{ . }} string
{{- end }}
type StringStruct struct {
{{- range $index, $type := .StringTypeNames }}
{{ printf "%s%d" "Name" $index }} {{ $type }} ` + "`" + `json:{{- ToLower $type | printf "%q" -}}` + "`" + `
{{- end }}
}
type rawStringStruct StringStruct
// UnmarshalJSON
func (self *StringStruct) UnmarshalJSON(b []byte) error {
var r rawStringStruct
err := json.Unmarshal(b, &r)
if err == nil {
o := StringStruct(r)
*self = o
err = self.Validate()
}
return err
}
//
// Validate - checks for missing required fields, etc
//
func (self *StringStruct) Validate() error {
{{- range $index, $type := .StringTypeNames }}
{{- with $fieldName := printf "%s%d" "Name" $index }}
if self.{{- $fieldName }} == "" {
return fmt.Errorf("StringStruct.{{- $fieldName }} is missing but is a required field")
} else {
val := rdl.Validate( {{ Title $.Name -}}Schema(), {{ printf "%q" $type }}, self.{{- $fieldName }} )
if !val.Valid {
return fmt.Errorf("StringStruct.{{ $fieldName }} does not contain a valid {{ $type }} (%v)", val.Error)
}
}
{{- end }}
{{- end }}
return nil
}
`))

var schemaTemplate = template.Must(template.New("").Funcs(funcMap).Parse(`// go generate
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by robots at
// {{ .Timestamp }}
package {{ .Name }}
import (
rdl "github.com/ardielle/ardielle-go/rdl"
)
var (
schema *rdl.Schema
)
func init() {
sb := rdl.NewSchemaBuilder({{- printf "%q" .Name }})
sb.Version(1)
{{ range $index, $type := .StringTypeNames }}
t{{- $type }} := rdl.NewStringTypeBuilder({{ printf "%q" $type}})
t{{- $type }}.Pattern("[a-zA-Z_][a-zA-Z_0-9]*")
sb.AddType(t{{- $type }}.Build())
{{ end }}
tStringStruct := rdl.NewStructTypeBuilder("Struct", "StringStruct")
{{- range $index, $type := .StringTypeNames }}
{{- with $fieldName := printf "%s%d" "Name" $index }}
tStringStruct.Field({{ printf "%q" $fieldName }}, {{ printf "%q" $type }}, false, nil, "")
{{- end }}
{{- end }}
sb.AddType(tStringStruct.Build())
schema = sb.Build()
}
func {{ Title .Name -}}Schema() *rdl.Schema {
return schema
}
`))

var dataTemplate = template.Must(template.New("").Funcs(funcMap).Parse(`// go generate
// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT
// This file was generated by robots at
// {{ .Timestamp }}
package {{ .Name }}
var Example = &StringStruct{
{{- range $index, $type := .StringTypeNames }}
{{- with $fieldName := printf "%s%d" "Name" $index }}
{{ $fieldName }}: {{ printf "%q" $type }},
{{- end }}
{{- end }}
}
`))
73 changes: 62 additions & 11 deletions rdl/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"regexp"
"strconv"
"strings"
"sync"
)

//
Expand All @@ -34,41 +35,91 @@ func (v Validation) String() string {
return string(data)
}

// A validator contains the requisite information to validate a schema against its types
type validator struct {
registry TypeRegistry
schema *Schema
}

var validatorCache = struct {
sync.RWMutex
m map[*Schema]*validator
}{m: make(map[*Schema]*validator)}

var useValidatorCache bool

func ValidatorUseCache(flag bool) {
useValidatorCache = flag
}

func getValidator(schema *Schema) *validator {
var v *validator
if !useValidatorCache {
v = &validator{
schema: schema,
registry: NewTypeRegistry(schema),
}
return v
}

validatorCache.RLock()
if v, ok := validatorCache.m[schema]; ok {
validatorCache.RUnlock()
return v
}
validatorCache.RUnlock()

validatorCache.Lock()
defer validatorCache.Unlock()

// Check to see if someone else got in and wrote it prior to us
v, ok := validatorCache.m[schema]
if !ok {
v = &validator{
schema: schema,
registry: NewTypeRegistry(schema),
}
validatorCache.m[schema] = v
}
return v
}

// Validate tests the provided generic data against a type in the specified schema. If the typename is empty,
// an attempt to guess the type is made, otherwise the check is done against the single type.
func Validate(schema *Schema, typename string, data interface{}) Validation {
checker := new(validator)
checker.registry = NewTypeRegistry(schema)
checker.schema = schema
v := getValidator(schema)
return validateWithValidator(v, typename, data)
}

if schema.Types == nil {
return checker.bad("top level", "Schema contains no types", data, "")
// ValidateWithValidator tests the provided generic data using the supplied validator. If the typename is empty,
// an attempt to guess the type is made, otherwise the check is done against the single type.
// Supplying the validator allows it to be reused between validations rather than constructing anew
func validateWithValidator(validator *validator, typename string, data interface{}) Validation {
typelist := validator.schema.Types
if typelist == nil {
return validator.bad("top level", "Schema contains no types", data, "")
}
if typename == "" {
//iterate over the types until we find the most (defined last) general match
//But: not always useful: if structs are not "closed", they match on almost anything.
typelist := schema.Types
for i := len(typelist) - 1; i >= 0; i-- {
t := typelist[i]
tName, _, _ := TypeInfo(t)
v := checker.validate(t, data, string(tName))
v := validator.validate(t, data, string(tName))
if v.Error == "" {
return v
}
}
return checker.bad("top level", "Cannot determine type of data in schema", data, "")
return validator.bad("top level", "Cannot determine type of data in schema", data, "")
}

context := typename
typedef := checker.registry.FindType(TypeRef(typename))
typedef := validator.registry.FindType(TypeRef(typename))

if typedef != nil {
return checker.validate(typedef, data, context)
return validator.validate(typedef, data, context)
}
return checker.bad(context, "No such type", nil, "")
return validator.bad(context, "No such type", nil, "")
}

func (checker *validator) resolveAliases(typedef *Type, context string) *Type {
Expand Down
Loading

0 comments on commit b83b97e

Please sign in to comment.