diff --git a/gno.land/pkg/integration/testdata/gc.txtar b/gno.land/pkg/integration/testdata/gc.txtar new file mode 100644 index 00000000000..bd025f447ad --- /dev/null +++ b/gno.land/pkg/integration/testdata/gc.txtar @@ -0,0 +1,26 @@ +# Init +## deploy realm +loadpkg gno.land/r/gc $WORK/r/gc + +## start a new node +gnoland start + + + +gnokey maketx call -pkgpath gno.land/r/gc -func Alloc -gas-fee 100000ugnot -gas-wanted 3000000 -simulate skip -broadcast -chainid tendermint_test test1 +stdout 'GAS USED: 576890' + + +-- r/gc/gc.gno -- +package gc + +func gen() { + _ = make([]byte, 250*1024*1024) +} + +func Alloc() { + for i := 0; i < 100; i++ { + gen() + gen() + } +} diff --git a/gnovm/pkg/gnolang/alloc.go b/gnovm/pkg/gnolang/alloc.go index 1fce05d9dcb..3750fb7aa39 100644 --- a/gnovm/pkg/gnolang/alloc.go +++ b/gnovm/pkg/gnolang/alloc.go @@ -1,12 +1,18 @@ package gnolang +import ( + "fmt" +) + // Keeps track of in-memory allocations. // In the future, allocations within realm boundaries will be // (optionally?) condensed (objects to be GC'd will be discarded), // but for now, allocations strictly increment across the whole tx. type Allocator struct { - maxBytes int64 - bytes int64 + maxBytes int64 + bytes int64 + visitCount int64 // times objects are visited for gc + gc func() (int64, bool) // gc callback } // for gonative, which doesn't consider the allocator. @@ -19,15 +25,19 @@ const ( // gno types _allocSlice = 24 _allocPointerValue = 40 - _allocStructValue = 152 - _allocArrayValue = 176 + _allocStringValue = 16 + _allocStructValue = 160 + _allocArrayValue = 184 _allocSliceValue = 40 - _allocFuncValue = 136 - _allocMapValue = 144 - _allocBoundMethodValue = 176 - _allocBlock = 464 + _allocFuncValue = 196 + _allocMapValue = 152 + _allocBoundMethodValue = 184 + _allocBlock = 480 + _allocPackageValue = 248 + _allocNativeValue = 48 _allocTypeValue = 16 _allocTypedValue = 40 + _allocRefValue = 72 _allocBigint = 200 // XXX _allocBigdec = 200 // XXX _allocType = 200 // XXX @@ -35,7 +45,7 @@ const ( ) const ( - allocString = _allocBase + allocString = _allocBase + _allocPointer + _allocStringValue allocStringByte = 1 allocBigint = _allocBase + _allocPointer + _allocBigint allocBigintByte = 1 @@ -53,12 +63,14 @@ const ( allocBoundMethod = _allocBase + _allocPointer + _allocBoundMethodValue allocBlock = _allocBase + _allocPointer + _allocBlock allocBlockItem = _allocTypedValue + allocNative = _allocBase + _allocPointer + _allocNativeValue + allocRefValue = _allocBase + +_allocRefValue allocType = _allocBase + _allocPointer + _allocType - // allocDataByte = 1 - // allocPackge = 1 - allocAmino = _allocBase + _allocPointer + _allocAny - allocAminoByte = 10 // XXX - allocHeapItem = _allocBase + _allocPointer + _allocTypedValue + allocDataByte = 1 + allocPackage = _allocBase + _allocPointer + _allocPackageValue + allocAmino = _allocBase + _allocPointer + _allocAny + allocAminoByte = 10 // XXX + allocHeapItem = _allocBase + _allocPointer + _allocTypedValue ) func NewAllocator(maxBytes int64) *Allocator { @@ -70,6 +82,18 @@ func NewAllocator(maxBytes int64) *Allocator { } } +func (alloc *Allocator) SetGCCallback(f func() (int64, bool)) { + alloc.gc = f +} + +func (alloc *Allocator) MemStats() string { + if alloc == nil { + return "nil allocator" + } else { + return fmt.Sprintf("Allocator{maxBytes:%d, bytes:%d}", alloc.maxBytes, alloc.bytes) + } +} + func (alloc *Allocator) Status() (maxBytes int64, bytes int64) { return alloc.maxBytes, alloc.bytes } @@ -100,7 +124,17 @@ func (alloc *Allocator) Allocate(size int64) { alloc.bytes += size if alloc.bytes > alloc.maxBytes { - panic("allocation limit exceeded") + if left, ok := alloc.gc(); !ok { + panic("should not happen, allocation limit exceeded while gc.") + } else { // retry + if debug { + debug.Printf("%d left after GC, required size: %d\n", left, size) + } + alloc.bytes += size + if alloc.bytes > alloc.maxBytes { + panic("allocation limit exceeded") + } + } } } @@ -299,3 +333,80 @@ func (alloc *Allocator) NewHeapItem(tv TypedValue) *HeapItemValue { alloc.AllocateHeapItem() return &HeapItemValue{Value: tv} } + +// ----------------------------------------------- + +func (pv *PackageValue) GetShallowSize() int64 { + return allocPackage +} + +func (b *Block) GetShallowSize() int64 { + return allocBlock +} + +func (av *ArrayValue) GetShallowSize() int64 { + if av.Data != nil { + return allocArray + int64(len(av.Data)) + } else { + return allocArray + } +} + +func (sv *StructValue) GetShallowSize() int64 { + return allocStruct +} + +func (mv *MapValue) GetShallowSize() int64 { + return allocMap +} + +func (bmv *BoundMethodValue) GetShallowSize() int64 { + return allocBoundMethod +} + +func (hiv *HeapItemValue) GetShallowSize() int64 { + return allocHeapItem +} + +func (rv RefValue) GetShallowSize() int64 { + fmt.Println("---refValue, get shallow size...") + return allocRefValue +} + +func (pv PointerValue) GetShallowSize() int64 { + return allocPointer +} + +func (sv *SliceValue) GetShallowSize() int64 { + return allocSlice +} + +// Only count for closures. +func (fv *FuncValue) GetShallowSize() int64 { + if fv.IsClosure { + return allocFunc + } + return 0 +} + +func (sv StringValue) GetShallowSize() int64 { + return allocString + allocStringByte*int64(len(sv)) +} + +func (bv BigintValue) GetShallowSize() int64 { + return allocBigint +} + +func (bv BigdecValue) GetShallowSize() int64 { + return allocBigdec +} + +func (dbv DataByteValue) GetShallowSize() int64 { + return allocDataByte +} + +// Do not count during recalculation, +// as the type should pre-exist. +func (tv TypeValue) GetShallowSize() int64 { + return 0 +} diff --git a/gnovm/pkg/gnolang/garbage_collector.go b/gnovm/pkg/gnolang/garbage_collector.go new file mode 100644 index 00000000000..07ff567e119 --- /dev/null +++ b/gnovm/pkg/gnolang/garbage_collector.go @@ -0,0 +1,408 @@ +package gnolang + +import ( + "reflect" + + "github.com/gnolang/gno/tm2/pkg/overflow" +) + +// Represents the "time unit" cost for +// a single garbage collection visit. +// It's similar to "CPU cycles" and is +// calculated based on a rough benchmarking +// results. +// TODO: more accurate benchmark. +const VisitCpuFactor = 8 + +// Visit visits all reachable associated values. +// It is used primarily for GC. +// The caller must provide a callback visitor +// which knows how to break cycles, otherwise +// the Visit function may recurse infinitely. +// (the GC does this with GCCycle) +// It does not call the visitor on itself. +type Visitor func(v Value) (stop bool) + +// Returns the amount of memory left over. If the allocator limit is exceeded +// it returns false. It doesn't actually garbage collect, but it recalculates +// allocated memory from what is already reachable. +// NOTE: +// +// the tv.T types must not be measured. this is because the types are +// supposed to pre-exist, and memory allocation for tv.T depends on the +// impl, whether it re-uses the same Type or not. +// +// XXX: make sure tv.T isn't bumped from allocation either. +func (m *Machine) GarbageCollect() (left int64, ok bool) { + defer func() { + gasCPU := overflow.Mul64p(m.Alloc.visitCount*VisitCpuFactor, GasFactorCPU) + m.Alloc.visitCount = 0 + if m.GasMeter != nil { + m.GasMeter.ConsumeGas(gasCPU, "GC") + } + }() + + // We don't need the old value anymore. + m.Alloc.Reset() + + // This is the only place where it's bumped. + m.GCCycle += 1 + + // Construct visitor callback. + vis := GCVisitorFn(m.GCCycle, m.Alloc) + + // Visit blocks + for _, block := range m.Blocks { + if block == nil { + continue + } + stop := vis(block) + if stop { + return -1, false + } + } + + // Visit frames + for _, frame := range m.Frames { + stop := frame.Visit(vis) + if stop { + return -1, false + } + } + + // Visit package + stop := vis(m.Package) + if stop { + return -1, false + } + + // Visit exceptions + for _, exception := range m.Exceptions { + stop = exception.Visit(vis) + if stop { + return -1, false + } + } + + // Return bytes remaining. + maxBytes, bytes := m.Alloc.Status() + return maxBytes - bytes, true +} + +// Returns a visitor that bumps the GCCycle counter +// and stops if alloc is out of memory. +func GCVisitorFn(gcCycle int64, alloc *Allocator) Visitor { + var vis func(value Value) bool + + vis = func(v Value) bool { + if debug { + debug.Printf("Visit, v: %v (type: %v)\n", v, reflect.TypeOf(v)) + } + + if oo, isObject := v.(Object); isObject { + defer func() { + // Finally bump cycle for object. + oo.SetLastGCCycle(gcCycle) + }() + + // Return if already measured. + if debug { + debug.Printf("oo.GetLastGCCycle: %d, gcCycle: %d\n", oo.GetLastGCCycle(), gcCycle) + } + if oo.GetLastGCCycle() == gcCycle { + return false // but don't stop + } + } + + alloc.visitCount++ // Count operations for gas calculation + + // Add object size to alloc. + size := v.GetShallowSize() + alloc.Allocate(size) + + // Stop if alloc max exceeded. + // NOTE: Unlikely to occur, but keep it here for + // now to handle potential edge cases. + // Consider removing it later if no issues arise. + maxBytes, curBytes := alloc.Status() + if maxBytes < curBytes { + return true + } + + // Invoke the traverser on v. + stop := v.VisitAssociated(vis) + + return stop + } + return vis +} + +// --------------------------------------------------------------- +// visit associated + +func (sv *SliceValue) VisitAssociated(vis Visitor) (stop bool) { + // Visit base. + if sv.Base != nil { + stop = vis(sv.Base) + } + return stop +} + +func (av *ArrayValue) VisitAssociated(vis Visitor) (stop bool) { + // Visit each value. + for i := 0; i < len(av.List); i++ { + v := av.List[i].V + if v == nil { + continue + } + stop = vis(v) + if stop { + return + } + } + return +} + +func (fv *FuncValue) VisitAssociated(vis Visitor) (stop bool) { + // visit captures + for _, tv := range fv.Captures { + v := tv.V + if v == nil { + continue + } + stop = vis(v) + if stop { + return + } + } + + // Skip visiting the parent to avoid redundancy + // and prevent a potential cycle. + return +} + +func (sv *StructValue) VisitAssociated(vis Visitor) (stop bool) { + // Visit each value. + for i := 0; i < len(sv.Fields); i++ { + v := sv.Fields[i].V + if v == nil { + continue + } + stop = vis(v) + if stop { + return + } + } + return +} + +func (bmv *BoundMethodValue) VisitAssociated(vis Visitor) (stop bool) { + // bmv.Func cannot be a closure, it must be a method. + // So we do not visit it (for garbage collection). + + // Visit receiver. + v := bmv.Receiver.V + if v != nil { + stop = vis(v) + } + return +} + +func (mv *MapValue) VisitAssociated(vis Visitor) (stop bool) { + // visit mv.List. + for cur := mv.List.Head; cur != nil; cur = cur.Next { + // vis key + k := cur.Key.V + if k != nil { + stop = vis(k) + } + + if stop { + return + } + + // vis value + v := cur.Value.V + if v != nil { + stop = vis(v) + } + + if stop { + return + } + } + return +} + +func (pv *PackageValue) VisitAssociated(vis Visitor) (stop bool) { + // visit pv.Block + v := pv.Block + if v != nil { + stop = vis(pv.Block) + } + + if stop { + return + } + + // visit pv.FBlocks + for _, fb := range pv.FBlocks { + if fb == nil { + continue + } + + stop = vis(fb) + if stop { + return + } + } + + // do NOT visit Realm. + + return +} + +func (b *Block) VisitAssociated(vis Visitor) (stop bool) { + // Visit each value. + for i := 0; i < len(b.Values); i++ { + v := b.Values[i].V + if v == nil { + continue + } + stop = vis(v) + if stop { + return + } + } + + // Visit parent. + switch v := b.Parent.(type) { + case nil: + return + case *Block: + if v != nil { + stop = vis(v) + } + case RefValue: + stop = vis(v) + } + + return +} + +func (hiv *HeapItemValue) VisitAssociated(vis Visitor) (stop bool) { + v := hiv.Value.V + if v != nil { + stop = vis(hiv.Value.V) + } + return +} + +func (pv PointerValue) VisitAssociated(vis Visitor) (stop bool) { + // NOTE: *TV and Key will be visited along with base. + v := pv.Base + if v != nil { + stop = vis(pv.Base) + } + return +} + +func (sv StringValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +func (bv BigintValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +func (bv BigdecValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +func (dbv DataByteValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +func (rv RefValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +// Do not count the TypeValue, neither shallowly nor deeply. +func (tv TypeValue) VisitAssociated(vis Visitor) (stop bool) { + return false +} + +// ------------------------------------------------------------------- +// custom visit methods + +func (fr *Frame) Visit(vis Visitor) (stop bool) { + // vis receiver + v := fr.Receiver.V + if v != nil { + stop = vis(v) + } + if stop { + return + } + + // vis FuncValue + if fv := fr.Func; fv != nil { + stop = vis(fv) + if stop { + return + } + } + + // vis defer + for _, dfr := range fr.Defers { + // visit dfr.Func + if dfr.Func != nil { + stop = vis(dfr.Func) + } + if stop { + return + } + + for _, arg := range dfr.Args { + if arg.V != nil { + stop = vis(arg.V) + } + if stop { + return + } + } + + if dfr.Parent != nil { + stop = vis(dfr.Parent) + } + if stop { + return + } + } + + // vis last package + if fr.LastPackage != nil { + stop = vis(fr.LastPackage) + } + if stop { + return + } + + return +} + +func (ex *Exception) Visit(vis Visitor) (stop bool) { + // vis value + v := ex.Value.V + if v != nil { + stop = vis(v) + } + if stop { + return + } + + // The frame should have been visited elsewhere. + // This ensures integrity and improves readability. + stop = ex.Frame.Visit(vis) + return +} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 461682435c8..32dbe6ed948 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -37,6 +37,7 @@ type Machine struct { Exceptions []Exception NumResults int // number of results returned Cycles int64 // number of "cpu" cycles + GCCycle int64 // number of "gc" cycles Debugger Debugger @@ -137,6 +138,9 @@ func NewMachineWithOptions(opts MachineOptions) *Machine { mm := machinePool.Get().(*Machine) mm.Package = pv mm.Alloc = alloc + if mm.Alloc != nil { + mm.Alloc.SetGCCallback(func() (int64, bool) { return mm.GarbageCollect() }) + } mm.PreprocessorMode = preprocessorMode mm.Output = output mm.Store = store diff --git a/gnovm/pkg/gnolang/nodes.go b/gnovm/pkg/gnolang/nodes.go index f85cfc03d25..4e67906f150 100644 --- a/gnovm/pkg/gnolang/nodes.go +++ b/gnovm/pkg/gnolang/nodes.go @@ -2422,8 +2422,10 @@ func validatePkgName(name string) error { // The distinction is used for validation to work // both before and after preprocessing. -const missingResultNamePrefix = ".res." // if there was no name -const underscoreResultNamePrefix = ".res_" // if was underscore +const ( + missingResultNamePrefix = ".res." // if there was no name + underscoreResultNamePrefix = ".res_" // if was underscore +) func isUnnamedResult(name string) bool { return isMissingResult(name) || isUnderscoreResult(name) diff --git a/gnovm/pkg/gnolang/ownership.go b/gnovm/pkg/gnolang/ownership.go index 752c944098f..0e577421f9a 100644 --- a/gnovm/pkg/gnolang/ownership.go +++ b/gnovm/pkg/gnolang/ownership.go @@ -122,6 +122,9 @@ type Object interface { SetIsNewDeleted(bool) GetIsTransient() bool + GetLastGCCycle() int64 + SetLastGCCycle(int64) + // Saves to realm along the way if owned, and also (dirty // or new). // ValueImage(rlm *Realm, owned bool) *ValueImage @@ -162,9 +165,8 @@ type ObjectInfo struct { // Object is marked for deletion in current transaction isNewDeleted bool - - // XXX huh? - owner Object // mem reference to owner. + lastGCCycle int64 + owner Object // mem reference to owner. } // Copy used for serialization of objects. @@ -182,6 +184,7 @@ func (oi *ObjectInfo) Copy() ObjectInfo { isNewReal: oi.isNewReal, isNewEscaped: oi.isNewEscaped, isNewDeleted: oi.isNewDeleted, + lastGCCycle: oi.lastGCCycle, } } @@ -344,6 +347,14 @@ func (oi *ObjectInfo) SetIsNewDeleted(x bool) { oi.isNewDeleted = x } +func (oi *ObjectInfo) GetLastGCCycle() int64 { + return oi.lastGCCycle +} + +func (oi *ObjectInfo) SetLastGCCycle(c int64) { + oi.lastGCCycle = c +} + func (oi *ObjectInfo) GetIsTransient() bool { return false } diff --git a/gnovm/pkg/gnolang/preprocess.go b/gnovm/pkg/gnolang/preprocess.go index a009a8a24a6..9f406b7cc09 100644 --- a/gnovm/pkg/gnolang/preprocess.go +++ b/gnovm/pkg/gnolang/preprocess.go @@ -451,7 +451,7 @@ func Preprocess(store Store, ctx BlockNode, n Node) Node { // XXX well the following may be isn't idempotent, // XXX so it is currently strange. if bn, ok := n.(BlockNode); ok { - //findGotoLoopDefines(ctx, bn) + // findGotoLoopDefines(ctx, bn) findHeapDefinesByUse(ctx, bn) findHeapUsesDemoteDefines(ctx, bn) findPackageSelectors(bn) @@ -2893,7 +2893,7 @@ func addHeapCapture(dbn BlockNode, fle *FuncLitExpr, depth int, nx *NameExpr) (i // vp := fle.GetPathForName(nil, name) vp := nx.Path vp.SetDepth(vp.Depth - uint8(depth)) - //vp.SetDepth(vp.Depth - 1) // minus 1 for fle itself. + // vp.SetDepth(vp.Depth - 1) // minus 1 for fle itself. ne := NameExpr{ Path: vp, Name: name, diff --git a/gnovm/pkg/gnolang/realm.go b/gnovm/pkg/gnolang/realm.go index 5bd4d735907..ef8b607fba9 100644 --- a/gnovm/pkg/gnolang/realm.go +++ b/gnovm/pkg/gnolang/realm.go @@ -558,7 +558,7 @@ func (rlm *Realm) processNewEscapedMarks(store Store, start int) int { // (and never can be unescaped,) // except for new-reals that get demoted // because ref-count isn't >= 2. - //for _, eo := range rlm.newEscaped[start:] { + // for _, eo := range rlm.newEscaped[start:] { for i := 0; i < len(rlm.newEscaped[start:]); i++ { // may expand. eo := rlm.newEscaped[i] if debug { @@ -1528,7 +1528,7 @@ func ensureUniq(oozz ...[]Object) { for _, ooz := range oozz { for _, uo := range ooz { if _, ok := om[uo]; ok { - panic("duplicate object") + panic(fmt.Sprintf("duplicate object: %v", uo)) } else { om[uo] = struct{}{} } diff --git a/gnovm/pkg/gnolang/string_methods.go b/gnovm/pkg/gnolang/string_methods.go index 22aad75bbf6..62f78aeebbf 100644 --- a/gnovm/pkg/gnolang/string_methods.go +++ b/gnovm/pkg/gnolang/string_methods.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go"; DO NOT EDIT. +// Code generated by "stringer -type=Kind,Op,TransCtrl,TransField,VPType,Word -output string_methods.go ."; DO NOT EDIT. package gnolang diff --git a/gnovm/pkg/gnolang/values.go b/gnovm/pkg/gnolang/values.go index aae7686d874..93cf70c81db 100644 --- a/gnovm/pkg/gnolang/values.go +++ b/gnovm/pkg/gnolang/values.go @@ -33,6 +33,9 @@ type Value interface { // NOTE must use the return value since PointerValue isn't a pointer // receiver, and RefValue returns another type entirely. DeepFill(store Store) Value + + GetShallowSize() int64 + VisitAssociated(tr Visitor) (stop bool) // for GC } // Fixed size primitive types are represented in TypedValue.N diff --git a/gnovm/pkg/gnolang/values_test.go b/gnovm/pkg/gnolang/values_test.go index ab1829abe7e..5f7ae8f7bca 100644 --- a/gnovm/pkg/gnolang/values_test.go +++ b/gnovm/pkg/gnolang/values_test.go @@ -9,7 +9,9 @@ type mockTypedValueStruct struct { field int } -func (m *mockTypedValueStruct) assertValue() {} +func (m *mockTypedValueStruct) assertValue() {} +func (m *mockTypedValueStruct) GetShallowSize() int64 { return 0 } +func (m *mockTypedValueStruct) VisitAssociated(vis Visitor) (stop bool) { return true } func (m *mockTypedValueStruct) String() string { return fmt.Sprintf("MockTypedValueStruct(%d)", m.field) diff --git a/gnovm/stdlibs/generated.go b/gnovm/stdlibs/generated.go index 902e6996729..fbcaaaa332a 100644 --- a/gnovm/stdlibs/generated.go +++ b/gnovm/stdlibs/generated.go @@ -10,6 +10,7 @@ import ( libs_crypto_ed25519 "github.com/gnolang/gno/gnovm/stdlibs/crypto/ed25519" libs_crypto_sha256 "github.com/gnolang/gno/gnovm/stdlibs/crypto/sha256" libs_math "github.com/gnolang/gno/gnovm/stdlibs/math" + libs_runtime "github.com/gnolang/gno/gnovm/stdlibs/runtime" libs_std "github.com/gnolang/gno/gnovm/stdlibs/std" libs_sys_params "github.com/gnolang/gno/gnovm/stdlibs/sys/params" libs_testing "github.com/gnolang/gno/gnovm/stdlibs/testing" @@ -227,6 +228,38 @@ var nativeFuncs = [...]NativeFunc{ )) }, }, + { + "runtime", + "GC", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{}, + true, + func(m *gno.Machine) { + libs_runtime.GC( + m, + ) + }, + }, + { + "runtime", + "MemStats", + []gno.FieldTypeExpr{}, + []gno.FieldTypeExpr{ + {NameExpr: *gno.Nx("r0"), Type: gno.X("string")}, + }, + true, + func(m *gno.Machine) { + r0 := libs_runtime.MemStats( + m, + ) + + m.PushValue(gno.Go2GnoValue( + m.Alloc, + m.Store, + reflect.ValueOf(&r0).Elem(), + )) + }, + }, { "std", "bankerGetCoins", @@ -1276,6 +1309,7 @@ var initOrder = [...]string{ "net/url", "regexp/syntax", "regexp", + "runtime", "std", "sys/params", "time", diff --git a/gnovm/stdlibs/runtime/runtime.gno b/gnovm/stdlibs/runtime/runtime.gno new file mode 100644 index 00000000000..e68c0840050 --- /dev/null +++ b/gnovm/stdlibs/runtime/runtime.gno @@ -0,0 +1,4 @@ +package runtime + +func GC() +func MemStats() string diff --git a/gnovm/stdlibs/runtime/runtime.go b/gnovm/stdlibs/runtime/runtime.go new file mode 100644 index 00000000000..a38e3d18758 --- /dev/null +++ b/gnovm/stdlibs/runtime/runtime.go @@ -0,0 +1,11 @@ +package runtime + +import gno "github.com/gnolang/gno/gnovm/pkg/gnolang" + +func GC(m *gno.Machine) { + m.GarbageCollect() +} + +func MemStats(m *gno.Machine) string { + return m.Alloc.MemStats() +} diff --git a/gnovm/tests/files/alloc_0.gno b/gnovm/tests/files/alloc_0.gno new file mode 100644 index 00000000000..084a9564d2b --- /dev/null +++ b/gnovm/tests/files/alloc_0.gno @@ -0,0 +1,19 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +type Foo struct { + name string +} + +var f = Foo{name: "foo"} + +func main() { + f1 := f + runtime.GC() + println("MemStats: ", runtime.MemStats()) +} + +// Output: +// MemStats: Allocator{maxBytes:100000000, bytes:5030} diff --git a/gnovm/tests/files/alloc_1.gno b/gnovm/tests/files/alloc_1.gno new file mode 100644 index 00000000000..b3bfa6a2c60 --- /dev/null +++ b/gnovm/tests/files/alloc_1.gno @@ -0,0 +1,24 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +type Bar struct { + name string +} + +type Foo struct { + name string + Bar +} + +var S = []*Foo{&Foo{name: "foo1", Bar: Bar{name: "bar1"}}, &Foo{name: "foo2", Bar: Bar{name: "bar2"}}} + +func main() { + S1 := S + runtime.GC() + println("MemStats: ", runtime.MemStats()) +} + +// Output: +// MemStats: Allocator{maxBytes:100000000, bytes:6072} diff --git a/gnovm/tests/files/alloc_2.gno b/gnovm/tests/files/alloc_2.gno new file mode 100644 index 00000000000..2d422deb640 --- /dev/null +++ b/gnovm/tests/files/alloc_2.gno @@ -0,0 +1,13 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +// GC does not reclaim enough memory +func main() { + data := make([]byte, 100*1024*1024) // > 100000000 + println("MemStats: ", runtime.MemStats()) +} + +// Error: +// allocation limit exceeded diff --git a/gnovm/tests/files/alloc_3.gno b/gnovm/tests/files/alloc_3.gno new file mode 100644 index 00000000000..5fa3094b752 --- /dev/null +++ b/gnovm/tests/files/alloc_3.gno @@ -0,0 +1,14 @@ +// MAXALLOC: 110000000 +package main + +import "runtime" + +func main() { + data := make([]byte, 100*1024*1024) // < 110000000 + data = nil + runtime.GC() + println("MemStats after GC: ", runtime.MemStats()) +} + +// Output: +// MemStats after GC: Allocator{maxBytes:110000000, bytes:4472} diff --git a/gnovm/tests/files/alloc_4.gno b/gnovm/tests/files/alloc_4.gno new file mode 100644 index 00000000000..28598a59965 --- /dev/null +++ b/gnovm/tests/files/alloc_4.gno @@ -0,0 +1,26 @@ +// MAXALLOC: 50000 +package main + +import "runtime" + +var s = "hello" + +func xyz() { + for i := 0; i < 110; i++ { + s += "world!!!" + } +} +func main() { + xyz() // first gc happens in xyz(), and m.Package is main + println("memstats in main after first GC: ", runtime.MemStats()) + + // first GC happens in xyz(), then more allocations + // happens. + // after second gc, more garbage collected. + runtime.GC() + println("memstats in main after second GC: ", runtime.MemStats()) +} + +// Output: +// memstats in main after first GC: Allocator{maxBytes:50000, bytes:12653} +// memstats in main after second GC: Allocator{maxBytes:50000, bytes:5381} diff --git a/gnovm/tests/files/alloc_5.gno b/gnovm/tests/files/alloc_5.gno new file mode 100644 index 00000000000..bc0b8a70f5e --- /dev/null +++ b/gnovm/tests/files/alloc_5.gno @@ -0,0 +1,22 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +func gen() { + data := make([]byte, 50*1024*1024) +} + +// this is mainly for gas usage check +// see gc.txtar +func main() { + for i := 0; i < 10; i++ { + gen() + gen() + } + runtime.GC() + println("memstats in main after GC: ", runtime.MemStats()) +} + +// Output: +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:4472} diff --git a/gnovm/tests/files/alloc_6.gno b/gnovm/tests/files/alloc_6.gno new file mode 100644 index 00000000000..b25d7d0f0b2 --- /dev/null +++ b/gnovm/tests/files/alloc_6.gno @@ -0,0 +1,16 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +func main() { + var a = func() int { + return 1 + } + + runtime.GC() + println("memstats in main after GC: ", runtime.MemStats()) +} + +// Output: +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:4700} diff --git a/gnovm/tests/files/alloc_6a.gno b/gnovm/tests/files/alloc_6a.gno new file mode 100644 index 00000000000..8605e3106f0 --- /dev/null +++ b/gnovm/tests/files/alloc_6a.gno @@ -0,0 +1,18 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +func main() { + { + var a = func() int { + return 1 + } + runtime.GC() + } + + println("memstats in main after GC: ", runtime.MemStats()) +} + +// Output: +// memstats in main after GC: Allocator{maxBytes:100000000, bytes:5212} diff --git a/gnovm/tests/files/alloc_7.gno b/gnovm/tests/files/alloc_7.gno new file mode 100644 index 00000000000..897619b047c --- /dev/null +++ b/gnovm/tests/files/alloc_7.gno @@ -0,0 +1,16 @@ +// MAXALLOC: 100000000 +package main + +import "runtime" + +var s = "hello world" + +func main() { + s += "!" // underlying array re-allocated + s1 := s // another string value, underlying array same + runtime.GC() + println("MemStats: ", runtime.MemStats()) +} + +// Output: +// MemStats: Allocator{maxBytes:100000000, bytes:4664}