Skip to content

Commit f1d5252

Browse files
committed
gopls/internal/golang: Hover: show wasted % of struct space
This change causes Hover to reveal the percentage of a struct type's size that is wasted due to suboptimal field ordering, if >=20%. + test, release note Fixes golang/go#66582 Change-Id: I618f68d8a277eb21c27a320c7a62cca09d8eef0a Reviewed-on: https://go-review.googlesource.com/c/tools/+/575375 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Suzy Mueller <[email protected]>
1 parent 951bb40 commit f1d5252

File tree

3 files changed

+117
-41
lines changed

3 files changed

+117
-41
lines changed

gopls/doc/release/v0.16.0.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ func (s S) set(x int) {
7878

7979
Hovering over the identifier that declares a type or struct field now
8080
displays the size information for the type, and the offset information
81-
for the field. This information may be helpful when making space
81+
for the field. In addition, it reports the percentage of wasted space
82+
due to suboptimal ordering of struct fields, if this figure is 20% or
83+
higher. This information may be helpful when making space
8284
optimizations to your data structures, or when reading assembly code.
8385

8486
TODO: example hover image.

gopls/internal/golang/hover.go

+87-39
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"go/types"
1818
"io/fs"
1919
"path/filepath"
20+
"sort"
2021
"strconv"
2122
"strings"
2223
"text/tabwriter"
@@ -39,6 +40,7 @@ import (
3940
"golang.org/x/tools/internal/aliases"
4041
"golang.org/x/tools/internal/event"
4142
"golang.org/x/tools/internal/tokeninternal"
43+
"golang.org/x/tools/internal/typeparams"
4244
"golang.org/x/tools/internal/typesinternal"
4345
)
4446

@@ -247,6 +249,9 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
247249
// Compute size information for types,
248250
// and (size, offset) for struct fields.
249251
//
252+
// Also, if a struct type's field ordering is significantly
253+
// wasteful of space, report its optimal size.
254+
//
250255
// This information is useful when debugging crashes or
251256
// optimizing layout. To reduce distraction, we show it only
252257
// when hovering over the declaring identifier,
@@ -272,50 +277,24 @@ func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp pro
272277
return fmt.Sprintf("%[1]d (%#[1]x)", x)
273278
}
274279

275-
var data []string // {size, offset}, both optional
276-
277-
// If the type has free type parameters, its size cannot be
278-
// computed. For now, we capture panics from go/types.Sizes.
279-
// TODO(adonovan): use newly factored typeparams.Free.
280-
try := func(f func()) bool {
281-
defer func() { recover() }()
282-
f()
283-
return true
284-
}
280+
path := pathEnclosingObjNode(pgf.File, pos)
285281

286-
// size (types and fields)
287-
if v, ok := obj.(*types.Var); ok && v.IsField() || is[*types.TypeName](obj) {
288-
var sz int64
289-
if try(func() { sz = pkg.TypesSizes().Sizeof(obj.Type()) }) {
290-
data = append(data, "size="+format(sz))
282+
// Build string of form "size=... (X% wasted), offset=...".
283+
size, wasted, offset := computeSizeOffsetInfo(pkg, path, obj)
284+
var buf strings.Builder
285+
if size >= 0 {
286+
fmt.Fprintf(&buf, "size=%s", format(size))
287+
if wasted >= 20 { // >=20% wasted
288+
fmt.Fprintf(&buf, " (%d%% wasted)", wasted)
291289
}
292290
}
293-
294-
// offset (fields)
295-
if v, ok := obj.(*types.Var); ok && v.IsField() {
296-
for _, n := range pathEnclosingObjNode(pgf.File, pos) {
297-
if n, ok := n.(*ast.StructType); ok {
298-
t := pkg.TypesInfo().TypeOf(n).(*types.Struct)
299-
var fields []*types.Var
300-
for i := 0; i < t.NumFields(); i++ {
301-
f := t.Field(i)
302-
fields = append(fields, f)
303-
if f == v {
304-
var offsets []int64
305-
if try(func() { offsets = pkg.TypesSizes().Offsetsof(fields) }) {
306-
if n := len(offsets); n > 0 {
307-
data = append(data, "offset="+format(offsets[n-1]))
308-
}
309-
}
310-
break
311-
}
312-
}
313-
break
314-
}
291+
if offset >= 0 {
292+
if buf.Len() > 0 {
293+
buf.WriteString(", ")
315294
}
295+
fmt.Fprintf(&buf, "offset=%s", format(offset))
316296
}
317-
318-
sizeOffset = strings.Join(data, ", ")
297+
sizeOffset = buf.String()
319298
}
320299

321300
var typeDecl, methods, fields string
@@ -1361,3 +1340,72 @@ func promotedFields(t types.Type, from *types.Package) []promotedField {
13611340
func accessibleTo(obj types.Object, pkg *types.Package) bool {
13621341
return obj.Exported() || obj.Pkg() == pkg
13631342
}
1343+
1344+
// computeSizeOffsetInfo reports the size of obj (if a type or struct
1345+
// field), its wasted space percentage (if a struct type), and its
1346+
// offset (if a struct field). It returns -1 for undefined components.
1347+
func computeSizeOffsetInfo(pkg *cache.Package, path []ast.Node, obj types.Object) (size, wasted, offset int64) {
1348+
size, wasted, offset = -1, -1, -1
1349+
1350+
var free typeparams.Free
1351+
sizes := pkg.TypesSizes()
1352+
1353+
// size (types and fields)
1354+
if v, ok := obj.(*types.Var); ok && v.IsField() || is[*types.TypeName](obj) {
1355+
// If the field's type has free type parameters,
1356+
// its size cannot be computed.
1357+
if !free.Has(obj.Type()) {
1358+
size = sizes.Sizeof(obj.Type())
1359+
}
1360+
1361+
// wasted space (struct types)
1362+
if tStruct, ok := obj.Type().Underlying().(*types.Struct); ok && is[*types.TypeName](obj) && size > 0 {
1363+
var fields []*types.Var
1364+
for i := 0; i < tStruct.NumFields(); i++ {
1365+
fields = append(fields, tStruct.Field(i))
1366+
}
1367+
if len(fields) > 0 {
1368+
// Sort into descending (most compact) order
1369+
// and recompute size of entire struct.
1370+
sort.Slice(fields, func(i, j int) bool {
1371+
return sizes.Sizeof(fields[i].Type()) >
1372+
sizes.Sizeof(fields[j].Type())
1373+
})
1374+
offsets := sizes.Offsetsof(fields)
1375+
compactSize := offsets[len(offsets)-1] + sizes.Sizeof(fields[len(fields)-1].Type())
1376+
wasted = 100 * (size - compactSize) / size
1377+
}
1378+
}
1379+
}
1380+
1381+
// offset (fields)
1382+
if v, ok := obj.(*types.Var); ok && v.IsField() {
1383+
// Find enclosing struct type.
1384+
var tStruct *types.Struct
1385+
for _, n := range path {
1386+
if n, ok := n.(*ast.StructType); ok {
1387+
tStruct = pkg.TypesInfo().TypeOf(n).(*types.Struct)
1388+
break
1389+
}
1390+
}
1391+
if tStruct != nil {
1392+
var fields []*types.Var
1393+
for i := 0; i < tStruct.NumFields(); i++ {
1394+
f := tStruct.Field(i)
1395+
// If any preceding field's type has free type parameters,
1396+
// its offset cannot be computed.
1397+
if free.Has(f.Type()) {
1398+
break
1399+
}
1400+
fields = append(fields, f)
1401+
if f == v {
1402+
offsets := sizes.Offsetsof(fields)
1403+
offset = offsets[len(offsets)-1]
1404+
break
1405+
}
1406+
}
1407+
}
1408+
}
1409+
1410+
return
1411+
}

gopls/internal/test/marker/testdata/hover/sizeoffset.txt

+27-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ Notes:
77
- the offset of a field is undefined if it or any preceding field
88
has undefined size/alignment.
99
- the test's size expectations assumes a 64-bit machine.
10+
- requires go1.22 because size information was inaccurate before.
1011

1112
-- flags --
1213
-skip_goarch=386
14+
-min_go=go1.22
1315

1416
-- go.mod --
1517
module example.com
@@ -18,7 +20,7 @@ go 1.18
1820
-- a.go --
1921
package a
2022

21-
type T struct {
23+
type T struct { //@ hover("T", "T", T)
2224
a int //@ hover("a", "a", a)
2325
U U //@ hover("U", "U", U)
2426
y, z int //@ hover("y", "y", y), hover("z", "z", z)
@@ -38,6 +40,30 @@ var _ struct {
3840
Gstring G[string] //@ hover("Gstring", "Gstring", Gstring)
3941
}
4042

43+
type wasteful struct { //@ hover("wasteful", "wasteful", wasteful)
44+
a bool
45+
b [2]string
46+
c bool
47+
}
48+
49+
-- @T --
50+
```go
51+
type T struct { // size=48 (0x30)
52+
a int //@ hover("a", "a", a)
53+
U U //@ hover("U", "U", U)
54+
y, z int //@ hover("y", "y", y), hover("z", "z", z)
55+
}
56+
```
57+
58+
[`a.T` on pkg.go.dev](https://pkg.go.dev/example.com#T)
59+
-- @wasteful --
60+
```go
61+
type wasteful struct { // size=48 (0x30) (29% wasted)
62+
a bool
63+
b [2]string
64+
c bool
65+
}
66+
```
4167
-- @a --
4268
```go
4369
field a int // size=8, offset=0

0 commit comments

Comments
 (0)