Skip to content

Commit 77b5c46

Browse files
committed
add memhog
1 parent 6c14e9d commit 77b5c46

File tree

6 files changed

+199
-0
lines changed

6 files changed

+199
-0
lines changed

go/memhog/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/memhog

go/memhog/Dockerfile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# syntax=docker/dockerfile:1.4
2+
ARG GO_VERSION=1.24
3+
FROM --platform=$BUILDPLATFORM golang:$GO_VERSION AS builder
4+
WORKDIR /src
5+
COPY go.mod go.sum ./
6+
RUN go mod download
7+
COPY . .
8+
ARG TARGETOS
9+
ARG TARGETARCH
10+
RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -o /memhog .
11+
FROM --platform=$TARGETPLATFORM scratch AS final
12+
WORKDIR /
13+
COPY --from=builder /memhog .
14+
ENTRYPOINT ["/memhog"]
15+

go/memhog/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# memhog
2+
3+
A small go program to allocate a bunch of RAM and sit on it for a while.
4+
5+
It writes only one byte per page to force the OS to allocate the page without
6+
taking so long to fill it all.
7+
8+
Optionally asks the go runtime to release memory back to the OS after a while,
9+
demonstrating how this affects memstats.
10+
11+
This is a dirty hack and the code is wretched. Don't use it as an example for
12+
anything except, possibly, how not to do things.

go/memhog/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/ringerc/scrapcode/go/memhog
2+
3+
go 1.24.1
4+
5+
require github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500

go/memhog/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4=
2+
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M=

go/memhog/memhog.go

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* If you're reading this wretched code, I'm sorry
3+
*/
4+
package main
5+
6+
import (
7+
"context"
8+
"flag"
9+
"fmt"
10+
"log"
11+
"os"
12+
"os/signal"
13+
"runtime"
14+
"runtime/debug"
15+
"sync"
16+
"syscall"
17+
"time"
18+
19+
"github.com/c2h5oh/datasize"
20+
)
21+
22+
type Config struct {
23+
sizeBytes uint64
24+
nThreads uint64
25+
sleepSecondsBeforeFree int
26+
sleepSecondsBeforeExit int
27+
statsIntervalSeconds int
28+
}
29+
30+
func printMemStats() {
31+
var m runtime.MemStats
32+
runtime.ReadMemStats(&m)
33+
fmt.Printf("\n")
34+
fmt.Printf("%50s: %12d bytes\n", "Currently allocated heap memory (HeapAlloc)", m.HeapAlloc)
35+
fmt.Printf("%50s: %12d bytes\n", "Idle heap memory (HeapAlloc)", m.HeapIdle)
36+
fmt.Printf("%50s: %12d bytes\n", "Runtime memory released to OS (HeapReleased)", m.HeapReleased)
37+
fmt.Printf("%50s: %12d bytes\n", "Idle non-released memory (HeapAlloc-HeapReleased)", m.HeapIdle-m.HeapReleased)
38+
fmt.Printf("%50s: %12d bytes\n", "Cumulative allocated memory (TotalAlloc)", m.TotalAlloc)
39+
fmt.Printf("%50s: %12d bytes\n", "Runtime memory allocated high-water mark (Sys)", m.Sys)
40+
fmt.Printf("%50s: %12d bytes\n", "Runtime memory still allocated (Sys-HeapReleased)", m.Sys-m.HeapReleased)
41+
}
42+
43+
func main() {
44+
var size string
45+
var nThreads int
46+
var config Config
47+
flag.StringVar(&size, "size", "1G", "Size in bytes with optional SI suffix")
48+
flag.IntVar(&nThreads, "threads", 10, "number of writer threads")
49+
flag.IntVar(&config.sleepSecondsBeforeFree, "sleep-seconds-before-free", -1, "Number of seconds to sleep after allocation before attempting to release memory. -1 means never release, 0 means no sleep.")
50+
flag.IntVar(&config.sleepSecondsBeforeExit, "sleep-seconds-before-exit", -1, "Number of seconds to sleep before exiting. -1 means never exit, 0 means exit immediately.")
51+
flag.IntVar(&config.statsIntervalSeconds, "stats-interval-seconds", -1, "Seconds to sleep between emitting memory stats during pre-exit sleep, or -1 to only print once")
52+
flag.Parse()
53+
54+
var parsedSize datasize.ByteSize
55+
err := parsedSize.UnmarshalText([]byte(size))
56+
if err != nil {
57+
log.Fatalf("parsing size failed: %v", err)
58+
}
59+
config.sizeBytes = parsedSize.Bytes()
60+
61+
if nThreads <= 0 {
62+
log.Fatalf("Invalid nthreads: %d", nThreads)
63+
}
64+
config.nThreads = uint64(nThreads)
65+
66+
// We only need to write into each page, not fill each page, unless
67+
// there is some kind of memory compression in use. This makes filling
68+
// lots of memory faster.
69+
fmt.Printf("Detecting platform page size... ")
70+
pageSize := uint64(syscall.Getpagesize())
71+
fmt.Printf("%v bytes\n", pageSize)
72+
73+
fmt.Printf("Attempting to allocate %s...\n", parsedSize.HumanReadable())
74+
75+
data := make([][]byte, config.nThreads)
76+
threadBytes := config.sizeBytes / config.nThreads
77+
var wg sync.WaitGroup
78+
79+
writerFunc := func(threadno int) {
80+
data[threadno] = make([]byte, threadBytes)
81+
for i := uint64(0); i < threadBytes; i += pageSize {
82+
data[threadno][i] = byte(i % 256)
83+
}
84+
wg.Done()
85+
}
86+
wg.Add(int(config.nThreads))
87+
for n := range config.nThreads {
88+
go writerFunc(int(n))
89+
}
90+
wg.Wait()
91+
92+
fmt.Println("Memory allocated and filled.")
93+
printMemStats()
94+
95+
// Print memory statistics to observe the heap size
96+
97+
if config.sleepSecondsBeforeFree >= 0 {
98+
fmt.Printf("\nSleeping %v seconds before freeing memory...", config.sleepSecondsBeforeFree)
99+
time.Sleep(time.Duration(int64(config.sleepSecondsBeforeFree) * int64(time.Second)))
100+
fmt.Printf(" done\n")
101+
102+
fmt.Printf("\nTrying to release memory...")
103+
data = make([][]byte, 0)
104+
// Force aggressive GC
105+
debug.SetMemoryLimit(0)
106+
runtime.GC()
107+
// This might take time
108+
debug.FreeOSMemory()
109+
fmt.Printf(" done\n\n")
110+
111+
printMemStats()
112+
}
113+
114+
// Because k8s doesn't have swap, it can't page out our unused memory
115+
// to relieve memory pressure. It's safe to allocate and fill the memory
116+
// then sleep indefinitely, there is no need to continually read it or
117+
// write to it like there would be on a sensible Linux system with swap.
118+
119+
sleepContext := context.Background()
120+
if config.sleepSecondsBeforeExit >= 0 {
121+
sleepContext, _ = context.WithTimeout(sleepContext, time.Duration(int64(config.sleepSecondsBeforeExit)*int64(time.Second)))
122+
}
123+
124+
var statsTicker *time.Ticker
125+
if config.statsIntervalSeconds > 0 {
126+
statsTicker = time.NewTicker(time.Duration(int64(config.statsIntervalSeconds) * int64(time.Second)))
127+
} else {
128+
// Fake ticker that never fires, because waiting on a nil timer
129+
// is not legal
130+
statsTicker = &time.Ticker{
131+
C: make(chan time.Time),
132+
}
133+
}
134+
135+
// To stop go complaining about a deadlock when we intentionally sleep forever
136+
// if there's no exit timer, explicitly wait for SIGINT
137+
sigs := make(chan os.Signal, 1)
138+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
139+
140+
for {
141+
select {
142+
case <-statsTicker.C:
143+
printMemStats()
144+
case <-sleepContext.Done():
145+
fmt.Printf("Deadline expired\n")
146+
os.Exit(0)
147+
case sig := <-sigs:
148+
fmt.Printf("Exiting: %v\n", sig)
149+
os.Exit(0)
150+
}
151+
}
152+
153+
if config.sleepSecondsBeforeExit >= 0 {
154+
fmt.Printf("\nSleeping %v seconds before exit...", config.sleepSecondsBeforeExit)
155+
time.Sleep(1)
156+
fmt.Printf(" done\n")
157+
} else {
158+
fmt.Printf("\nSleeping until ^C...\n")
159+
for {
160+
time.Sleep(1 * time.Second)
161+
printMemStats()
162+
}
163+
}
164+
}

0 commit comments

Comments
 (0)