performance close to handlers if no bindings but even if bindings except service (new feature is that we can bind functions as well) is x1.1 faster than the previous mvc implementation - make BaseController (so and C) optionally but not break the existing APIs that using iris.C or mvc.C

Former-commit-id: a26a8f836894c061e0f435df8ac1c2c534f0ee48
This commit is contained in:
Gerasimos (Makis) Maropoulos 2017-12-13 06:17:28 +02:00
parent 257f1318c9
commit 8dcbdc0741
11 changed files with 238 additions and 74 deletions

View File

@ -0,0 +1,23 @@
package controllers
// ValuesController is the equivalent
// `ValuesController` of the .net core 2.0 mvc application.
type ValuesController struct{}
/* on windows tests(older) the Get was:
func (vc *ValuesController) Get() {
// id,_ := vc.Params.GetInt("id")
// vc.Ctx.WriteString("value")
}
but as Iris is always going better, now supports return values as well*/
// Get handles "GET" requests to "api/values/{id}".
func (vc *ValuesController) Get() string {
return "value"
}
// Put handles "PUT" requests to "api/values/{id}".
func (vc *ValuesController) Put() {}
// Delete handles "DELETE" requests to "api/values/{id}".
func (vc *ValuesController) Delete() {}

View File

@ -0,0 +1,17 @@
package main
/// TODO: remove this on the "master" branch, or even replace it
// with the "iris-mvc" (the new implementatioin is even faster, close to handlers version,
// with bindings or without).
import (
"github.com/kataras/iris"
"github.com/kataras/iris/_benchmarks/iris-mvc2/controllers"
"github.com/kataras/iris/mvc2"
)
func main() {
app := iris.New()
mvc2.New().Controller(app.Party("/api/values/{id}"), new(controllers.ValuesController))
app.Run(iris.Addr(":5000"), iris.WithoutVersionChecker)
}

View File

@ -32,7 +32,7 @@ func init() {
filename := homeConfigurationFilename(".yml") filename := homeConfigurationFilename(".yml")
c, err := parseYAML(filename) c, err := parseYAML(filename)
if err != nil { if err != nil {
// this error will be occured the first time that the configuration // this error will be occurred the first time that the configuration
// file doesn't exist. // file doesn't exist.
// Create the YAML-ONLY global configuration file now using the default configuration 'c'. // Create the YAML-ONLY global configuration file now using the default configuration 'c'.
// This is useful when we run multiple iris servers that share the same // This is useful when we run multiple iris servers that share the same

View File

@ -25,6 +25,13 @@ type (
Store []Entry Store []Entry
) )
// GetByKindOrNil will try to get this entry's value of "k" kind,
// if value is not that kind it will NOT try to convert it the "k", instead
// it will return nil, except if boolean; then it will return false
// even if the value was not bool.
//
// If the "k" kind is not a string or int or int64 or bool
// then it will return the raw value of the entry as it's.
func (e Entry) GetByKindOrNil(k reflect.Kind) interface{} { func (e Entry) GetByKindOrNil(k reflect.Kind) interface{} {
switch k { switch k {
case reflect.String: case reflect.String:

View File

@ -86,6 +86,20 @@ func makeBindObject(v reflect.Value) (b bindObject, err error) {
return return
} }
// newContextBindObject is being used on both targetFunc and targetStruct.
// if the func's input argument or the struct's field is a type of Context
// then we can do a fast binding using the ctxValue
// which is used as slice of reflect.Value, because of the final method's `Call`.
func newContextBindObject() *bindObject {
return &bindObject{
Type: contextTyp,
BindType: functionResultType,
ReturnValue: func(ctxValue []reflect.Value) reflect.Value {
return ctxValue[0]
},
}
}
func (b *bindObject) IsAssignable(to reflect.Type) bool { func (b *bindObject) IsAssignable(to reflect.Type) bool {
return equalTypes(b.Type, to) return equalTypes(b.Type, to)
} }
@ -120,6 +134,15 @@ func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct
fields := lookupFields(typ, nil) fields := lookupFields(typ, nil)
for _, f := range fields { for _, f := range fields {
// if it's context then bind it directly here and continue to the next field.
if isContext(f.Type) {
s.Fields = append(s.Fields, &targetField{
FieldIndex: f.Index,
Object: newContextBindObject(),
})
continue
}
for _, val := range bindValues { for _, val := range bindValues {
// the binded values to the struct's fields. // the binded values to the struct's fields.
b, err := makeBindObject(val) b, err := makeBindObject(val)
@ -147,12 +170,9 @@ func newTargetStruct(v reflect.Value, bindValues ...reflect.Value) *targetStruct
func (s *targetStruct) Fill(destElem reflect.Value, ctx ...reflect.Value) { func (s *targetStruct) Fill(destElem reflect.Value, ctx ...reflect.Value) {
for _, f := range s.Fields { for _, f := range s.Fields {
f.Object.Assign(ctx, func(v reflect.Value) { f.Object.Assign(ctx, func(v reflect.Value) {
// defer func() { // if isContext(v.Type()) {
// if err := recover(); err != nil { // println("WTF BIND CONTEXT TYPE WHEN BASE CONTROLLER?")
// fmt.Printf("for index: %#v on: %s where num fields are: %d\n",
// f.FieldIndex, f.Object.Type.String(), destElem.NumField())
// } // }
// }()
destElem.FieldByIndex(f.FieldIndex).Set(v) destElem.FieldByIndex(f.FieldIndex).Set(v)
}) })
} }
@ -187,13 +207,7 @@ func newTargetFunc(fn reflect.Value, bindValues ...reflect.Value) *targetFunc {
if isContext(inTyp) { if isContext(inTyp) {
s.Inputs = append(s.Inputs, &targetFuncInput{ s.Inputs = append(s.Inputs, &targetFuncInput{
InputIndex: i, InputIndex: i,
Object: &bindObject{ Object: newContextBindObject(),
Type: contextTyp,
BindType: functionResultType,
ReturnValue: func(ctxValue []reflect.Value) reflect.Value {
return ctxValue[0]
},
},
}) })
continue continue
} }

99
mvc2/bind_values.go Normal file
View File

@ -0,0 +1,99 @@
package mvc2
import (
"reflect"
)
/// TODO:
// create another package because these bindings things are useful
// for other libraries I'm working on, so something like github.com/kataras/di
// will be great, combine these with the bind.go and controller's inside handler
// but generic things.
type ValueStore []reflect.Value
// Bind binds values to this controller, if you want to share
// binding values between controllers use the Engine's `Bind` function instead.
func (bv *ValueStore) Bind(values ...interface{}) {
for _, val := range values {
bv.bind(reflect.ValueOf(val))
}
}
func (bv *ValueStore) bind(v reflect.Value) {
if !goodVal(v) {
return
}
*bv = append(*bv, v)
}
// Unbind unbinds a binding value based on the type,
// it returns true if at least one field is not binded anymore.
//
// The "n" indicates the number of elements to remove, if <=0 then it's 1,
// this is useful because you may have bind more than one value to two or more fields
// with the same type.
func (bv *ValueStore) Unbind(value interface{}, n int) bool {
return bv.unbind(reflect.TypeOf(value), n)
}
func (bv *ValueStore) unbind(typ reflect.Type, n int) (ok bool) {
input := *bv
for i, in := range input {
if equalTypes(in.Type(), typ) {
ok = true
input = input[:i+copy(input[i:], input[i+1:])]
if n > 1 {
continue
}
break
}
}
*bv = input
return
}
// BindExists returns true if a binder responsible to
// bind and return a type of "typ" is already registered to this controller.
func (bv *ValueStore) BindExists(value interface{}) bool {
return bv.bindTypeExists(reflect.TypeOf(value))
}
func (bv *ValueStore) bindTypeExists(typ reflect.Type) bool {
input := *bv
for _, in := range input {
if equalTypes(in.Type(), typ) {
return true
}
}
return false
}
// BindIfNotExists bind a value to the controller's field with the same type,
// if it's not binded already.
//
// Returns false if binded already or the value is not the proper one for binding,
// otherwise true.
func (bv *ValueStore) BindIfNotExists(value interface{}) bool {
return bv.bindIfNotExists(reflect.ValueOf(value))
}
func (bv *ValueStore) bindIfNotExists(v reflect.Value) bool {
var (
typ = v.Type() // no element, raw things here.
)
if !goodVal(v) {
return false
}
if bv.bindTypeExists(typ) {
return false
}
bv.bind(v)
return true
}

View File

@ -62,8 +62,10 @@ type C struct {
var _ BaseController = &C{} var _ BaseController = &C{}
// BeginRequest starts the request by initializing the `Context` field. // BeginRequest does nothing anymore, is here to complet ethe `BaseController` interface.
func (c *C) BeginRequest(ctx context.Context) { c.Ctx = ctx } // BaseController is not required anymore, `Ctx` is binded automatically by the engine's
// wrapped Handler.
func (c *C) BeginRequest(ctx context.Context) {}
// EndRequest does nothing, is here to complete the `BaseController` interface. // EndRequest does nothing, is here to complete the `BaseController` interface.
func (c *C) EndRequest(ctx context.Context) {} func (c *C) EndRequest(ctx context.Context) {}
@ -91,13 +93,13 @@ type ControllerActivator struct {
// are used to build the bindings, and we need this field // are used to build the bindings, and we need this field
// because we have 3 states (Engine.Input, OnActivate, Bind) // because we have 3 states (Engine.Input, OnActivate, Bind)
// that we can add or override binding values. // that we can add or override binding values.
input []reflect.Value ValueStore // TODO: or ... this is dirty code I will have to re format it a bit tomorrow.
// the bindings that comes from input (and Engine) and can be binded to the controller's(initRef) fields. // the bindings that comes from input (and Engine) and can be binded to the controller's(initRef) fields.
bindings *targetStruct bindings *targetStruct
} }
func newControllerActivator(router router.Party, controller BaseController, bindValues ...reflect.Value) *ControllerActivator { func newControllerActivator(router router.Party, controller interface{}, bindValues ...reflect.Value) *ControllerActivator {
var ( var (
val = reflect.ValueOf(controller) val = reflect.ValueOf(controller)
typ = val.Type() typ = val.Type()
@ -128,6 +130,9 @@ func newControllerActivator(router router.Party, controller BaseController, bind
// are being appended to the slice at the `parseMethods`, // are being appended to the slice at the `parseMethods`,
// if a new method is registered via `Handle` its function name // if a new method is registered via `Handle` its function name
// is also appended to that slice. // is also appended to that slice.
//
// TODO: now that BaseController is totally optionally
// we have to check if BeginRequest and EndRequest should be here.
reservedMethods: []string{ reservedMethods: []string{
"BeginRequest", "BeginRequest",
"EndRequest", "EndRequest",
@ -136,10 +141,9 @@ func newControllerActivator(router router.Party, controller BaseController, bind
// set the input as []reflect.Value in order to be able // set the input as []reflect.Value in order to be able
// to check if a bind type is already exists, or even // to check if a bind type is already exists, or even
// override the structBindings that are being generated later on. // override the structBindings that are being generated later on.
input: bindValues, ValueStore: bindValues,
} }
c.parseMethods()
return c return c
} }
@ -178,33 +182,13 @@ func (c *ControllerActivator) parseMethods() {
// SetBindings will override any bindings with the new "values". // SetBindings will override any bindings with the new "values".
func (c *ControllerActivator) SetBindings(values ...reflect.Value) { func (c *ControllerActivator) SetBindings(values ...reflect.Value) {
// set field index with matching binders, if any. // set field index with matching binders, if any.
c.input = values c.ValueStore = values
c.bindings = newTargetStruct(c.Value, values...) c.bindings = newTargetStruct(c.Value, values...)
} }
// Bind binds values to this controller, if you want to share
// binding values between controllers use the Engine's `Bind` function instead.
func (c *ControllerActivator) Bind(values ...interface{}) {
for _, val := range values {
if v := reflect.ValueOf(val); goodVal(v) {
c.input = append(c.input, v)
}
}
}
// BindTypeExists returns true if a binder responsible to
// bind and return a type of "typ" is already registered to this controller.
func (c *ControllerActivator) BindTypeExists(typ reflect.Type) bool {
for _, in := range c.input {
if equalTypes(in.Type(), typ) {
return true
}
}
return false
}
func (c *ControllerActivator) activate() { func (c *ControllerActivator) activate() {
c.SetBindings(c.input...) c.SetBindings(c.ValueStore...)
c.parseMethods()
} }
var emptyIn = []reflect.Value{} var emptyIn = []reflect.Value{}
@ -255,17 +239,31 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
// get the function's input arguments' bindings. // get the function's input arguments' bindings.
funcBindings := newTargetFunc(m.Func, pathParams...) funcBindings := newTargetFunc(m.Func, pathParams...)
// the element value, not the pointer.
elemTyp := indirectTyp(c.Type)
// we will make use of 'n' to make a slice of reflect.Value // we will make use of 'n' to make a slice of reflect.Value
// to pass into if the function has input arguments that // to pass into if the function has input arguments that
// are will being filled by the funcBindings. // are will being filled by the funcBindings.
n := len(funcIn) n := len(funcIn)
handler := func(ctx context.Context) { // the element value, not the pointer, wil lbe used to create a
// new controller on each incoming request.
elemTyp := indirectTyp(c.Type)
implementsBase := isBaseController(c.Type)
handler := func(ctx context.Context) {
// create a new controller instance of that type(>ptr). // create a new controller instance of that type(>ptr).
ctrl := reflect.New(elemTyp) ctrl := reflect.New(elemTyp)
// // the Interface(). is faster than MethodByName or pre-selected methods.
// b := ctrl.Interface().(BaseController)
// // init the request.
// b.BeginRequest(ctx)
// // if begin request stopped the execution.
// if ctx.IsStopped() {
// return
// }
if implementsBase {
// the Interface(). is faster than MethodByName or pre-selected methods. // the Interface(). is faster than MethodByName or pre-selected methods.
b := ctrl.Interface().(BaseController) b := ctrl.Interface().(BaseController)
// init the request. // init the request.
@ -276,11 +274,15 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
return return
} }
// EndRequest will be called at any case except the `BeginRequest` is
// stopped.
defer b.EndRequest(ctx)
}
if !c.bindings.Valid && !funcBindings.Valid { if !c.bindings.Valid && !funcBindings.Valid {
DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn)) DispatchFuncResult(ctx, ctrl.Method(m.Index).Call(emptyIn))
} else { } else {
ctxValue := reflect.ValueOf(ctx) ctxValue := reflect.ValueOf(ctx)
if c.bindings.Valid { if c.bindings.Valid {
elem := ctrl.Elem() elem := ctrl.Elem()
c.bindings.Fill(elem, ctxValue) c.bindings.Fill(elem, ctxValue)
@ -309,11 +311,11 @@ func (c *ControllerActivator) Handle(method, path, funcName string, middleware .
} }
if ctx.IsStopped() { // if ctx.IsStopped() {
return // return
} // }
b.EndRequest(ctx) // b.EndRequest(ctx)
} }
// register the handler now. // register the handler now.

View File

@ -2,7 +2,6 @@
package mvc2_test package mvc2_test
import ( import (
"reflect"
"testing" "testing"
"github.com/kataras/iris" "github.com/kataras/iris"
@ -413,7 +412,7 @@ type testControllerActivateListener struct {
} }
func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) { func (c *testControllerActivateListener) OnActivate(ca *ControllerActivator) {
if !ca.BindTypeExists(reflect.TypeOf(&testBindType{})) { if !ca.BindExists(&testBindType{}) {
ca.Bind(&testBindType{ ca.Bind(&testBindType{
title: "default title", title: "default title",
}) })

View File

@ -37,8 +37,8 @@ func (e *Engine) Child() *Engine {
child := New() child := New()
// copy the current parent's ctx func binders and services to this new child. // copy the current parent's ctx func binders and services to this new child.
if l := len(e.Input); l > 0 { if n := len(e.Input); n > 0 {
input := make([]reflect.Value, l, l) input := make([]reflect.Value, n, n)
copy(input, e.Input) copy(input, e.Input)
child.Input = input child.Input = input
} }
@ -54,7 +54,7 @@ func (e *Engine) Handler(handler interface{}) context.Handler {
return h return h
} }
func (e *Engine) Controller(router router.Party, controller BaseController, onActivate ...func(*ControllerActivator)) { func (e *Engine) Controller(router router.Party, controller interface{}, onActivate ...func(*ControllerActivator)) {
ca := newControllerActivator(router, controller, e.Input...) ca := newControllerActivator(router, controller, e.Input...)
// give a priority to the "onActivate" // give a priority to the "onActivate"

View File

@ -8,10 +8,16 @@ import (
"github.com/kataras/pkg/zerocheck" "github.com/kataras/pkg/zerocheck"
) )
var contextTyp = reflect.TypeOf(context.NewContext(nil)) var baseControllerTyp = reflect.TypeOf((*BaseController)(nil)).Elem()
func isBaseController(ctrlTyp reflect.Type) bool {
return ctrlTyp.Implements(baseControllerTyp)
}
var contextTyp = reflect.TypeOf((*context.Context)(nil)).Elem()
func isContext(inTyp reflect.Type) bool { func isContext(inTyp reflect.Type) bool {
return inTyp.String() == "context.Context" // I couldn't find another way; context/context.go is not exported. return inTyp.Implements(contextTyp)
} }
func indirectVal(v reflect.Value) reflect.Value { func indirectVal(v reflect.Value) reflect.Value {

View File

@ -1,9 +1,6 @@
package mvc2 package mvc2
import ( import (
"reflect"
"github.com/kataras/golog"
"github.com/kataras/iris/context" "github.com/kataras/iris/context"
"github.com/kataras/iris/sessions" "github.com/kataras/iris/sessions"
) )
@ -25,11 +22,11 @@ type SessionController struct {
// It makes sure that its "Manager" field is filled // It makes sure that its "Manager" field is filled
// even if the caller didn't provide any sessions manager via the `app.Controller` function. // even if the caller didn't provide any sessions manager via the `app.Controller` function.
func (s *SessionController) OnActivate(ca *ControllerActivator) { func (s *SessionController) OnActivate(ca *ControllerActivator) {
if !ca.BindTypeExists(reflect.TypeOf(defaultManager)) { if didntBindManually := ca.BindIfNotExists(defaultManager); didntBindManually {
ca.Bind(defaultManager) ca.Router.GetReporter().Add(
golog.Warnf(`MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field, `MVC SessionController: couldn't find any "*sessions.Sessions" bindable value to fill the "Manager" field,
therefore this controller is using the default sessions manager instead. therefore this controller is using the default sessions manager instead.
Please refer to the documentation to learn how you can provide the session manager`) Please refer to the documentation to learn how you can provide the session manager`)
} }
} }