├── .travis.yml ├── LICENSE ├── README ├── bitmap.go ├── bitmap_test.go ├── doc.go ├── go.mod ├── mapiter_go12.go ├── mapiter_old.go ├── memsize.go ├── memsize_test.go ├── memsizedemo └── main.go ├── memsizeui ├── template.go └── ui.go ├── runtimefunc.go ├── runtimefunc.s ├── runtimefunc_go120.go ├── runtimefunc_go121.go ├── type.go └── type_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.10.x" 4 | - "1.12.x" 5 | - "1.x" 6 | env: 7 | - GOARCH=i386 8 | - GOARCH=amd64 9 | - GOARCH=arm64 10 | 11 | script: go test -v ./... 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Felix Lange 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | NOTE: As of Go 1.23, memsize no longer works because of a restriction added by the 2 | Go toolchain. The Go 1.23 compiler no longer allows access to runtime symbols via 3 | go:linkname, which prevents memsize from accessing the Stop-the-World 4 | functionality of the Go runtime. 5 | 6 | If your program depends on memsize, you can disable the restriction when building 7 | your program: 8 | 9 | go build -ldflags=-checklinkname=0 10 | 11 | --- 12 | 13 | For Go API documentation, go to https://pkg.go.dev/github.com/fjl/memsize 14 | 15 | --- 16 | 17 | Package memsize computes the size of your object graph. 18 | 19 | For any Go object, it can compute the amount of memory referenced by the object. 20 | Almost all Go types are supported, except for function pointers. 21 | 22 | To scan a value and print the amount of memory it uses, run 23 | 24 | sizes := memsize.Scan(myValue) 25 | fmt.Println(sizes.Total) 26 | 27 | If your program provides an HTTP server for debugging (e.g. using net/http/pprof), 28 | you can also add an interactive memsize tool there and use it from a 29 | web browser. To do this, add 30 | 31 | import "github.com/fjl/memsize/memsizeui" 32 | 33 | var memsizeH memsizeui.Handler 34 | 35 | and then hook the handler up to your debugging HTTP server. The web 36 | interface will display buttons for added 'roots', which you must register 37 | on the handler: 38 | 39 | memsizeH.Add("myObject", &myObject) 40 | -------------------------------------------------------------------------------- /bitmap.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "math/bits" 5 | ) 6 | 7 | const ( 8 | uintptrBits = 32 << (uint64(^uintptr(0)) >> 63) 9 | uintptrBytes = uintptrBits / 8 10 | bmBlockRange = 1 * 1024 * 1024 // bytes covered by bmBlock 11 | bmBlockWords = bmBlockRange / uintptrBits 12 | ) 13 | 14 | // bitmap is a sparse bitmap. 15 | type bitmap struct { 16 | blocks map[uintptr]*bmBlock 17 | } 18 | 19 | func newBitmap() *bitmap { 20 | return &bitmap{make(map[uintptr]*bmBlock)} 21 | } 22 | 23 | // markRange sets n consecutive bits starting at addr. 24 | func (b *bitmap) markRange(addr, n uintptr) { 25 | for end := addr + n; addr < end; { 26 | block, baddr := b.block(addr) 27 | for i := baddr; i < bmBlockRange && addr < end; i++ { 28 | block.mark(i) 29 | addr++ 30 | } 31 | } 32 | } 33 | 34 | // isMarked returns the value of the bit at the given address. 35 | func (b *bitmap) isMarked(addr uintptr) bool { 36 | block, baddr := b.block(addr) 37 | return block.isMarked(baddr) 38 | } 39 | 40 | // countRange returns the number of set bits in the range [addr, addr+n]. 41 | func (b *bitmap) countRange(addr, n uintptr) uintptr { 42 | c := uintptr(0) 43 | for end := addr + n; addr < end; { 44 | block, baddr := b.block(addr) 45 | bend := uintptr(bmBlockRange - 1) 46 | if baddr+(end-addr) < bmBlockRange { 47 | bend = baddr + (end - addr) 48 | } 49 | c += uintptr(block.count(baddr, bend)) 50 | // Move addr to next block. 51 | addr += bmBlockRange - baddr 52 | } 53 | return c 54 | } 55 | 56 | // block finds the block corresponding to the given memory address. 57 | // It also returns the block's starting address. 58 | func (b *bitmap) block(addr uintptr) (*bmBlock, uintptr) { 59 | index := addr / bmBlockRange 60 | block := b.blocks[index] 61 | if block == nil { 62 | block = new(bmBlock) 63 | b.blocks[index] = block 64 | } 65 | return block, addr % bmBlockRange 66 | } 67 | 68 | // size returns the sum of the byte sizes of all blocks. 69 | func (b *bitmap) size() uintptr { 70 | return uintptr(len(b.blocks)) * bmBlockWords * uintptrBytes 71 | } 72 | 73 | // utilization returns the mean percentage of one bits across all blocks. 74 | func (b *bitmap) utilization() float32 { 75 | var avg float32 76 | for _, block := range b.blocks { 77 | avg += float32(block.count(0, bmBlockRange-1)) / float32(bmBlockRange) 78 | } 79 | return avg / float32(len(b.blocks)) 80 | } 81 | 82 | // bmBlock is a bitmap block. 83 | type bmBlock [bmBlockWords]uintptr 84 | 85 | // mark sets the i'th bit to one. 86 | func (b *bmBlock) mark(i uintptr) { 87 | b[i/uintptrBits] |= 1 << (i % uintptrBits) 88 | } 89 | 90 | // isMarked returns the value of the i'th bit. 91 | func (b *bmBlock) isMarked(i uintptr) bool { 92 | return (b[i/uintptrBits] & (1 << (i % uintptrBits))) != 0 93 | } 94 | 95 | // count returns the number of set bits in the range [start, end]. 96 | func (b *bmBlock) count(start, end uintptr) (count int) { 97 | br := b[start/uintptrBits : end/uintptrBits+1] 98 | for i, w := range br { 99 | if i == 0 { 100 | w &= blockmask(start) 101 | } 102 | if i == len(br)-1 { 103 | w &^= blockmask(end) 104 | } 105 | count += onesCountPtr(w) 106 | } 107 | return count 108 | } 109 | 110 | func blockmask(x uintptr) uintptr { 111 | return ^uintptr(0) << (x % uintptrBits) 112 | } 113 | 114 | func onesCountPtr(x uintptr) int { 115 | if uintptrBits == 64 { 116 | return bits.OnesCount64(uint64(x)) 117 | } 118 | return bits.OnesCount32(uint32(x)) 119 | } 120 | -------------------------------------------------------------------------------- /bitmap_test.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "testing" 7 | ) 8 | 9 | func TestBitmapBlock(t *testing.T) { 10 | marks := map[uintptr]bool{ 11 | 10: true, 12 | 13: true, 13 | 44: true, 14 | 128: true, 15 | 129: true, 16 | 256: true, 17 | 700: true, 18 | } 19 | var b bmBlock 20 | for i := range marks { 21 | b.mark(i) 22 | } 23 | for i := uintptr(0); i < bmBlockRange; i++ { 24 | if b.isMarked(i) && !marks[i] { 25 | t.Fatalf("wrong mark at %d", i) 26 | } 27 | } 28 | if count := b.count(0, bmBlockRange-1); count != len(marks) { 29 | t.Fatalf("wrong onesCount: got %d, want %d", count, len(marks)) 30 | } 31 | } 32 | 33 | func TestBitmapBlockCount(t *testing.T) { 34 | var b bmBlock 35 | // Mark addresses (90,250) 36 | for i := 90; i < 250; i++ { 37 | b.mark(uintptr(i)) 38 | } 39 | // Check counts. 40 | tests := []struct { 41 | start, end uintptr 42 | want int 43 | }{ 44 | {start: 0, end: 0, want: 0}, 45 | {start: 0, end: 10, want: 0}, 46 | {start: 0, end: 250, want: 160}, 47 | {start: 0, end: 240, want: 150}, 48 | {start: 0, end: bmBlockRange - 1, want: 160}, 49 | {start: 100, end: bmBlockRange - 1, want: 150}, 50 | {start: 100, end: 110, want: 10}, 51 | {start: 100, end: 250, want: 150}, 52 | {start: 100, end: 211, want: 111}, 53 | {start: 111, end: 211, want: 100}, 54 | } 55 | for _, test := range tests { 56 | t.Run(fmt.Sprintf("%d-%d", test.start, test.end), func(t *testing.T) { 57 | if c := b.count(test.start, test.end); c != test.want { 58 | t.Errorf("wrong onesCountRange(%d, %d): got %d, want %d", test.start, test.end, c, test.want) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | func TestBitmapMarkRange(t *testing.T) { 65 | N := 1000 66 | 67 | // Generate random non-overlapping mark ranges. 68 | var ( 69 | r = rand.New(rand.NewSource(312321312)) 70 | bm = newBitmap() 71 | ranges = make(map[uintptr]uintptr) 72 | addr uintptr 73 | total uintptr // number of bytes marked 74 | ) 75 | for i := 0; i < N; i++ { 76 | addr += uintptr(r.Intn(bmBlockRange)) 77 | len := uintptr(r.Intn(40)) 78 | total += len 79 | ranges[addr] = len 80 | bm.markRange(addr, len) 81 | } 82 | 83 | // Check all marks are set. 84 | for start, len := range ranges { 85 | for i := uintptr(0); i < len; i++ { 86 | if !bm.isMarked(start + i) { 87 | t.Fatalf("not marked at %d", start) 88 | } 89 | } 90 | } 91 | 92 | // Check total number of bits is reported correctly. 93 | if c := bm.countRange(0, addr+ranges[addr]); c != total { 94 | t.Errorf("countRange(0, %d) returned %d, want %d", addr, c, total) 95 | } 96 | 97 | // Probe random addresses. 98 | for i := 0; i < N; i++ { 99 | addr := uintptr(r.Uint64()) 100 | marked := false 101 | for start, len := range ranges { 102 | if addr >= start && addr < start+len { 103 | marked = true 104 | break 105 | } 106 | } 107 | if bm.isMarked(addr) && !marked { 108 | t.Fatalf("extra mark at %d", addr) 109 | } 110 | } 111 | } 112 | 113 | func BenchmarkBitmapMarkRange(b *testing.B) { 114 | var addrs [2048]uintptr 115 | r := rand.New(rand.NewSource(423098209802)) 116 | for i := range addrs { 117 | addrs[i] = uintptr(r.Uint64()) 118 | } 119 | 120 | doit := func(b *testing.B, rlen int) { 121 | bm := newBitmap() 122 | for i := 0; i < b.N; i++ { 123 | addr := addrs[i%len(addrs)] 124 | bm.markRange(addr, uintptr(rlen)) 125 | } 126 | } 127 | for rlen := 1; rlen <= 4096; rlen *= 8 { 128 | b.Run(fmt.Sprintf("%d", rlen), func(b *testing.B) { doit(b, rlen) }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package memsize computes the size of your object graph. 3 | 4 | So you made a spiffy algorithm and it works really well, but geez it's using 5 | way too much memory. Where did it all go? memsize to the rescue! 6 | 7 | To get started, find a value that references all your objects and scan it. 8 | This traverses the graph, counting sizes per type. 9 | 10 | sizes := memsize.Scan(myValue) 11 | fmt.Println(sizes.Total) 12 | 13 | memsize can handle cycles just fine and tracks both private and public struct fields. 14 | Unfortunately function closures cannot be inspected in any way. 15 | */ 16 | package memsize 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fjl/memsize 2 | -------------------------------------------------------------------------------- /mapiter_go12.go: -------------------------------------------------------------------------------- 1 | // +build go1.12 2 | 3 | package memsize 4 | 5 | import "reflect" 6 | 7 | func iterateMap(m reflect.Value, fn func(k, v reflect.Value)) { 8 | it := m.MapRange() 9 | for it.Next() { 10 | fn(it.Key(), it.Value()) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mapiter_old.go: -------------------------------------------------------------------------------- 1 | // +build !go1.12 2 | 3 | package memsize 4 | 5 | import "reflect" 6 | 7 | func iterateMap(m reflect.Value, fn func(k, v reflect.Value)) { 8 | for _, k := range m.MapKeys() { 9 | fn(k, m.MapIndex(k)) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /memsize.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "reflect" 7 | "sort" 8 | "strings" 9 | "text/tabwriter" 10 | "unsafe" 11 | ) 12 | 13 | // Scan traverses all objects reachable from v and counts how much memory 14 | // is used per type. The value must be a non-nil pointer to any value. 15 | func Scan(v interface{}) Sizes { 16 | rv := reflect.ValueOf(v) 17 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 18 | panic("value to scan must be non-nil pointer") 19 | } 20 | 21 | stopTheWorld(stwReadMemStats) 22 | defer startTheWorld() 23 | 24 | ctx := newContext() 25 | ctx.scan(invalidAddr, rv, false) 26 | ctx.s.BitmapSize = ctx.seen.size() 27 | ctx.s.BitmapUtilization = ctx.seen.utilization() 28 | return *ctx.s 29 | } 30 | 31 | // Sizes is the result of a scan. 32 | type Sizes struct { 33 | Total uintptr 34 | ByType map[reflect.Type]*TypeSize 35 | // Internal stats (for debugging) 36 | BitmapSize uintptr 37 | BitmapUtilization float32 38 | } 39 | 40 | type TypeSize struct { 41 | Total uintptr 42 | Count uintptr 43 | } 44 | 45 | func newSizes() *Sizes { 46 | return &Sizes{ByType: make(map[reflect.Type]*TypeSize)} 47 | } 48 | 49 | // Report returns a human-readable report. 50 | func (s Sizes) Report() string { 51 | type typLine struct { 52 | name string 53 | count uintptr 54 | total uintptr 55 | } 56 | tab := []typLine{{"ALL", 0, s.Total}} 57 | for _, typ := range s.ByType { 58 | tab[0].count += typ.Count 59 | } 60 | maxname := 0 61 | for typ, s := range s.ByType { 62 | line := typLine{typ.String(), s.Count, s.Total} 63 | tab = append(tab, line) 64 | if len(line.name) > maxname { 65 | maxname = len(line.name) 66 | } 67 | } 68 | sort.Slice(tab, func(i, j int) bool { return tab[i].total > tab[j].total }) 69 | 70 | buf := new(bytes.Buffer) 71 | w := tabwriter.NewWriter(buf, 0, 0, 0, ' ', tabwriter.AlignRight) 72 | for _, line := range tab { 73 | namespace := strings.Repeat(" ", maxname-len(line.name)) 74 | fmt.Fprintf(w, "%s%s\t %v\t %s\t\n", line.name, namespace, line.count, HumanSize(line.total)) 75 | } 76 | w.Flush() 77 | return buf.String() 78 | } 79 | 80 | // addValue is called during scan and adds the memory of given object. 81 | func (s *Sizes) addValue(v reflect.Value, size uintptr) { 82 | s.Total += size 83 | rs := s.ByType[v.Type()] 84 | if rs == nil { 85 | rs = new(TypeSize) 86 | s.ByType[v.Type()] = rs 87 | } 88 | rs.Total += size 89 | rs.Count++ 90 | } 91 | 92 | type context struct { 93 | // We track previously scanned objects to prevent infinite loops 94 | // when scanning cycles and to prevent counting objects more than once. 95 | seen *bitmap 96 | tc typCache 97 | s *Sizes 98 | } 99 | 100 | func newContext() *context { 101 | return &context{seen: newBitmap(), tc: make(typCache), s: newSizes()} 102 | } 103 | 104 | // scan walks all objects below v, determining their size. It returns the size of the 105 | // previously unscanned parts of the object. 106 | func (c *context) scan(addr address, v reflect.Value, add bool) (extraSize uintptr) { 107 | size := v.Type().Size() 108 | var marked uintptr 109 | if addr.valid() { 110 | marked = c.seen.countRange(uintptr(addr), size) 111 | if marked == size { 112 | return 0 // Skip if we have already seen the whole object. 113 | } 114 | c.seen.markRange(uintptr(addr), size) 115 | } 116 | // fmt.Printf("%v: %v ⮑ (marked %d)\n", addr, v.Type(), marked) 117 | if c.tc.needScan(v.Type()) { 118 | extraSize = c.scanContent(addr, v) 119 | } 120 | size -= marked 121 | size += extraSize 122 | // fmt.Printf("%v: %v %d (add %v, size %d, marked %d, extra %d)\n", addr, v.Type(), size+extraSize, add, v.Type().Size(), marked, extraSize) 123 | if add { 124 | c.s.addValue(v, size) 125 | } 126 | return size 127 | } 128 | 129 | // scanContent and all other scan* functions below return the amount of 'extra' memory 130 | // (e.g. slice data) that is referenced by the object. 131 | func (c *context) scanContent(addr address, v reflect.Value) uintptr { 132 | switch v.Kind() { 133 | case reflect.Array: 134 | return c.scanArray(addr, v) 135 | case reflect.Chan: 136 | return c.scanChan(v) 137 | case reflect.Func: 138 | // can't do anything here 139 | return 0 140 | case reflect.Interface: 141 | return c.scanInterface(v) 142 | case reflect.Map: 143 | return c.scanMap(v) 144 | case reflect.Ptr: 145 | if !v.IsNil() { 146 | c.scan(address(v.Pointer()), v.Elem(), true) 147 | } 148 | return 0 149 | case reflect.Slice: 150 | return c.scanSlice(v) 151 | case reflect.String: 152 | return uintptr(v.Len()) 153 | case reflect.Struct: 154 | return c.scanStruct(addr, v) 155 | default: 156 | unhandledKind(v.Kind()) 157 | return 0 158 | } 159 | } 160 | 161 | func (c *context) scanChan(v reflect.Value) uintptr { 162 | etyp := v.Type().Elem() 163 | extra := uintptr(0) 164 | if c.tc.needScan(etyp) { 165 | // Scan the channel buffer. This is unsafe but doesn't race because 166 | // the world is stopped during scan. 167 | hchan := unsafe.Pointer(v.Pointer()) 168 | for i := uint(0); i < uint(v.Cap()); i++ { 169 | addr := chanbuf(hchan, i) 170 | elem := reflect.NewAt(etyp, addr).Elem() 171 | extra += c.scanContent(address(addr), elem) 172 | } 173 | } 174 | return uintptr(v.Cap())*etyp.Size() + extra 175 | } 176 | 177 | func (c *context) scanStruct(base address, v reflect.Value) uintptr { 178 | extra := uintptr(0) 179 | for i := 0; i < v.NumField(); i++ { 180 | f := v.Type().Field(i) 181 | if c.tc.needScan(f.Type) { 182 | addr := base.addOffset(f.Offset) 183 | extra += c.scanContent(addr, v.Field(i)) 184 | } 185 | } 186 | return extra 187 | } 188 | 189 | func (c *context) scanArray(addr address, v reflect.Value) uintptr { 190 | esize := v.Type().Elem().Size() 191 | extra := uintptr(0) 192 | for i := 0; i < v.Len(); i++ { 193 | extra += c.scanContent(addr, v.Index(i)) 194 | addr = addr.addOffset(esize) 195 | } 196 | return extra 197 | } 198 | 199 | func (c *context) scanSlice(v reflect.Value) uintptr { 200 | slice := v.Slice(0, v.Cap()) 201 | esize := slice.Type().Elem().Size() 202 | base := slice.Pointer() 203 | // Add size of the unscanned portion of the backing array to extra. 204 | blen := uintptr(slice.Len()) * esize 205 | marked := c.seen.countRange(base, blen) 206 | extra := blen - marked 207 | c.seen.markRange(uintptr(base), blen) 208 | if c.tc.needScan(slice.Type().Elem()) { 209 | // Elements may contain pointers, scan them individually. 210 | addr := address(base) 211 | for i := 0; i < slice.Len(); i++ { 212 | extra += c.scanContent(addr, slice.Index(i)) 213 | addr = addr.addOffset(esize) 214 | } 215 | } 216 | return extra 217 | } 218 | 219 | func (c *context) scanMap(v reflect.Value) uintptr { 220 | var ( 221 | typ = v.Type() 222 | len = uintptr(v.Len()) 223 | extra = uintptr(0) 224 | ) 225 | if c.tc.needScan(typ.Key()) || c.tc.needScan(typ.Elem()) { 226 | iterateMap(v, func(k, v reflect.Value) { 227 | extra += c.scan(invalidAddr, k, false) 228 | extra += c.scan(invalidAddr, v, false) 229 | }) 230 | } else { 231 | extra = len*typ.Key().Size() + len*typ.Elem().Size() 232 | } 233 | return extra 234 | } 235 | 236 | func (c *context) scanInterface(v reflect.Value) uintptr { 237 | elem := v.Elem() 238 | if !elem.IsValid() { 239 | return 0 // nil interface 240 | } 241 | extra := c.scan(invalidAddr, elem, false) 242 | if elem.Type().Kind() == reflect.Ptr { 243 | extra -= uintptrBytes 244 | } 245 | return extra 246 | } 247 | -------------------------------------------------------------------------------- /memsize_test.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "testing" 5 | "unsafe" 6 | ) 7 | 8 | const ( 9 | sizeofSlice = unsafe.Sizeof([]byte{}) 10 | sizeofMap = unsafe.Sizeof(map[string]string{}) 11 | sizeofInterface = unsafe.Sizeof((interface{})(nil)) 12 | sizeofString = unsafe.Sizeof("") 13 | sizeofWord = unsafe.Sizeof(uintptr(0)) 14 | sizeofChan = unsafe.Sizeof(make(chan struct{})) 15 | ) 16 | 17 | type ( 18 | struct16 struct { 19 | x, y uint64 20 | } 21 | structptr struct { 22 | x uint32 23 | cld *structptr 24 | } 25 | structuint32ptr struct { 26 | x *uint32 27 | } 28 | structmultiptr struct { 29 | s1 *structptr 30 | u1 *structuint32ptr 31 | s2 *structptr 32 | u2 *structuint32ptr 33 | s3 *structptr 34 | u3 *structuint32ptr 35 | } 36 | structarrayptr struct { 37 | x *uint64 38 | a [10]uint64 39 | } 40 | structiface struct { 41 | s *struct16 42 | x interface{} 43 | } 44 | struct64array struct{ array64 } 45 | structslice struct{ s []uint32 } 46 | structstring struct{ s string } 47 | structloop struct{ s *structloop } 48 | structptrslice struct{ s *structslice } 49 | array64 [64]byte 50 | ) 51 | 52 | func TestTotal(t *testing.T) { 53 | tests := []struct { 54 | name string 55 | v interface{} 56 | want uintptr 57 | }{ 58 | { 59 | name: "struct16", 60 | v: &struct16{}, 61 | want: 16, 62 | }, 63 | { 64 | name: "structptr_nil", 65 | v: &structptr{}, 66 | want: 2 * sizeofWord, 67 | }, 68 | { 69 | name: "structptr", 70 | v: &structptr{cld: &structptr{}}, 71 | want: 2 * 2 * sizeofWord, 72 | }, 73 | { 74 | name: "structptr_loop", 75 | v: func() *structptr { 76 | v := &structptr{} 77 | v.cld = v 78 | return v 79 | }(), 80 | want: 2 * sizeofWord, 81 | }, 82 | { 83 | name: "structmultiptr_loop", 84 | v: func() *structmultiptr { 85 | v1 := &structptr{x: 1} 86 | v2 := &structptr{x: 2, cld: v1} 87 | return &structmultiptr{s1: v1, s2: v1, s3: v2} 88 | }(), 89 | want: 6*sizeofWord /* structmultiptr */ + 2*2*sizeofWord, /* structptr */ 90 | }, 91 | { 92 | name: "structmultiptr_interior", 93 | v: func() *structmultiptr { 94 | v1 := &structptr{x: 1} 95 | v2 := &structptr{x: 2} 96 | return &structmultiptr{ 97 | // s1 is scanned before u1, which has a reference to a field of s1. 98 | s1: v1, 99 | u1: &structuint32ptr{x: &v1.x}, 100 | // This one goes the other way around: u2, which has a reference to a 101 | // field of s3 is scanned before s3. 102 | u2: &structuint32ptr{x: &v2.x}, 103 | s3: v2, 104 | } 105 | }(), 106 | want: 6*sizeofWord /* structmultiptr */ + 2*2*sizeofWord /* structptr */ + 2*sizeofWord, /* structuint32ptr */ 107 | }, 108 | { 109 | name: "struct64array", 110 | v: &struct64array{}, 111 | want: 64, 112 | }, 113 | { 114 | name: "structptrslice", 115 | v: &structptrslice{&structslice{s: []uint32{1, 2, 3}}}, 116 | want: sizeofWord + sizeofSlice + 3*4, 117 | }, 118 | { 119 | name: "array_unadressable", 120 | v: func() *map[[3]uint64]struct{} { 121 | v := map[[3]uint64]struct{}{ 122 | {1, 2, 3}: struct{}{}, 123 | } 124 | return &v 125 | }(), 126 | want: sizeofMap + 3*8, 127 | }, 128 | { 129 | name: "structslice", 130 | v: &structslice{s: []uint32{1, 2, 3}}, 131 | want: sizeofSlice + 3*4, 132 | }, 133 | { 134 | name: "structloop", 135 | v: func() *structloop { 136 | v := new(structloop) 137 | v.s = v 138 | return v 139 | }(), 140 | want: sizeofWord, 141 | }, 142 | { 143 | name: "array64", 144 | v: &array64{}, 145 | want: 64, 146 | }, 147 | { 148 | name: "byteslice", 149 | v: &[]byte{1, 2, 3}, 150 | want: sizeofSlice + 3, 151 | }, 152 | { 153 | name: "slice3_ptrval", 154 | v: &[]*struct16{{}, {}, {}}, 155 | want: sizeofSlice + 3*sizeofWord + 3*16, 156 | }, 157 | { 158 | name: "map0", 159 | v: &map[uint64]uint64{}, 160 | want: sizeofMap, 161 | }, 162 | { 163 | name: "map3", 164 | v: &map[uint64]uint64{1: 1, 2: 2, 3: 3}, 165 | want: sizeofMap + 3*8 /* keys */ + 3*8, /* values */ 166 | }, 167 | { 168 | name: "map3_ptrval", 169 | v: &map[uint64]*struct16{1: {}, 2: {}, 3: {}}, 170 | want: sizeofMap + 3*8 /* keys */ + 3*sizeofWord /* value pointers */ + 3*16, /* values */ 171 | }, 172 | { 173 | name: "map3_ptrkey", 174 | v: &map[*struct16]uint64{{x: 1}: 1, {x: 2}: 2, {x: 3}: 3}, 175 | want: sizeofMap + 3*sizeofWord /* key pointers */ + 3*16 /* keys */ + 3*8, /* values */ 176 | }, 177 | { 178 | name: "map_interface", 179 | v: &map[interface{}]interface{}{"aa": uint64(1)}, 180 | want: sizeofMap + sizeofInterface + sizeofString + 2 /* key */ + sizeofInterface + 8, /* value */ 181 | }, 182 | { 183 | name: "pointerpointer", 184 | v: func() **uint64 { 185 | i := uint64(0) 186 | p := &i 187 | return &p 188 | }(), 189 | want: sizeofWord + 8, 190 | }, 191 | { 192 | name: "structstring", 193 | v: &structstring{"123"}, 194 | want: sizeofString + 3, 195 | }, 196 | { 197 | name: "slices_samearray", 198 | v: func() *[3][]byte { 199 | backarray := [64]byte{} 200 | return &[3][]byte{ 201 | backarray[16:], 202 | backarray[4:16], 203 | backarray[0:4], 204 | } 205 | }(), 206 | want: 3*sizeofSlice + 64, 207 | }, 208 | { 209 | name: "slices_nil", 210 | v: func() *[2][]byte { 211 | return &[2][]byte{nil, nil} 212 | }(), 213 | want: 2 * sizeofSlice, 214 | }, 215 | { 216 | name: "slices_overlap_total", 217 | v: func() *[2][]byte { 218 | backarray := [32]byte{} 219 | return &[2][]byte{backarray[:], backarray[:]} 220 | }(), 221 | want: 2*sizeofSlice + 32, 222 | }, 223 | { 224 | name: "slices_overlap", 225 | v: func() *[4][]uint16 { 226 | backarray := [32]uint16{} 227 | return &[4][]uint16{ 228 | backarray[2:4], 229 | backarray[10:12], 230 | backarray[20:25], 231 | backarray[:], 232 | } 233 | }(), 234 | want: 4*sizeofSlice + 32*2, 235 | }, 236 | { 237 | name: "slices_overlap_array", 238 | v: func() *struct { 239 | a [32]byte 240 | s [2][]byte 241 | } { 242 | v := struct { 243 | a [32]byte 244 | s [2][]byte 245 | }{} 246 | v.s[0] = v.a[2:4] 247 | v.s[1] = v.a[5:8] 248 | return &v 249 | }(), 250 | want: 32 + 2*sizeofSlice, 251 | }, 252 | { 253 | name: "interface", 254 | v: &[2]interface{}{uint64(0), &struct16{}}, 255 | want: 2*sizeofInterface + 8 + 16, 256 | }, 257 | { 258 | name: "interface_nil", 259 | v: &[2]interface{}{nil, nil}, 260 | want: 2 * sizeofInterface, 261 | }, 262 | { 263 | name: "structiface_slice", 264 | v: &structiface{x: make([]byte, 10)}, 265 | want: sizeofWord + sizeofInterface + sizeofSlice + 10, 266 | }, 267 | { 268 | name: "structiface_pointer", 269 | v: func() *structiface { 270 | s := &struct16{1, 2} 271 | return &structiface{s: s, x: &s.x} 272 | }(), 273 | want: sizeofWord + 16 + sizeofInterface, 274 | }, 275 | { 276 | name: "empty_chan", 277 | v: func() *chan uint64 { 278 | c := make(chan uint64) 279 | return &c 280 | }(), 281 | want: sizeofChan, 282 | }, 283 | { 284 | name: "empty_closed_chan", 285 | v: func() *chan uint64 { 286 | c := make(chan uint64) 287 | close(c) 288 | return &c 289 | }(), 290 | want: sizeofChan, 291 | }, 292 | { 293 | name: "empty_chan_buffer", 294 | v: func() *chan uint64 { 295 | c := make(chan uint64, 10) 296 | return &c 297 | }(), 298 | want: sizeofChan + 10*8, 299 | }, 300 | { 301 | name: "chan_buffer", 302 | v: func() *chan uint64 { 303 | c := make(chan uint64, 10) 304 | for i := 0; i < 8; i++ { 305 | c <- 0 306 | } 307 | return &c 308 | }(), 309 | want: sizeofChan + 10*8, 310 | }, 311 | { 312 | name: "closed_chan_buffer", 313 | v: func() *chan uint64 { 314 | c := make(chan uint64, 10) 315 | for i := 0; i < 8; i++ { 316 | c <- 0 317 | } 318 | close(c) 319 | return &c 320 | }(), 321 | want: sizeofChan + 10*8, 322 | }, 323 | { 324 | name: "chan_buffer_escan", 325 | v: func() *chan *struct16 { 326 | c := make(chan *struct16, 10) 327 | for i := 0; i < 8; i++ { 328 | c <- &struct16{x: uint64(i)} 329 | } 330 | return &c 331 | }(), 332 | want: sizeofChan + 10*sizeofWord + 8*16, 333 | }, 334 | { 335 | name: "closed_chan_buffer_escan", 336 | v: func() *chan *struct16 { 337 | c := make(chan *struct16, 10) 338 | for i := 0; i < 8; i++ { 339 | c <- &struct16{x: uint64(i)} 340 | } 341 | close(c) 342 | return &c 343 | }(), 344 | want: sizeofChan + 10*sizeofWord + 8*16, 345 | }, 346 | { 347 | name: "nil_chan", 348 | v: func() *chan *struct16 { 349 | var c chan *struct16 350 | return &c 351 | }(), 352 | want: sizeofChan, 353 | }, 354 | } 355 | for _, test := range tests { 356 | t.Run(test.name, func(t *testing.T) { 357 | size := Scan(test.v) 358 | if size.Total != test.want { 359 | t.Errorf("total=%d, want %d", size.Total, test.want) 360 | t.Logf("\n%s", size.Report()) 361 | } 362 | }) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /memsizedemo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/fjl/memsize/memsizeui" 8 | ) 9 | 10 | func main() { 11 | byteslice := make([]byte, 200) 12 | intslice := make([]int, 100) 13 | 14 | h := new(memsizeui.Handler) 15 | s := &http.Server{Addr: "127.0.0.1:8080", Handler: h} 16 | h.Add("byteslice", &byteslice) 17 | h.Add("intslice", &intslice) 18 | h.Add("server", s) 19 | log.Println("listening on http://127.0.0.1:8080") 20 | s.ListenAndServe() 21 | } 22 | -------------------------------------------------------------------------------- /memsizeui/template.go: -------------------------------------------------------------------------------- 1 | package memsizeui 2 | 3 | import ( 4 | "html/template" 5 | "strconv" 6 | "sync" 7 | 8 | "github.com/fjl/memsize" 9 | ) 10 | 11 | var ( 12 | base *template.Template // the "base" template 13 | baseInitOnce sync.Once 14 | ) 15 | 16 | func baseInit() { 17 | base = template.Must(template.New("base").Parse(` 18 | 19 | 20 | 21 | memsize 22 | 42 | 43 | 44 | {{template "content" .}} 45 | 46 | `)) 47 | 48 | base.Funcs(template.FuncMap{ 49 | "quote": strconv.Quote, 50 | "humansize": memsize.HumanSize, 51 | }) 52 | 53 | template.Must(base.New("rootbuttons").Parse(` 54 | Overview 55 | {{- range $root := .Roots -}} 56 |
57 | 58 |
59 | {{- end -}}`)) 60 | } 61 | 62 | func contentTemplate(source string) *template.Template { 63 | baseInitOnce.Do(baseInit) 64 | t := template.Must(base.Clone()) 65 | template.Must(t.New("content").Parse(source)) 66 | return t 67 | } 68 | 69 | var rootTemplate = contentTemplate(` 70 |

Memsize

71 | {{template "rootbuttons" .}} 72 |
73 |

Reports

74 | 81 | `) 82 | 83 | var notFoundTemplate = contentTemplate(` 84 |

{{.Data}}

85 | {{template "rootbuttons" .}} 86 | `) 87 | 88 | var reportTemplate = contentTemplate(` 89 | {{- $report := .Data -}} 90 |

Memsize Report {{$report.ID}}

91 |
92 | Overview 93 | 94 |
95 |
 96 | Root: {{quote $report.RootName}}
 97 | Date: {{$report.Date}}
 98 | Duration: {{$report.Duration}}
 99 | Bitmap Size: {{$report.Sizes.BitmapSize | humansize}}
100 | Bitmap Utilization: {{$report.Sizes.BitmapUtilization}}
101 | 
102 |
103 |
104 | {{$report.Sizes.Report}}
105 | 
106 | `) 107 | -------------------------------------------------------------------------------- /memsizeui/ui.go: -------------------------------------------------------------------------------- 1 | package memsizeui 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | "reflect" 9 | "sort" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/fjl/memsize" 15 | ) 16 | 17 | type Handler struct { 18 | init sync.Once 19 | mux http.ServeMux 20 | mu sync.Mutex 21 | reports map[int]Report 22 | roots map[string]interface{} 23 | reportID int 24 | } 25 | 26 | type Report struct { 27 | ID int 28 | Date time.Time 29 | Duration time.Duration 30 | RootName string 31 | Sizes memsize.Sizes 32 | } 33 | 34 | type templateInfo struct { 35 | Roots []string 36 | Reports map[int]Report 37 | PathDepth int 38 | Data interface{} 39 | } 40 | 41 | func (ti *templateInfo) Link(path ...string) string { 42 | prefix := strings.Repeat("../", ti.PathDepth) 43 | return prefix + strings.Join(path, "") 44 | } 45 | 46 | func (h *Handler) Add(name string, v interface{}) { 47 | rv := reflect.ValueOf(v) 48 | if rv.Kind() != reflect.Ptr || rv.IsNil() { 49 | panic("root must be non-nil pointer") 50 | } 51 | h.mu.Lock() 52 | if h.roots == nil { 53 | h.roots = make(map[string]interface{}) 54 | } 55 | h.roots[name] = v 56 | h.mu.Unlock() 57 | } 58 | 59 | func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 60 | h.init.Do(func() { 61 | h.reports = make(map[int]Report) 62 | h.mux.HandleFunc("/", h.handleRoot) 63 | h.mux.HandleFunc("/scan", h.handleScan) 64 | h.mux.HandleFunc("/report/", h.handleReport) 65 | }) 66 | h.mux.ServeHTTP(w, r) 67 | } 68 | 69 | func (h *Handler) templateInfo(r *http.Request, data interface{}) *templateInfo { 70 | h.mu.Lock() 71 | roots := make([]string, 0, len(h.roots)) 72 | for name := range h.roots { 73 | roots = append(roots, name) 74 | } 75 | h.mu.Unlock() 76 | sort.Strings(roots) 77 | 78 | return &templateInfo{ 79 | Roots: roots, 80 | Reports: h.reports, 81 | PathDepth: strings.Count(r.URL.Path, "/") - 1, 82 | Data: data, 83 | } 84 | } 85 | 86 | func (h *Handler) handleRoot(w http.ResponseWriter, r *http.Request) { 87 | if r.URL.Path != "/" { 88 | http.NotFound(w, r) 89 | return 90 | } 91 | serveHTML(w, rootTemplate, http.StatusOK, h.templateInfo(r, nil)) 92 | } 93 | 94 | func (h *Handler) handleScan(w http.ResponseWriter, r *http.Request) { 95 | if r.Method != http.MethodPost { 96 | http.Error(w, "invalid HTTP method, want POST", http.StatusMethodNotAllowed) 97 | return 98 | } 99 | ti := h.templateInfo(r, "Unknown root") 100 | id, ok := h.scan(r.URL.Query().Get("root")) 101 | if !ok { 102 | serveHTML(w, notFoundTemplate, http.StatusNotFound, ti) 103 | return 104 | } 105 | w.Header().Add("Location", ti.Link(fmt.Sprintf("report/%d", id))) 106 | w.WriteHeader(http.StatusSeeOther) 107 | } 108 | 109 | func (h *Handler) handleReport(w http.ResponseWriter, r *http.Request) { 110 | var id int 111 | fmt.Sscan(strings.TrimPrefix(r.URL.Path, "/report/"), &id) 112 | h.mu.Lock() 113 | report, ok := h.reports[id] 114 | h.mu.Unlock() 115 | 116 | if !ok { 117 | serveHTML(w, notFoundTemplate, http.StatusNotFound, h.templateInfo(r, "Report not found")) 118 | } else { 119 | serveHTML(w, reportTemplate, http.StatusOK, h.templateInfo(r, report)) 120 | } 121 | } 122 | 123 | func (h *Handler) scan(root string) (int, bool) { 124 | h.mu.Lock() 125 | defer h.mu.Unlock() 126 | 127 | val, ok := h.roots[root] 128 | if !ok { 129 | return 0, false 130 | } 131 | id := h.reportID 132 | start := time.Now() 133 | sizes := memsize.Scan(val) 134 | h.reports[id] = Report{ 135 | ID: id, 136 | RootName: root, 137 | Date: start.Truncate(1 * time.Second), 138 | Duration: time.Since(start), 139 | Sizes: sizes, 140 | } 141 | h.reportID++ 142 | return id, true 143 | } 144 | 145 | func serveHTML(w http.ResponseWriter, tpl *template.Template, status int, ti *templateInfo) { 146 | w.Header().Set("content-type", "text/html") 147 | var buf bytes.Buffer 148 | if err := tpl.Execute(&buf, ti); err != nil { 149 | http.Error(w, err.Error(), http.StatusInternalServerError) 150 | return 151 | } 152 | buf.WriteTo(w) 153 | } 154 | -------------------------------------------------------------------------------- /runtimefunc.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import "unsafe" 4 | 5 | var _ = unsafe.Pointer(nil) 6 | 7 | //go:linkname startTheWorld runtime.startTheWorld 8 | func startTheWorld() 9 | 10 | //go:linkname chanbuf runtime.chanbuf 11 | func chanbuf(ch unsafe.Pointer, i uint) unsafe.Pointer 12 | -------------------------------------------------------------------------------- /runtimefunc.s: -------------------------------------------------------------------------------- 1 | // This file is required to make stub function declarations work. 2 | -------------------------------------------------------------------------------- /runtimefunc_go120.go: -------------------------------------------------------------------------------- 1 | //go:build !go1.21 2 | // +build !go1.21 3 | 4 | package memsize 5 | 6 | import "unsafe" 7 | 8 | var _ = unsafe.Pointer(nil) 9 | 10 | const stwReadMemStats string = "memsize scan" 11 | 12 | //go:linkname stopTheWorld runtime.stopTheWorld 13 | func stopTheWorld(reason string) 14 | -------------------------------------------------------------------------------- /runtimefunc_go121.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | // +build go1.21 3 | 4 | package memsize 5 | 6 | import "unsafe" 7 | 8 | var _ = unsafe.Pointer(nil) 9 | 10 | //go:linkname stwReason runtime.stwReason 11 | type stwReason uint8 12 | 13 | //go:linkname stwReadMemStats runtime.stwReadMemStats 14 | const stwReadMemStats stwReason = 7 15 | 16 | //go:linkname stopTheWorld runtime.stopTheWorld 17 | func stopTheWorld(reason stwReason) 18 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // address is a memory location. 9 | // 10 | // Code dealing with uintptr is oblivious to the zero address. 11 | // Code dealing with address is not: it treats the zero address 12 | // as invalid. Offsetting an invalid address doesn't do anything. 13 | // 14 | // This distinction is useful because there are objects that we can't 15 | // get the pointer to. 16 | type address uintptr 17 | 18 | const invalidAddr = address(0) 19 | 20 | func (a address) valid() bool { 21 | return a != 0 22 | } 23 | 24 | func (a address) addOffset(off uintptr) address { 25 | if !a.valid() { 26 | return invalidAddr 27 | } 28 | return a + address(off) 29 | } 30 | 31 | func (a address) String() string { 32 | if uintptrBits == 32 { 33 | return fmt.Sprintf("%#0.8x", uintptr(a)) 34 | } 35 | return fmt.Sprintf("%#0.16x", uintptr(a)) 36 | } 37 | 38 | type typCache map[reflect.Type]typInfo 39 | 40 | type typInfo struct { 41 | isPointer bool 42 | needScan bool 43 | } 44 | 45 | // isPointer returns true for pointer-ish values. The notion of 46 | // pointer includes everything but plain values, i.e. slices, maps 47 | // channels, interfaces are 'pointer', too. 48 | func (tc *typCache) isPointer(typ reflect.Type) bool { 49 | return tc.info(typ).isPointer 50 | } 51 | 52 | // needScan reports whether a value of the type needs to be scanned 53 | // recursively because it may contain pointers. 54 | func (tc *typCache) needScan(typ reflect.Type) bool { 55 | return tc.info(typ).needScan 56 | } 57 | 58 | func (tc *typCache) info(typ reflect.Type) typInfo { 59 | info, found := (*tc)[typ] 60 | switch { 61 | case found: 62 | return info 63 | case isPointer(typ): 64 | info = typInfo{true, true} 65 | default: 66 | info = typInfo{false, tc.checkNeedScan(typ)} 67 | } 68 | (*tc)[typ] = info 69 | return info 70 | } 71 | 72 | func (tc *typCache) checkNeedScan(typ reflect.Type) bool { 73 | switch k := typ.Kind(); k { 74 | case reflect.Struct: 75 | // Structs don't need scan if none of their fields need it. 76 | for i := 0; i < typ.NumField(); i++ { 77 | if tc.needScan(typ.Field(i).Type) { 78 | return true 79 | } 80 | } 81 | case reflect.Array: 82 | // Arrays don't need scan if their element type doesn't. 83 | return tc.needScan(typ.Elem()) 84 | } 85 | return false 86 | } 87 | 88 | func isPointer(typ reflect.Type) bool { 89 | k := typ.Kind() 90 | switch { 91 | case k <= reflect.Complex128: 92 | return false 93 | case k == reflect.Array: 94 | return false 95 | case k >= reflect.Chan && k <= reflect.String: 96 | return true 97 | case k == reflect.Struct || k == reflect.UnsafePointer: 98 | return false 99 | default: 100 | unhandledKind(k) 101 | return false 102 | } 103 | } 104 | 105 | func unhandledKind(k reflect.Kind) { 106 | panic("unhandled kind " + k.String()) 107 | } 108 | 109 | // HumanSize formats the given number of bytes as a readable string. 110 | func HumanSize(bytes uintptr) string { 111 | switch { 112 | case bytes < 1024: 113 | return fmt.Sprintf("%d B", bytes) 114 | case bytes < 1024*1024: 115 | return fmt.Sprintf("%.3f KB", float64(bytes)/1024) 116 | default: 117 | return fmt.Sprintf("%.3f MB", float64(bytes)/1024/1024) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | package memsize 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | var typCacheTests = []struct { 9 | val interface{} 10 | want typInfo 11 | }{ 12 | { 13 | val: int(0), 14 | want: typInfo{isPointer: false, needScan: false}, 15 | }, 16 | { 17 | val: make(chan struct{}, 1), 18 | want: typInfo{isPointer: true, needScan: true}, 19 | }, 20 | { 21 | val: struct{ A int }{}, 22 | want: typInfo{isPointer: false, needScan: false}, 23 | }, 24 | { 25 | val: struct{ S string }{}, 26 | want: typInfo{isPointer: false, needScan: true}, 27 | }, 28 | { 29 | val: structloop{}, 30 | want: typInfo{isPointer: false, needScan: true}, 31 | }, 32 | { 33 | val: [3]int{}, 34 | want: typInfo{isPointer: false, needScan: false}, 35 | }, 36 | { 37 | val: [3]struct{ A int }{}, 38 | want: typInfo{isPointer: false, needScan: false}, 39 | }, 40 | { 41 | val: [3]struct{ S string }{}, 42 | want: typInfo{isPointer: false, needScan: true}, 43 | }, 44 | { 45 | val: [3]structloop{}, 46 | want: typInfo{isPointer: false, needScan: true}, 47 | }, 48 | { 49 | val: struct { 50 | a [32]uint8 51 | s [2][]uint8 52 | }{}, 53 | want: typInfo{isPointer: false, needScan: true}, 54 | }, 55 | } 56 | 57 | func TestTypeInfo(t *testing.T) { 58 | // This cache is shared among all test cases. It is used 59 | // to verify that putting many different types into the cache 60 | // doesn't change the resulting info. 61 | sharedtc := make(typCache) 62 | 63 | for i := range typCacheTests { 64 | test := typCacheTests[i] 65 | typ := reflect.TypeOf(test.val) 66 | t.Run(typ.String(), func(t *testing.T) { 67 | tc := make(typCache) 68 | info := tc.info(typ) 69 | if !reflect.DeepEqual(info, test.want) { 70 | t.Fatalf("wrong info from local cache:\ngot %+v, want %+v", info, test.want) 71 | } 72 | info = sharedtc.info(typ) 73 | if !reflect.DeepEqual(info, test.want) { 74 | t.Fatalf("wrong info from shared cache:\ngot %+v, want %+v", info, test.want) 75 | } 76 | }) 77 | } 78 | } 79 | --------------------------------------------------------------------------------