Skip to content

Commit 88a4914

Browse files
committed
fix: use better size calculation
1 parent fe67c99 commit 88a4914

4 files changed

Lines changed: 315 additions & 2 deletions

File tree

collector/db_query_collector.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"context"
55
"database/sql/driver"
66
"time"
7+
8+
"github.com/networkteam/devlog/internal/utils"
79
)
810

911
// DBQuery represents a database query execution record
@@ -27,8 +29,15 @@ func (q DBQuery) Size() uint64 {
2729
size := uint64(100) // base struct overhead
2830
size += uint64(len(q.Query))
2931
size += uint64(len(q.Language))
30-
// Estimate 50 bytes per arg (name + value)
31-
size += uint64(len(q.Args) * 50)
32+
// Calculate actual size of arguments using reflection
33+
for _, arg := range q.Args {
34+
size += uint64(len(arg.Name))
35+
size += 8 // Ordinal int field
36+
argSize := utils.SizeOf(arg.Value)
37+
if argSize > 0 {
38+
size += uint64(argSize)
39+
}
40+
}
3241
return size
3342
}
3443

docs/screenshot.png

-50.8 KB
Loading

internal/utils/size.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Package utils provides utility functions for the devlog package.
2+
//
3+
// The size calculation code in this file is based on github.com/DmitriyVTitov/size
4+
// Original source: https://github.com/DmitriyVTitov/size
5+
//
6+
// MIT License
7+
//
8+
// Copyright (c) 2020 DmitriyVTitov
9+
//
10+
// Permission is hereby granted, free of charge, to any person obtaining a copy
11+
// of this software and associated documentation files (the "Software"), to deal
12+
// in the Software without restriction, including without limitation the rights
13+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14+
// copies of the Software, and to permit persons to whom the Software is
15+
// furnished to do so, subject to the following conditions:
16+
//
17+
// The above copyright notice and this permission notice shall be included in all
18+
// copies or substantial portions of the Software.
19+
//
20+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26+
// SOFTWARE.
27+
package utils
28+
29+
import (
30+
"reflect"
31+
"unsafe"
32+
)
33+
34+
// SizeOf returns the size of 'v' in bytes.
35+
// If there is an error during calculation, SizeOf returns -1.
36+
func SizeOf(v any) int {
37+
// Cache with every visited pointer so we don't count two pointers
38+
// to the same memory twice.
39+
cache := make(map[uintptr]bool)
40+
return sizeOf(reflect.Indirect(reflect.ValueOf(v)), cache)
41+
}
42+
43+
// sizeOf returns the number of bytes the actual data represented by v occupies in memory.
44+
// If there is an error, sizeOf returns -1.
45+
func sizeOf(v reflect.Value, cache map[uintptr]bool) int {
46+
switch v.Kind() {
47+
48+
case reflect.Array:
49+
sum := 0
50+
for i := 0; i < v.Len(); i++ {
51+
s := sizeOf(v.Index(i), cache)
52+
if s < 0 {
53+
return -1
54+
}
55+
sum += s
56+
}
57+
58+
return sum + (v.Cap()-v.Len())*int(v.Type().Elem().Size())
59+
60+
case reflect.Slice:
61+
// return 0 if this node has been visited already
62+
if cache[v.Pointer()] {
63+
return 0
64+
}
65+
cache[v.Pointer()] = true
66+
67+
sum := 0
68+
for i := 0; i < v.Len(); i++ {
69+
s := sizeOf(v.Index(i), cache)
70+
if s < 0 {
71+
return -1
72+
}
73+
sum += s
74+
}
75+
76+
sum += (v.Cap() - v.Len()) * int(v.Type().Elem().Size())
77+
78+
return sum + int(v.Type().Size())
79+
80+
case reflect.Struct:
81+
sum := 0
82+
for i, n := 0, v.NumField(); i < n; i++ {
83+
s := sizeOf(v.Field(i), cache)
84+
if s < 0 {
85+
return -1
86+
}
87+
sum += s
88+
}
89+
90+
// Look for struct padding.
91+
padding := int(v.Type().Size())
92+
for i, n := 0, v.NumField(); i < n; i++ {
93+
padding -= int(v.Field(i).Type().Size())
94+
}
95+
96+
return sum + padding
97+
98+
case reflect.String:
99+
s := v.String()
100+
if len(s) == 0 {
101+
return int(v.Type().Size())
102+
}
103+
// Use unsafe.StringData (Go 1.20+) instead of deprecated reflect.StringHeader
104+
dataPtr := uintptr(unsafe.Pointer(unsafe.StringData(s)))
105+
if cache[dataPtr] {
106+
return int(v.Type().Size())
107+
}
108+
cache[dataPtr] = true
109+
return len(s) + int(v.Type().Size())
110+
111+
case reflect.Ptr:
112+
// return Ptr size if this node has been visited already (infinite recursion)
113+
if cache[v.Pointer()] {
114+
return int(v.Type().Size())
115+
}
116+
cache[v.Pointer()] = true
117+
if v.IsNil() {
118+
return int(reflect.New(v.Type()).Type().Size())
119+
}
120+
s := sizeOf(reflect.Indirect(v), cache)
121+
if s < 0 {
122+
return -1
123+
}
124+
return s + int(v.Type().Size())
125+
126+
case reflect.Bool,
127+
reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
128+
reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
129+
reflect.Int, reflect.Uint,
130+
reflect.Chan,
131+
reflect.Uintptr,
132+
reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128,
133+
reflect.Func:
134+
return int(v.Type().Size())
135+
136+
case reflect.Map:
137+
// return 0 if this node has been visited already (infinite recursion)
138+
if cache[v.Pointer()] {
139+
return 0
140+
}
141+
cache[v.Pointer()] = true
142+
sum := 0
143+
keys := v.MapKeys()
144+
for i := range keys {
145+
val := v.MapIndex(keys[i])
146+
// calculate size of key and value separately
147+
sv := sizeOf(val, cache)
148+
if sv < 0 {
149+
return -1
150+
}
151+
sum += sv
152+
sk := sizeOf(keys[i], cache)
153+
if sk < 0 {
154+
return -1
155+
}
156+
sum += sk
157+
}
158+
// Include overhead due to unused map buckets. 10.79 comes
159+
// from https://golang.org/src/runtime/map.go.
160+
return sum + int(v.Type().Size()) + int(float64(len(keys))*10.79)
161+
162+
case reflect.Interface:
163+
return sizeOf(v.Elem(), cache) + int(v.Type().Size())
164+
165+
case reflect.Invalid:
166+
return 0
167+
}
168+
169+
return -1
170+
}

internal/utils/size_test.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// Tests adapted from github.com/DmitriyVTitov/size
2+
// Original source: https://github.com/DmitriyVTitov/size/blob/master/size_test.go
3+
package utils
4+
5+
import (
6+
"testing"
7+
)
8+
9+
func TestSizeOf(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
v any
13+
want int
14+
}{
15+
{
16+
name: "Array",
17+
v: [3]int32{1, 2, 3}, // 3 * 4 = 12
18+
want: 12,
19+
},
20+
{
21+
name: "Slice",
22+
v: make([]int64, 2, 5), // 5 * 8 + 24 = 64
23+
want: 64,
24+
},
25+
{
26+
name: "String",
27+
v: "ABCdef", // 6 + 16 = 22
28+
want: 22,
29+
},
30+
{
31+
name: "Map",
32+
// (8 + 3 + 16) + (8 + 4 + 16) = 55
33+
// 55 + 8 + 10.79 * 2 = 84
34+
v: map[int64]string{0: "ABC", 1: "DEFG"},
35+
want: 84,
36+
},
37+
{
38+
name: "Struct",
39+
v: struct {
40+
slice []int64
41+
array [2]bool
42+
structure struct {
43+
i int8
44+
s string
45+
}
46+
}{
47+
slice: []int64{12345, 67890}, // 2 * 8 + 24 = 40
48+
array: [2]bool{true, false}, // 2 * 1 = 2
49+
structure: struct {
50+
i int8
51+
s string
52+
}{
53+
i: 5, // 1
54+
s: "abc", // 3 * 1 + 16 = 19
55+
}, // 20 + 7 (padding) = 27
56+
}, // 40 + 2 + 27 = 69 + 6 (padding) = 75
57+
want: 75,
58+
},
59+
{
60+
name: "Nil",
61+
v: nil,
62+
want: 0,
63+
},
64+
{
65+
name: "Int64",
66+
v: int64(42),
67+
want: 8,
68+
},
69+
{
70+
name: "Float64",
71+
v: float64(3.14),
72+
want: 8,
73+
},
74+
{
75+
name: "Bool",
76+
v: true,
77+
want: 1,
78+
},
79+
{
80+
name: "ByteSlice",
81+
v: []byte("hello world"), // 11 + 24 = 35
82+
want: 35,
83+
},
84+
{
85+
name: "EmptyString",
86+
v: "",
87+
want: 16, // just the string header
88+
},
89+
{
90+
name: "LargeByteSlice",
91+
v: make([]byte, 1000), // 1000 + 24 = 1024
92+
want: 1024,
93+
},
94+
}
95+
for _, tt := range tests {
96+
t.Run(tt.name, func(t *testing.T) {
97+
if got := SizeOf(tt.v); got != tt.want {
98+
t.Errorf("SizeOf() = %v, want %v", got, tt.want)
99+
}
100+
})
101+
}
102+
}
103+
104+
func TestSizeOf_Pointer(t *testing.T) {
105+
s := "test"
106+
ptr := &s
107+
108+
// SizeOf uses reflect.Indirect at the top level, so it measures
109+
// the dereferenced value: 4 (string data) + 16 (string header) = 20
110+
got := SizeOf(ptr)
111+
want := 20
112+
if got != want {
113+
t.Errorf("SizeOf(ptr) = %v, want %v", got, want)
114+
}
115+
}
116+
117+
func TestSizeOf_CircularReference(t *testing.T) {
118+
type Node struct {
119+
Value int
120+
Next *Node
121+
}
122+
123+
// Create a circular reference
124+
a := &Node{Value: 1}
125+
b := &Node{Value: 2}
126+
a.Next = b
127+
b.Next = a
128+
129+
// Should not panic or infinite loop
130+
got := SizeOf(a)
131+
if got < 0 {
132+
t.Errorf("SizeOf() returned error for circular reference: %v", got)
133+
}
134+
}

0 commit comments

Comments
 (0)