go-ozzo / ozzo-validation

An idiomatic Go (golang) validation package. Supports configurable and extensible validation rules (validators) using normal language constructs instead of error-prone struct tags.
MIT License
3.73k stars 224 forks source link

Bad Performance? #104

Closed Codebreaker101 closed 4 years ago

Codebreaker101 commented 4 years ago

Here is a simple test that check performance:

package tenant_test

import (
    "fmt"
    "testing"
    "time"

    ozzo "github.com/go-ozzo/ozzo-validation/v4"
    validation "github.com/go-ozzo/ozzo-validation/v4"
    ozzois "github.com/go-ozzo/ozzo-validation/v4/is"
    "github.com/go-playground/validator/v10"
)

// User contains user information
type User struct {
    FirstName string `validate:"required"`
    LastName  string `validate:"required"`
    Age       int32  `validate:"gte=0,lte=130"`
    Email     string `validate:"required,email"`
    Street    string `validate:"required"`
    City      string `validate:"required"`
    Planet    string `validate:"required"`
    Phone     string `validate:"required"`
}

func (u User) Validate() error {
    return ozzo.ValidateStruct(&u,
        validation.Field(&u.FirstName, ozzo.Required),
        validation.Field(&u.LastName, ozzo.Required),
        validation.Field(&u.Age, ozzo.Min(0), ozzo.Max(130)),
        validation.Field(&u.Email, ozzo.Required, ozzois.Email),
        validation.Field(&u.Street, ozzo.Required),
        validation.Field(&u.City, ozzo.Required),
        validation.Field(&u.Planet, ozzo.Required),
        validation.Field(&u.Phone, ozzo.Required),
    )
}

var validate *validator.Validate

const iterations = 1000

func TestValidation(t *testing.T) {
    validate = validator.New()

    user := &User{
        FirstName: "Badger",
        LastName:  "Smith",
        Age:       80,
        City:      "string",
        Email:     "Badger.Smith@gmail.com",
        Street:    "Eavesdown Docks",
        Planet:    "Persphone",
        Phone:     "none",
    }

    before := time.Now()
    for i := 0; i < iterations; i++ {
        validate.Struct(user)
    }
    fmt.Println(time.Since(before))

    before = time.Now()
    for i := 0; i < iterations; i++ {
        validate.Var(user.FirstName, "required")
        validate.Var(user.LastName, "required")
        validate.Var(user.Age, "gte=0,lte=130")
        validate.Var(user.Email, "required,email")
        validate.Var(user.Street, "required")
        validate.Var(user.City, "required")
        validate.Var(user.Planet, "required")
        validate.Var(user.Phone, "required")
    }
    fmt.Println(time.Since(before))

    before = time.Now()
    for i := 0; i < iterations; i++ {
        ozzo.Validate(&user.FirstName, ozzo.Required)
        ozzo.Validate(&user.LastName, ozzo.Required)
        ozzo.Validate(&user.Age, ozzo.Min(0), ozzo.Max(130))
        ozzo.Validate(&user.Email, ozzo.Required, ozzois.Email)
        ozzo.Validate(&user.Street, ozzo.Required)
        ozzo.Validate(&user.City, ozzo.Required)
        ozzo.Validate(&user.Planet, ozzo.Required)
        ozzo.Validate(&user.Phone, ozzo.Required)
    }
    fmt.Println(time.Since(before))

    before = time.Now()
    for i := 0; i < iterations; i++ {
        user.Validate()
    }
    fmt.Println(time.Since(before))
}

And the results are:

11.725439ms
12.067247ms
1.107462798s
1.154800302s

ozzo validator is around 100x slower than pure go-validator. Any information about that?

y4h2 commented 4 years ago

I ran @Codebreaker101 's code on my local, it shows the same result. I dived into the details and found that the most time consuming part is the email validation part. If we remove the email validation part from both sides, we could find that ozzo validator is only a little bit slower. The below is my result.

1.216925ms
1.42975ms
2.081075ms
7.263525ms

ozzo validator depends on gopkg.in/asaskevich/govalidator.v9 Here is the source code how it validates emails

func IsEmail(email string) bool {
    if len(email) < 6 || len(email) > 254 {
        return false
    }
    at := strings.LastIndex(email, "@")
    if at <= 0 || at > len(email)-3 {
        return false
    }
    user := email[:at]
    host := email[at+1:]
    if len(user) > 64 {
        return false
    }

    if userDotRegexp.MatchString(user) || !userRegexp.MatchString(user) || !hostRegexp.MatchString(host) {
        return false
    }

    switch host {
    case "localhost", "example.com":
        return true
    }
    if _, err := net.LookupMX(host); err != nil {
        if _, err := net.LookupIP(host); err != nil {
            return false
        }
    }

    return true
}

You could notice that its logic is really time-consuming, it splits the email string into two pieces and then use regex match three times, and then it tries to use net package to check whether the host is reachable. I think ozzo validator may need to use other validators or implement itself's.

y4h2 commented 4 years ago

I think https://github.com/go-ozzo/ozzo-validation/issues/98 talks about the similiar thing.

qiangxue commented 4 years ago

Thank you for the investigation! Added is.EmailFormat which does string matching without checking MX. See https://github.com/go-ozzo/ozzo-validation/commit/f0cccd2ddbe5e809bdc6b8bfeb40d10ed25be3fb