Skip to content

Commit

Permalink
Don't call MethodByName with a variable arg
Browse files Browse the repository at this point in the history
Go 1.22 goes somewhat toward addressing the issue of calls to reflection
MethodByName disabling linker deadcode elimination (DCE) resulting in
huge binaries by using GORM beause the linker cannot prune unused code
that *could* be reached via reflection.

Go Issue golang/go#62257 reduces the number of
incidences by leveraging a compiler assist to avoid marking functions
containing calls to MethodByName as ReflectMethods. An analysis of Uber
Technologies code base however shows that a number of transitive imports
still contain calls to MethodByName with a variable argument.

In the case of GORM, the solution we are proposing is because the
number of possible methods is finite, we will "unroll" this. This
demonstrably shows that GORM is not longer a problem for DCE.

Before
```
% go version
go version devel go1.22-2f3458a8ce Sat Sep 16 16:26:48 2023 -0700 darwin/arm64
% go test ./... -ldflags='-v=2' 2>  >(grep -i 'deadcode: giving up')
deadcode: giving up on static analysis due to relect method symbol gorm.io/gorm/schema.ParseWithSpecialTableName<3938>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).Method<119948>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).MethodByName<119949>
deadcode: giving up on static analysis due to relect method symbol gorm.io/gorm/schema.ParseWithSpecialTableName<49693>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).Method<117898>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).MethodByName<117899>
deadcode: giving up on static analysis due to relect method symbol gorm.io/gorm/schema.ParseWithSpecialTableName<54029>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).Method<117635>
deadcode: giving up on static analysis due to relect method symbol reflect.(*Value).MethodByName<117636>
ok      gorm.io/gorm    (cached)
ok      gorm.io/gorm/callbacks  (cached)
?       gorm.io/gorm/migrator   [no test files]
ok      gorm.io/gorm/clause     (cached)
ok      gorm.io/gorm/logger     (cached)
?       gorm.io/gorm/utils/tests        [no test files]
ok      gorm.io/gorm/schema     (cached)
ok      gorm.io/gorm/utils      (cached)
%
```

After

```
% go version
go version devel go1.22-2f3458a8ce Sat Sep 16 16:26:48 2023 -0700 darwin/arm64
%go test ./... -ldflags='-v=2' 2>  >(grep -i 'deadcode: giving up')
?       gorm.io/gorm/migrator   [no test files]
?       gorm.io/gorm/utils/tests        [no test files]
ok      gorm.io/gorm    0.474s
ok      gorm.io/gorm/callbacks  0.372s
ok      gorm.io/gorm/clause     0.605s
ok      gorm.io/gorm/logger     (cached)
ok      gorm.io/gorm/schema     0.689s
ok      gorm.io/gorm/utils      (cached)
%
```
  • Loading branch information
jquirke committed Sep 17, 2023
1 parent e57e5d8 commit 0db0c43
Showing 1 changed file with 55 additions and 5 deletions.
60 changes: 55 additions & 5 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ import (
"gorm.io/gorm/logger"
)

const (
_callbackTypeBeforeCreate = "BeforeCreate"
_callbackTypeAfterCreate = "AfterCreate"
_callbackTypeBeforeUpdate = "BeforeUpdate"
_callbackTypeAfterUpdate = "AfterUpdate"
_callbackTypeBeforeSave = "BeforeSave"
_callbackTypeAfterSave = "AfterSave"
_callbackTypeBeforeDelete = "BeforeDelete"
_callbakAfterDelete = "AfterDelete"
_callbackTypeAfterFind = "AfterFind"
)

// ErrUnsupportedDataType unsupported data type
var ErrUnsupportedDataType = errors.New("unsupported data type")

Expand Down Expand Up @@ -288,14 +300,20 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam
}
}

callbacks := []string{"BeforeCreate", "AfterCreate", "BeforeUpdate", "AfterUpdate", "BeforeSave", "AfterSave", "BeforeDelete", "AfterDelete", "AfterFind"}
for _, name := range callbacks {
if methodValue := modelValue.MethodByName(name); methodValue.IsValid() {
callbackTypes := []string{
_callbackTypeBeforeCreate, _callbackTypeAfterCreate,
_callbackTypeBeforeUpdate, _callbackTypeAfterUpdate,
_callbackTypeBeforeSave, _callbackTypeAfterSave,
_callbackTypeBeforeDelete, _callbakAfterDelete,
_callbackTypeAfterFind,
}
for _, cbName := range callbackTypes {
if methodValue := callBackToMethodValue(modelValue, cbName); methodValue.IsValid() {
switch methodValue.Type().String() {
case "func(*gorm.DB) error": // TODO hack
reflect.Indirect(reflect.ValueOf(schema)).FieldByName(name).SetBool(true)
reflect.Indirect(reflect.ValueOf(schema)).FieldByName(cbName).SetBool(true)
default:
logger.Default.Warn(context.Background(), "Model %v don't match %vInterface, should be `%v(*gorm.DB) error`. Please see https://gorm.io/docs/hooks.html", schema, name, name)
logger.Default.Warn(context.Background(), "Model %v don't match %vInterface, should be `%v(*gorm.DB) error`. Please see https://gorm.io/docs/hooks.html", schema, cbName, cbName)
}
}
}
Expand Down Expand Up @@ -349,6 +367,38 @@ func ParseWithSpecialTableName(dest interface{}, cacheStore *sync.Map, namer Nam
return schema, schema.err
}

// This unrolling is needed to show to the compiler the exact set of methods
// that can be used on the modelType.
// Prior to go1.22 any use of MethodByName would cause the linker to
// abandon dead code elimination for the entire binary.
// As of go1.22 the compiler supports one special case of a string constant
// being passed to MethodByName. For enterprise customers or those building
// large binaries, this gives a significant reduction in binary size.
// https://github.com/golang/go/issues/62257
func callBackToMethodValue(modelType reflect.Value, cbType string) reflect.Value {
switch cbType {
case _callbackTypeBeforeCreate:
return modelType.MethodByName(_callbackTypeBeforeCreate)
case _callbackTypeAfterCreate:
return modelType.MethodByName(_callbackTypeAfterCreate)
case _callbackTypeBeforeUpdate:
return modelType.MethodByName(_callbackTypeBeforeUpdate)
case _callbackTypeAfterUpdate:
return modelType.MethodByName(_callbackTypeAfterUpdate)
case _callbackTypeBeforeSave:
return modelType.MethodByName(_callbackTypeBeforeSave)
case _callbackTypeAfterSave:
return modelType.MethodByName(_callbackTypeAfterSave)
case _callbackTypeBeforeDelete:
return modelType.MethodByName(_callbackTypeBeforeDelete)
case _callbakAfterDelete:
return modelType.MethodByName(_callbakAfterDelete)
case _callbackTypeAfterFind:
return modelType.MethodByName(_callbackTypeAfterFind)
}
panic("unreachable")
}

func getOrParse(dest interface{}, cacheStore *sync.Map, namer Namer) (*Schema, error) {
modelType := reflect.ValueOf(dest).Type()
for modelType.Kind() == reflect.Slice || modelType.Kind() == reflect.Array || modelType.Kind() == reflect.Ptr {
Expand Down

0 comments on commit 0db0c43

Please sign in to comment.