github.com/orijtech/structslop@v0.0.9-0.20230520012622-069644583b8b/structslop.go (about) 1 // Copyright 2020 Orijtech, Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package structslop 16 17 import ( 18 "bytes" 19 "fmt" 20 "go/ast" 21 "go/build" 22 "go/format" 23 "go/parser" 24 "go/token" 25 "go/types" 26 "os" 27 "sort" 28 "strings" 29 30 "github.com/dave/dst" 31 "github.com/dave/dst/decorator" 32 "golang.org/x/tools/go/analysis" 33 "golang.org/x/tools/go/analysis/passes/inspect" 34 "golang.org/x/tools/go/ast/inspector" 35 ) 36 37 var ( 38 includeTestFiles bool 39 verbose bool 40 apply bool 41 generated bool 42 ) 43 44 func init() { 45 Analyzer.Flags.BoolVar(&includeTestFiles, "include-test-files", includeTestFiles, "also check test files") 46 Analyzer.Flags.BoolVar(&verbose, "verbose", verbose, "print all information, even when struct is not sloppy") 47 Analyzer.Flags.BoolVar(&apply, "apply", apply, "apply suggested fixes (using -fix won't work)") 48 Analyzer.Flags.BoolVar(&generated, "generated", generated, "report issues in generated code") 49 } 50 51 const Doc = `check for structs that can be rearrange fields to provide for maximum space/allocation efficiency` 52 53 // Analyzer describes struct slop analysis function detector. 54 var Analyzer = &analysis.Analyzer{ 55 Name: "structslop", 56 Doc: Doc, 57 Requires: []*analysis.Analyzer{inspect.Analyzer}, 58 Run: run, 59 } 60 61 func run(pass *analysis.Pass) (interface{}, error) { 62 // Use custom sizes instance, which implements types.Sizes for calculating struct size. 63 // go/types and gc does not agree about the struct size. 64 // See https://github.com/golang/go/issues/14909#issuecomment-199936232 65 pass.TypesSizes = &sizes{ 66 stdSizes: types.SizesFor(build.Default.Compiler, build.Default.GOARCH), 67 maxAlign: pass.TypesSizes.Alignof(types.Typ[types.UnsafePointer]), 68 } 69 70 dec := decorator.NewDecorator(pass.Fset) 71 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 72 nodeFilter := []ast.Node{ 73 (*ast.File)(nil), 74 (*ast.StructType)(nil), 75 } 76 77 fileDiags := make(map[string][]byte) 78 var af *ast.File 79 var df *dst.File 80 81 // Track generated files unless -generated is set. 82 genFiles := make(map[*token.File]bool) 83 if !generated { 84 files: 85 for _, f := range pass.Files { 86 for _, c := range f.Comments { 87 for _, l := range c.List { 88 if strings.HasPrefix(l.Text, "// Code generated ") && strings.HasSuffix(l.Text, " DO NOT EDIT.") { 89 file := pass.Fset.File(f.Pos()) 90 genFiles[file] = true 91 continue files 92 } 93 } 94 } 95 } 96 } 97 inspect.Preorder(nodeFilter, func(n ast.Node) { 98 file := pass.Fset.File(n.Pos()) 99 if strings.HasSuffix(file.Name(), "_test.go") && !includeTestFiles { 100 return 101 } 102 // Skip generated structs if instructed. 103 if !generated && genFiles[file] { 104 return 105 } 106 if f, ok := n.(*ast.File); ok { 107 af = f 108 df, _ = dec.DecorateFile(af) 109 return 110 } 111 atyp := n.(*ast.StructType) 112 styp, ok := pass.TypesInfo.Types[atyp].Type.(*types.Struct) 113 // Type information may be incomplete. 114 if !ok { 115 return 116 } 117 if !verbose && styp.NumFields() < 2 { 118 return 119 } 120 121 r := checkSloppy(pass, styp) 122 if !verbose && !r.sloppy() { 123 return 124 } 125 126 var buf bytes.Buffer 127 expr, err := parser.ParseExpr(formatStruct(r.optStruct, pass.Pkg.Path())) 128 if err != nil { 129 return 130 } 131 if err := format.Node(&buf, token.NewFileSet(), expr.(*ast.StructType)); err != nil { 132 return 133 } 134 135 var msg string 136 switch { 137 case r.oldGcSize == r.newGcSize: 138 msg = fmt.Sprintf("struct has size %d (size class %d)", r.oldGcSize, r.oldRuntimeSize) 139 case r.oldGcSize != r.newGcSize: 140 msg = fmt.Sprintf( 141 "struct has size %d (size class %d), could be %d (size class %d), optimal fields order:\n%s\n", 142 r.oldGcSize, 143 r.oldRuntimeSize, 144 r.newGcSize, 145 r.newRuntimeSize, 146 buf.String(), 147 ) 148 if r.sloppy() { 149 msg = fmt.Sprintf( 150 "struct has size %d (size class %d), could be %d (size class %d), you'll save %.2f%% if you rearrange it to:\n%s\n", 151 r.oldGcSize, 152 r.oldRuntimeSize, 153 r.newGcSize, 154 r.newRuntimeSize, 155 r.savings(), 156 buf.String(), 157 ) 158 } 159 } 160 161 dtyp := dec.Dst.Nodes[atyp].(*dst.StructType) 162 fields := make([]*dst.Field, 0, len(r.optIdx)) 163 dummy := &dst.Field{} 164 for _, f := range dtyp.Fields.List { 165 fields = append(fields, f) 166 if len(f.Names) == 0 { 167 continue 168 } 169 for range f.Names[1:] { 170 fields = append(fields, dummy) 171 } 172 } 173 optFields := make([]*dst.Field, 0, len(r.optIdx)) 174 for _, i := range r.optIdx { 175 f := fields[i] 176 if f == dummy { 177 continue 178 } 179 optFields = append(optFields, f) 180 } 181 dtyp.Fields.List = optFields 182 183 var suggested bytes.Buffer 184 if err := decorator.Fprint(&suggested, df); err != nil { 185 return 186 } 187 pass.Report(analysis.Diagnostic{ 188 Pos: n.Pos(), 189 End: n.End(), 190 Message: msg, 191 SuggestedFixes: nil, 192 }) 193 f := pass.Fset.File(n.Pos()) 194 fileDiags[f.Name()] = suggested.Bytes() 195 }) 196 197 if !apply { 198 return nil, nil 199 } 200 for fn, content := range fileDiags { 201 fi, err := os.Open(fn) 202 if err != nil { 203 _, _ = fmt.Fprintf(os.Stderr, "failed to open file: %v", err) 204 } 205 st, err := fi.Stat() 206 if err != nil { 207 _, _ = fmt.Fprintf(os.Stderr, "failed to get file stat: %v", err) 208 } 209 if err := fi.Close(); err != nil { 210 _, _ = fmt.Fprintf(os.Stderr, "failed to close file: %v", err) 211 } 212 if err := os.WriteFile(fn, content, st.Mode()); err != nil { 213 _, _ = fmt.Fprintf(os.Stderr, "failed to write suggested fix to file: %v", err) 214 } 215 } 216 return nil, nil 217 } 218 219 type result struct { 220 oldGcSize int64 221 newGcSize int64 222 oldRuntimeSize int64 223 newRuntimeSize int64 224 optStruct *types.Struct 225 optIdx []int 226 } 227 228 func (r result) sloppy() bool { 229 return r.oldRuntimeSize > r.newRuntimeSize 230 } 231 232 func (r result) savings() float64 { 233 return float64(r.oldRuntimeSize-r.newRuntimeSize) / float64(r.oldRuntimeSize) * 100 234 } 235 236 func mapFieldIdx(s *types.Struct) map[*types.Var]int { 237 m := make(map[*types.Var]int, s.NumFields()) 238 for i := 0; i < s.NumFields(); i++ { 239 m[s.Field(i)] = i 240 } 241 return m 242 } 243 244 func checkSloppy(pass *analysis.Pass, origStruct *types.Struct) result { 245 m := mapFieldIdx(origStruct) 246 optStruct := optimalStructArrangement(pass.TypesSizes, m) 247 idx := make([]int, optStruct.NumFields()) 248 for i := range idx { 249 idx[i] = m[optStruct.Field(i)] 250 } 251 r := result{ 252 oldGcSize: pass.TypesSizes.Sizeof(origStruct), 253 newGcSize: pass.TypesSizes.Sizeof(optStruct), 254 optStruct: optStruct, 255 optIdx: idx, 256 } 257 r.oldRuntimeSize = int64(roundUpSize(uintptr(r.oldGcSize))) 258 r.newRuntimeSize = int64(roundUpSize(uintptr(r.newGcSize))) 259 return r 260 } 261 262 func optimalStructArrangement(sizes types.Sizes, m map[*types.Var]int) *types.Struct { 263 fields := make([]*types.Var, len(m)) 264 for v, i := range m { 265 fields[i] = v 266 } 267 268 sort.Slice(fields, func(i, j int) bool { 269 ti, tj := fields[i].Type(), fields[j].Type() 270 si, sj := sizes.Sizeof(ti), sizes.Sizeof(tj) 271 272 if si == 0 && sj != 0 { 273 return true 274 } 275 if sj == 0 && si != 0 { 276 return false 277 } 278 279 ai, aj := sizes.Alignof(ti), sizes.Alignof(tj) 280 if ai != aj { 281 return ai > aj 282 } 283 284 if si != sj { 285 return si > sj 286 } 287 288 return false 289 }) 290 291 return types.NewStruct(fields, nil) 292 } 293 294 func formatStruct(styp *types.Struct, curPkgPath string) string { 295 qualifier := func(p *types.Package) string { 296 if p.Path() == curPkgPath { 297 return "" 298 } 299 return p.Name() 300 } 301 return types.TypeString(styp, qualifier) 302 }