Skip to content

Commit

Permalink
Cleaning up.
Browse files Browse the repository at this point in the history
  • Loading branch information
strategicpause committed Sep 10, 2023
1 parent b20fdbc commit bce581e
Show file tree
Hide file tree
Showing 10 changed files with 278 additions and 73 deletions.
8 changes: 3 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
FROM golang:latest as build

WORKDIR /src
COPY go.mod /src/go.mod
COPY main.go /src/main.go
COPY . /src

RUN go build -o /memory-leak
RUN go build -o /leak

FROM busybox:latest
COPY --from=build /memory-leak /memory-leak
CMD ["/memory-leak"]
COPY --from=build /leak /leak
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,30 @@ podman build . -t memory-leak
~~~

## Run
Locally
~~~
$ podman run --memory 500m --name=memory-leak --rm --memory-swap=0 localhost/memory-leak
go run main.go memory --max-memory 1GB --block-size 100MB --pause=1s
~~~

Via Podman
~~~
$ podman run \
--memory=1024m \
--name=memory-leak \
--rm \
--memory-swap=0 \
localhost/memory-leak \
./leak memory --max-memory 1GB --block-size 100MB --pause=1s
~~~
By default Podman will enable swap memory with a size equal to the memory limit specified.
This will turn off swap to make sure OOMs can be recreated at the expected memory limit.

TCP Leak
~~~
$ podman run \
--memory=1024m \
--name=tcp-leak \
--rm \
localhost/memory-leak \
./leak tcp
~~~
By default Podman will enable swap memory with a size equal to the memory limit specified.
This will turn off swap to make sure OOMs can be recreated at the expected memory limit.
38 changes: 38 additions & 0 deletions command/memory/leak.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package memory

import (
"fmt"
"github.com/strategicpause/memory-leak/metrics"
"time"
)

type Params struct {
MaxMemoryInBytes uint64
BlockSizeInBytes uint64
PauseTimeInSeconds time.Duration
}

func memoryLeak(params *Params) error {
PrintParams(params)

numEntries := int(params.MaxMemoryInBytes / params.BlockSizeInBytes)
list := make([][]byte, numEntries)

for i := 0; i < numEntries; i++ {
list[i] = make([]byte, params.BlockSizeInBytes)
for j := 0; j < int(params.BlockSizeInBytes); j++ {
list[i][j] = 0
}
metrics.PrintMemory()
time.Sleep(params.PauseTimeInSeconds)
}

fmt.Println("Done")

return nil
}

func PrintParams(params *Params) {
fmt.Printf("MaxMemory = %v MiB\tBlockSize = %v MiB\tPauseTime = %v.\n",
metrics.BToMiB(params.MaxMemoryInBytes), metrics.BToMiB(params.BlockSizeInBytes), params.PauseTimeInSeconds)
}
64 changes: 64 additions & 0 deletions command/memory/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package memory

import (
"errors"
"github.com/inhies/go-bytesize"
"github.com/urfave/cli"
)

const MaxMemoryName = "max-memory"
const BlockSizeName = "block-size"
const PauseDurationName = "pause"

func Register() cli.Command {
return cli.Command{
Name: "memory",
Usage: "Reproduces a memory leak.",
Action: action,
Flags: flags(),
}
}

func flags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: MaxMemoryName,
Usage: "Specify the maximum amount of memory to acquire.",
Value: "1GB",
},
cli.StringFlag{
Name: BlockSizeName,
Usage: "Specify the block size of memory which will be allocated at any given time.",
Value: "10MB",
},
cli.DurationFlag{
Name: PauseDurationName,
Usage: "Time between allocations in seconds.",
Value: 1,
},
}
}

func action(ctx *cli.Context) error {
maxMemory, err := bytesize.Parse(ctx.String(MaxMemoryName))
if err != nil {
return err
}

blockSize, err := bytesize.Parse(ctx.String(BlockSizeName))
if err != nil {
return err
}

if blockSize > maxMemory {
return errors.New("block-size must be less than or equal to max-memory")
}

params := &Params{
MaxMemoryInBytes: uint64(maxMemory),
BlockSizeInBytes: uint64(blockSize),
PauseTimeInSeconds: ctx.Duration(PauseDurationName),
}

return memoryLeak(params)
}
33 changes: 33 additions & 0 deletions command/tcp/leak.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package tcp

import (
"fmt"
"github.com/strategicpause/memory-leak/metrics"
"net/http"
"time"
)

func tcpLeak() error {
go func() {
fmt.Println("Listening on port", Port)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(time.Now().String()))
})
_ = http.ListenAndServe(Port, nil)
}()
for {
go func() {
req := Must(http.NewRequest("GET", "http://localhost:8080/", nil))
client := http.Client{}
Must(client.Do(req))
metrics.PrintMemory()
}()
}
}

func Must[T any](obj T, err error) T {
if err != nil {
panic(err)
}
return obj
}
24 changes: 24 additions & 0 deletions command/tcp/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package tcp

import (
"github.com/urfave/cli"
)

const Port = ":8080"

func Register() cli.Command {
return cli.Command{
Name: "tcp",
Usage: "Reproduces a TCP socket leak.",
Action: action,
Flags: flags(),
}
}

func flags() []cli.Flag {
return []cli.Flag{}
}

func action(_ *cli.Context) error {
return tcpLeak()
}
12 changes: 11 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
module memory-leak
module github.com/strategicpause/memory-leak

go 1.19

require (
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf
github.com/urfave/cli v1.22.14
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
)
26 changes: 26 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s=
github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk=
github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
77 changes: 13 additions & 64 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,77 +1,26 @@
package main

import (
"fmt"
"net/http"
"runtime"
"sync"
"time"
)

const (
MiB = 1024 * 1024
Port = ":8080"
TcpLeak = true
"github.com/strategicpause/memory-leak/command/memory"
"github.com/strategicpause/memory-leak/command/tcp"
"github.com/urfave/cli"
"log"
"os"
)

func main() {
if TcpLeak {
tcpLeak()
} else {
memoryLeak()
app := &cli.App{
Commands: RegisterCommands(),
}

}

func tcpLeak() {
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
fmt.Println("Listening on port", Port)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(time.Now().String()))
})
http.ListenAndServe(Port, nil)
wg.Done()
}()
for {
go func() {
req := Must(http.NewRequest("GET", "http://localhost:8080/", nil))
client := http.Client{}
Must(client.Do(req))
PrintMemory()
}()
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
wg.Wait()
}

func Must[T any](obj T, err error) T {
if err != nil {
panic(err)
func RegisterCommands() cli.Commands {
return cli.Commands{
memory.Register(),
tcp.Register(),
}
return obj
}

func memoryLeak() {
// Allocate 10 GiB in 100 MiB increments
list := make([][]byte, 100)
for i := 0; i < 100; i++ {
list[i] = make([]byte, 100*MiB)
for j := 0; j < 100*MiB; j++ {
list[i][j] = 0
}
PrintMemory()
time.Sleep(time.Second)
}
fmt.Println("Done")
}

func PrintMemory() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("TotalAlloc = %v MiB\tSys = %v MiB\n", bToMiB(m.TotalAlloc), bToMiB(m.Sys))
}

func bToMiB(b uint64) uint64 {
return b / 1024 / 1024
}
41 changes: 41 additions & 0 deletions metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package metrics

import (
"fmt"
"runtime"
)

func PrintMemory() {
var m runtime.MemStats
runtime.ReadMemStats(&m)

var avgGCTime uint64
if m.NumGC > 0 {
avgGCTime = NsToUs(m.PauseTotalNs / uint64(m.NumGC))
}

fmt.Printf("TotalAlloc = %v MiB\tHeapAlloc = %v MiB\tSys = %v MiB\tNextGC = %v MiB\tPauseTotalUs = %v\tNumGC = %v\tAvgGCTime = %v\n",
// Total number of bytes allocated for the heap. Does not decrease when objects are freed.
BToMiB(m.TotalAlloc),
// Total number of bytes allocated on the heap. This includes reachable objects and objects
// not yet freed by the GC. This value should decrease when objects are cleaned by the GC.
BToMiB(m.HeapAlloc),
// The total number of bytes obtained from the OS.
BToMiB(m.Sys),
// The target heap size of the next GC cycle. The GC goal is to kep HeapAlloc <= NextGC.
BToMiB(m.NextGC),
// The cumulative nanoseconds in GC stop-the-world pauses since the program started.
NsToUs(m.PauseTotalNs),
// Number of completed GC cycles.
m.NumGC,
// Average time spent in GC across. Pretty hacky way to get some idea of how GC time is being spent during the leak.
avgGCTime)
}

func BToMiB[T uint64 | uint32](b T) uint64 {
return uint64(b) / (1024 * 1024)
}

func NsToUs(s uint64) uint64 {
return s / 1000
}

0 comments on commit bce581e

Please sign in to comment.