Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve scanner.Map's performance by caching resolved tags #128

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions scanner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ ScanMapClose is the same as ScanMap but it also close the rows
Test cases blow may make sense

```go
package scaner
package scaner_test

import (
"github.com/didi/gendry/scanner"
"github.com/stretchr/testify/assert"
"testing"
)

func TestMap(t *testing.T) {
// you can improve the performance of `scanner.Map` by scanner.EnableMapNameCache(true)
scanner.EnableMapNameCache(true)
type Person struct {
Name string `ddb:"name"`
Age int `ddb:"age"`
Expand All @@ -65,14 +68,14 @@ func TestMap(t *testing.T) {
a := Person{"deen", 22, 1}
b := &Person{"caibirdme", 23, 1}
c := &b
mapA, err := Map(a, DefaultTagName)
mapA, err := scanner.Map(a, scanner.DefaultTagName)
ass := assert.New(t)
ass.NoError(err)
ass.Equal("deen", mapA["name"])
ass.Equal(22, mapA["age"])
_, ok := mapA["foo"]
ass.False(ok)
mapB, err := Map(c, "")
mapB, err := scanner.Map(c, "")
ass.NoError(err)
ass.Equal("caibirdme", mapB["Name"])
ass.Equal(23, mapB["Age"])
Expand Down
67 changes: 66 additions & 1 deletion scanner/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,25 @@ import (
"errors"
"reflect"
"strings"
"sync"
)

var (
// ErrNoneStructTarget as its name says
ErrNoneStructTarget = errors.New("[scanner] target must be a struct type")

enableMapNameCache bool

nameCache = &keyNameCache{
nameMap: make(map[reflect.Type]map[string][]string),
}
)

// EnableMapNameCache improve performance by caching resolved key names
func EnableMapNameCache(isEnable bool) {
enableMapNameCache = isEnable
}

// Map converts a struct to a map
// type for each field of the struct must be built-in type
func Map(target interface{}, useTag string) (map[string]interface{}, error) {
Expand All @@ -26,8 +38,21 @@ func Map(target interface{}, useTag string) (map[string]interface{}, error) {
}
t := v.Type()
result := make(map[string]interface{})

var getKeyName func(int) string
if enableMapNameCache {
names := nameCache.GetKeyNames(t, useTag)
getKeyName = func(i int) string {
return names[i]
}
} else {
getKeyName = func(i int) string {
return getKey(t.Field(i), useTag)
}
}

for i := 0; i < t.NumField(); i++ {
keyName := getKey(t.Field(i), useTag)
keyName := getKeyName(i)
if "" == keyName {
continue
}
Expand Down Expand Up @@ -64,3 +89,43 @@ func resolveTagName(tag string) string {
}
return tag[:idx]
}

type keyNameCache struct {
mutex sync.Mutex

// nameMap: type of struct -> map(tag -> resolved key names of the struct, array indexed by field index)
nameMap map[reflect.Type]map[string][]string
}

func (cache *keyNameCache) GetKeyNames(t reflect.Type, useTag string) []string {
names := cache.nameMap[t][useTag]
if names != nil {
return names
}
cache.mutex.Lock()
defer cache.mutex.Unlock()
// double check
names = cache.nameMap[t][useTag]
if names != nil {
return names
}
// resolve names then set to cache
names = cache.resolveKeyNamesOf(t, useTag)
if m, ok := cache.nameMap[t]; ok {
m[useTag] = names
} else {
m = make(map[string][]string)
cache.nameMap[t] = m
m[useTag] = names
}
return names
}

func (cache *keyNameCache) resolveKeyNamesOf(t reflect.Type, useTag string) []string {
result := make([]string, t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
result[i] = getKey(field, useTag)
}
return result
}
27 changes: 27 additions & 0 deletions scanner/map_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package scanner

import (
"testing"
)

var p = 12
var a = person{"deen", 22, 1, &p}

// go test -run=BenchmarkMap -bench=BenchmarkMap -cpu=1,2,4,8 -benchtime=20000000x -benchmem
func BenchmarkMapWithCache(b *testing.B) {
EnableMapNameCache(true)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = Map(&a, DefaultTagName)
}
})
}

func BenchmarkMapDisableCache(b *testing.B) {
EnableMapNameCache(false)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = Map(&a, DefaultTagName)
}
})
}