diff --git a/offheap/object_reference.go b/offheap/object_reference.go index 8a5f1eb..cbcb9ff 100644 --- a/offheap/object_reference.go +++ b/offheap/object_reference.go @@ -5,7 +5,7 @@ package offheap import ( - "fmt" + "reflect" "unsafe" "github.com/fmstephe/memorymanager/offheap/internal/pointerstore" @@ -19,10 +19,8 @@ import ( // Go allocations objects acquired via AllocObject do _not_ have their contents // zeroed out. func AllocObject[T any](s *Store) RefObject[T] { - // TODO this is not fast - we _need_ to cache this type data - if err := containsNoPointers[T](); err != nil { - panic(fmt.Errorf("cannot allocate generic type containing pointers %w", err)) - } + t := reflect.TypeFor[T]() + s.checker.checkType(t) idx := indexForType[T]() diff --git a/offheap/object_store.go b/offheap/object_store.go index d1f9673..e7ef279 100644 --- a/offheap/object_store.go +++ b/offheap/object_store.go @@ -11,6 +11,7 @@ import ( const defaultSlabSize = 1 << 13 type Store struct { + checker typeChecker sizedStores []*pointerstore.Store } @@ -18,9 +19,7 @@ type Store struct { // // This store manages allocation and freeing of any offheap allocated objects. func New() *Store { - return &Store{ - sizedStores: initSizeStore(defaultSlabSize), - } + return newSized(defaultSlabSize) } // Returns a new *Store. @@ -35,7 +34,13 @@ func New() *Store { // small slab sizes to allow faster tests with reduced memory usage. Most users // will probably prefer to use the default New() above. func NewSized(slabSize int) *Store { + return newSized(slabSize) +} + +// Returns a new *Store +func newSized(slabSize int) *Store { return &Store{ + checker: newTypeChecker(), sizedStores: initSizeStore(slabSize), } } diff --git a/offheap/pointer_checker_test.go b/offheap/pointer_checker_test.go deleted file mode 100644 index 88f4285..0000000 --- a/offheap/pointer_checker_test.go +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright 2024 Francis Michael Stephens. All rights reserved. Use of this -// source code is governed by an MIT license that can be found in the LICENSE -// file. - -package offheap - -import ( - "testing" - "unsafe" - - "github.com/stretchr/testify/assert" -) - -type deepBadStruct struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - badInt *int - //lint:ignore U1000 this field looks unused but is observed by reflection - deepBadField badStruct -} - -type badStruct struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - badField string -} - -// We will create a test which uses this struct in the future -// -//lint:ignore U1000 this struct actually is unused - but it represents a real bug in our code -type stringSmugglerStruct struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - reference RefObject[string] -} - -type manyPointers struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - chanField chan int - //lint:ignore U1000 this field looks unused but is observed by reflection - funcField func(int) int - //lint:ignore U1000 this field looks unused but is observed by reflection - interfaceField any - //lint:ignore U1000 this field looks unused but is observed by reflection - mapField map[int]int - //lint:ignore U1000 this field looks unused but is observed by reflection - pointerField *int - //lint:ignore U1000 this field looks unused but is observed by reflection - sliceField []int - //lint:ignore U1000 this field looks unused but is observed by reflection - stringField string -} - -func TestBadTypes(t *testing.T) { - // No arrays with pointers in them - assert.EqualError(t, containsNoPointers[[32]badStruct](), "found pointer(s): [32](offheap.badStruct)badField") - // No channels - assert.EqualError(t, containsNoPointers[chan int](), "found pointer(s): ") - // No functions - assert.EqualError(t, containsNoPointers[func(int) int](), "found pointer(s): ") - // No interfaces - assert.EqualError(t, containsNoPointers[any](), "found pointer(s): ") - // No maps - assert.EqualError(t, containsNoPointers[map[int]int](), "found pointer(s): ") - // No pointer(s) - assert.EqualError(t, containsNoPointers[*int](), "found pointer(s): <*int>") - // No slices - assert.EqualError(t, containsNoPointers[[]int](), "found pointer(s): <[]int>") - // No strings - assert.EqualError(t, containsNoPointers[string](), "found pointer(s): ") - // No structs with any pointerful fields - assert.EqualError(t, containsNoPointers[badStruct](), "found pointer(s): (offheap.badStruct)badField") - assert.EqualError(t, containsNoPointers[deepBadStruct](), "found pointer(s): (offheap.deepBadStruct)badInt<*int>,(offheap.deepBadStruct)deepBadField(offheap.badStruct)badField") - // assert.EqualError(t, containsNoPointers[stringSmugglerStruct](), "found pointer(s): ") - // No unsafe pointer(s) - assert.EqualError(t, containsNoPointers[unsafe.Pointer](), "found pointer(s): ") - // We should find all of the bad fields in this struct - assert.EqualError(t, containsNoPointers[manyPointers](), "found pointer(s): "+ - "(offheap.manyPointers)chanField,"+ - "(offheap.manyPointers)funcField,"+ - "(offheap.manyPointers)interfaceField,"+ - "(offheap.manyPointers)mapField,"+ - "(offheap.manyPointers)pointerField<*int>,"+ - "(offheap.manyPointers)sliceField<[]int>,"+ - "(offheap.manyPointers)stringField") -} - -type deepGoodStruct struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - boolField bool - //lint:ignore U1000 this field looks unused but is observed by reflection - deepField goodStruct -} - -type goodStruct struct { - //lint:ignore U1000 this field looks unused but is observed by reflection - intField int - //lint:ignore U1000 this field looks unused but is observed by reflection - floatField float64 - //lint:ignore U1000 this field looks unused but is observed by reflection - referenceField RefObject[goodStruct] -} - -func TestGoodTypes(t *testing.T) { - // bool is fine - assert.Nil(t, containsNoPointers[bool]()) - // ints are fine - assert.Nil(t, containsNoPointers[int]()) - // uints are fine - assert.Nil(t, containsNoPointers[uint]()) - // floats are fine - assert.Nil(t, containsNoPointers[float32]()) - // complex numbers are fine - assert.Nil(t, containsNoPointers[complex64]()) - // arrays are fine - assert.Nil(t, containsNoPointers[[32]int]()) - // structs with no pointerful fields are fine - assert.Nil(t, containsNoPointers[goodStruct]()) - assert.Nil(t, containsNoPointers[deepGoodStruct]()) -} diff --git a/offheap/slice_reference.go b/offheap/slice_reference.go index 60c1c90..2ad3820 100644 --- a/offheap/slice_reference.go +++ b/offheap/slice_reference.go @@ -6,6 +6,7 @@ package offheap import ( "fmt" + "reflect" "unsafe" "github.com/fmstephe/memorymanager/offheap/internal/pointerstore" @@ -18,10 +19,8 @@ import ( // The contents of the slice will be arbitrary. Unlike Go slices acquired via // AllocSlice do _not_ have their contents zeroed out. func AllocSlice[T any](s *Store, length, requestedCapacity int) RefSlice[T] { - // TODO this is not fast - we _need_ to cache this type data - if err := containsNoPointers[T](); err != nil { - panic(fmt.Errorf("cannot allocate generic type containing pointers %w", err)) - } + t := reflect.TypeFor[T]() + s.checker.checkType(t) // Round the requested capacity up to a power of 2 actualCapacity := capacityForSlice(requestedCapacity) diff --git a/offheap/pointer_checker.go b/offheap/type_checker.go similarity index 68% rename from offheap/pointer_checker.go rename to offheap/type_checker.go index 7c3142a..f994487 100644 --- a/offheap/pointer_checker.go +++ b/offheap/type_checker.go @@ -1,4 +1,4 @@ -// Copyright 2024 Francis Michael Stephens. All rights reserved. Use of this +// Copyright 2025 Francis Michael Stephens. All rights reserved. Use of this // source code is governed by an MIT license that can be found in the LICENSE // file. @@ -8,8 +8,48 @@ import ( "fmt" "reflect" "strconv" + "sync" ) +type typeChecker struct { + typeCacheLock sync.RWMutex + typeCache map[reflect.Type]struct{} +} + +func newTypeChecker() typeChecker { + return typeChecker{ + typeCacheLock: sync.RWMutex{}, + typeCache: map[reflect.Type]struct{}{}, + } +} + +func (c *typeChecker) checkType(t reflect.Type) { + if c.lookupTypeCache(t) { + return + } + + if err := containsNoPointers(t); err != nil { + panic(fmt.Errorf("cannot allocate generic type containing pointers %w", err)) + } + + c.writeTypeToCache(t) +} + +func (c *typeChecker) lookupTypeCache(t reflect.Type) bool { + c.typeCacheLock.RLock() + defer c.typeCacheLock.RUnlock() + + _, ok := c.typeCache[t] + return ok +} + +func (c *typeChecker) writeTypeToCache(t reflect.Type) { + c.typeCacheLock.Lock() + defer c.typeCacheLock.Unlock() + + c.typeCache[t] = struct{}{} +} + type typePaths struct { paths []string } @@ -35,8 +75,7 @@ func (p *typePaths) String() string { return result[:len(result)-1] } -func containsNoPointers[O any]() error { - t := reflect.TypeFor[O]() +func containsNoPointers(t reflect.Type) error { paths := &typePaths{} searchForPointers(t, "", paths) if paths.Len() != 0 { diff --git a/offheap/type_checker_test.go b/offheap/type_checker_test.go new file mode 100644 index 0000000..17be822 --- /dev/null +++ b/offheap/type_checker_test.go @@ -0,0 +1,192 @@ +// Copyright 2024 Francis Michael Stephens. All rights reserved. Use of this +// source code is governed by an MIT license that can be found in the LICENSE +// file. + +package offheap + +import ( + "reflect" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +type deepBadStruct struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + badInt *int + //lint:ignore U1000 this field looks unused but is observed by reflection + deepBadField badStruct +} + +type badStruct struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + badField string +} + +// We will create a test which uses this struct in the future +// +//lint:ignore U1000 this struct actually is unused - but it represents a real bug in our code +type stringSmugglerStruct struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + reference RefObject[string] +} + +type manyPointers struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + chanField chan int + //lint:ignore U1000 this field looks unused but is observed by reflection + funcField func(int) int + //lint:ignore U1000 this field looks unused but is observed by reflection + interfaceField any + //lint:ignore U1000 this field looks unused but is observed by reflection + mapField map[int]int + //lint:ignore U1000 this field looks unused but is observed by reflection + pointerField *int + //lint:ignore U1000 this field looks unused but is observed by reflection + sliceField []int + //lint:ignore U1000 this field looks unused but is observed by reflection + stringField string +} + +func TestBadTypes_containsNoPointers(t *testing.T) { + // No arrays with pointers in them + assert.EqualError(t, containsNoPointers(reflect.TypeFor[[32]badStruct]()), "found pointer(s): [32](offheap.badStruct)badField") + // No channels + assert.EqualError(t, containsNoPointers(reflect.TypeFor[chan int]()), "found pointer(s): ") + // No functions + assert.EqualError(t, containsNoPointers(reflect.TypeFor[func(int) int]()), "found pointer(s): ") + // No interfaces + assert.EqualError(t, containsNoPointers(reflect.TypeFor[any]()), "found pointer(s): ") + // No maps + assert.EqualError(t, containsNoPointers(reflect.TypeFor[map[int]int]()), "found pointer(s): ") + // No pointer(s) + assert.EqualError(t, containsNoPointers(reflect.TypeFor[*int]()), "found pointer(s): <*int>") + // No slices + assert.EqualError(t, containsNoPointers(reflect.TypeFor[[]int]()), "found pointer(s): <[]int>") + // No strings + assert.EqualError(t, containsNoPointers(reflect.TypeFor[string]()), "found pointer(s): ") + // No structs with any pointerful fields + assert.EqualError(t, containsNoPointers(reflect.TypeFor[badStruct]()), "found pointer(s): (offheap.badStruct)badField") + assert.EqualError(t, containsNoPointers(reflect.TypeFor[deepBadStruct]()), "found pointer(s): (offheap.deepBadStruct)badInt<*int>,(offheap.deepBadStruct)deepBadField(offheap.badStruct)badField") + // assert.EqualError(t, containsNoPointers(reflect.TypeFor[stringSmugglerStruct]()), "found pointer(s): ") + // No unsafe pointer(s) + assert.EqualError(t, containsNoPointers(reflect.TypeFor[unsafe.Pointer]()), "found pointer(s): ") + // We should find all of the bad fields in this struct + assert.EqualError(t, containsNoPointers(reflect.TypeFor[manyPointers]()), "found pointer(s): "+ + "(offheap.manyPointers)chanField,"+ + "(offheap.manyPointers)funcField,"+ + "(offheap.manyPointers)interfaceField,"+ + "(offheap.manyPointers)mapField,"+ + "(offheap.manyPointers)pointerField<*int>,"+ + "(offheap.manyPointers)sliceField<[]int>,"+ + "(offheap.manyPointers)stringField") +} + +func TestBadTypes_checkTypes(t *testing.T) { + checker := newTypeChecker() + + // No arrays with pointers in them} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[[32]badStruct]()) }) + // No channels} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[chan int]()) }) + // No functions} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[func(int) int]()) }) + // No interfaces} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[any]()) }) + // No maps} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[map[int]int]()) }) + // No pointer(s)} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[*int]()) }) + // No slices} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[[]int]()) }) + // No strings} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[string]()) }) + // No structs with any pointerful fields} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[badStruct]()) }) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[deepBadStruct]()) }) + // assert.Panics(t, func () { checker.checkType(reflect.TypeFor[stringSmugglerStruct]())} ) + // No unsafe pointer(s)} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[unsafe.Pointer]()) }) + // We should find all of the bad fields in this struct} ) + assert.Panics(t, func() { checker.checkType(reflect.TypeFor[manyPointers]()) }) +} + +type deepGoodStruct struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + boolField bool + //lint:ignore U1000 this field looks unused but is observed by reflection + deepField goodStruct +} + +type goodStruct struct { + //lint:ignore U1000 this field looks unused but is observed by reflection + intField int + //lint:ignore U1000 this field looks unused but is observed by reflection + floatField float64 + //lint:ignore U1000 this field looks unused but is observed by reflection + referenceField RefObject[goodStruct] +} + +func TestGoodTypes_containsNoPointers(t *testing.T) { + // bool is fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[bool]())) + // ints are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[int]())) + // uints are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[uint]())) + // floats are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[float32]())) + // complex numbers are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[complex64]())) + // arrays are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[[32]int]())) + // structs with no pointerful fields are fine + assert.Nil(t, containsNoPointers(reflect.TypeFor[goodStruct]())) + assert.Nil(t, containsNoPointers(reflect.TypeFor[deepGoodStruct]())) +} + +func TestGoodTypes_checkType(t *testing.T) { + checker := newTypeChecker() + + // bool is fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[bool]()) }) + // ints are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[int]()) }) + // uints are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[uint]()) }) + // floats are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[float32]()) }) + // complex numbers are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[complex64]()) }) + // arrays are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[[32]int]()) }) + // structs with no pointerful fields are fine + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[goodStruct]()) }) + assert.NotPanics(t, func() { checker.checkType(reflect.TypeFor[deepGoodStruct]()) }) +} + +func TestTypeCaching(t *testing.T) { + checker := newTypeChecker() + + types := []reflect.Type{ + reflect.TypeFor[bool](), + reflect.TypeFor[int](), + reflect.TypeFor[uint](), + reflect.TypeFor[float32](), + reflect.TypeFor[complex64](), + reflect.TypeFor[goodStruct](), + reflect.TypeFor[deepGoodStruct](), + } + + for _, typ := range types { + // Initially the type is not in the cache + assert.NotContains(t, checker.typeCache, typ) + // Check the type + checker.checkType(typ) + // The type is now in the cache + assert.Contains(t, checker.typeCache, typ) + // Check the type again, still works + checker.checkType(typ) + } +}