Packages

mem

Measure a program's memory use, force garbage collection, write pprof profiles and serve live profiling data.

import "std:mem"

std:mem shows what a program is doing with memory. Reach for it when a long-running program grows, when you want to know what a data structure costs, or when you need a profile to find where the allocations come from. OSL frees memory for you with a garbage collector, so most programs never need this package. It is for the ones that do.

Exact numbers differ between machines and runs, so the examples print comparisons, not the figures themselves.

Memory statistics#

mem.stats() returns a snapshot. The fields you will use most are alloc (bytes in use now), totalAlloc (bytes ever allocated), heapObjects (live objects), numGC (collections so far) and goroutines (running threads):

main.osl
import "std:mem"

auto now = mem.stats()
log now.alloc > 0
log now.heapObjects > 0
log now.goroutines >= 1
log now.sys >= now.alloc
Click the code to edit it

All sizes are in bytes. sys is what the program has asked the operating system for, which is always at least what is in use.

Measuring what something costs#

Take a snapshot before and after, and compare. mem.gc() runs a collection first and then returns fresh statistics, so garbage from earlier work doesn't blur the reading:

main.osl
import "std:mem"

auto before = mem.gc()

int[] numbers = []
for i in 1 to 200000 (
  numbers.append(i)
)

auto after = mem.gc()
log numbers.len
log after.alloc > before.alloc
log after.numGC > before.numGC
log (after.alloc - before.alloc) > 1000000
Click the code to edit it

The array holds 200000 numbers, so it takes well over a million bytes. Keep a reference to the data until after the second reading, or the collector may free it before you measure.

Watching for growth#

A leak is memory that keeps growing because something still refers to it. Measure after a collection, at intervals, and look at the trend, not one number:

main.osl
import "std:mem"

string[] history = []

def remember(string line) (
  history.append(line)
)

auto start = mem.gc()
for round in 1 to 3 (
  for i in 1 to 50000 (
    remember("entry " ++ i)
  )
  auto now = mem.gc()
  log "round" round "grew:" now.alloc > start.alloc
)
log history.len
Click the code to edit it

A program that logs "grew: true" forever when it should be steady is keeping something it doesn't need, such as a list or a cache with no limit. cache has a capacity for this reason.

Writing a profile#

mem.dump(path) writes a pprof profile, which Go's go tool pprof can open to show where memory was allocated. The default is a heap profile, and the profile option picks others: allocs, goroutine, threadcreate, block or mutex:

main.osl
import "std:mem"
import "std:fs"

fs.remove("heap.pprof", {force: true}).unwrap()
mem.dump("heap.pprof").unwrap()
log fs.size("heap.pprof").unwrap() > 0

mem.dump("threads.pprof", {profile: "goroutine"}).unwrap()
log fs.exists("threads.pprof")

log mem.dump("no-such-folder/heap.pprof").unwrapErr().kind

fs.remove("heap.pprof", {force: true}).unwrap()
fs.remove("threads.pprof", {force: true}).unwrap()
Click the code to edit it

The file is replaced atomically, so a profile is never half written. Then, in a shell, go tool pprof heap.pprof opens it. A heap profile samples allocations, so call mem.setProfileRate early, before the allocations you want to see, to change how often it samples.

Live profiling#

mem.serve(address) starts a profiling server in the background and returns a handle. Profiles are under /debug/pprof/, where go tool pprof http://host/debug/pprof/heap can read them from a running program. Use 127.0.0.1 so only this machine can reach it, and port 0 for a free one:

main.osl
import "std:mem"
import "std:requests"

auto profiler = mem.serve("127.0.0.1:0").unwrap()
log profiler.address().startsWith("127.0.0.1:")

string url = "http://" ++ profiler.address() ++ "/debug/pprof/goroutine?debug=1"
auto reply = requests.get(url).unwrap()
log reply.status reply.body.startsWith("goroutine profile")

profiler.close()
Click the code to edit it

Tuning the collector#

Two settings change how eagerly the collector runs, and both return the previous value so you can put it back. setGCPercent is how far the heap may grow past the live data before the next collection (100 by default), and setMemoryLimit is a soft limit in bytes where the collector works harder near the limit. A limit of 0 means none:

main.osl
import "std:mem"

int old = mem.setGCPercent(200)
log old
log mem.setGCPercent(old)

log mem.setMemoryLimit(512 * 1024 * 1024)
log mem.setMemoryLimit(0)
Click the code to edit it

A higher percentage collects less often and uses more memory, which suits a program with a small live heap that allocates a lot. A negative limit or profile rate throws a ValueError.

Quick check

Why does the page call mem.gc() before taking a measurement?

Your turn

Did it grow?

Measure alloc after a collection, build a list of 100000 numbers, measure again after a collection, and print whether it grew. Keep using the list after the second reading.

main.osl
Ctrl Enter runs
Expected output
true
100000

API reference#

13 members, generated from the std:mem source of OSL 9.3.0.

Functions

#mem.address() string

Deprecated. Deprecated: use the Server from mem.serve(address) and server.address() instead.

address returns the deprecated package-level server's address, or "".

#mem.dump(string path, object? options) result<unit, error>

dump writes a pprof profile to path, replacing the file atomically, for go tool pprof. Options: profile (default "heap") names the profile: "heap", "allocs", "goroutine", "threadcreate", "block" or "mutex". A heap or allocs profile runs a garbage collection first. Fails with NotFoundError when the directory doesn't exist, PermissionError or IOError. Throws ValueError for an unknown profile.

#mem.dumpProfile(any name, any path) boolean

Deprecated. Deprecated: use mem.dump(path, {profile: name}) instead.

dumpProfile writes a named pprof profile and reports whether it worked.

#mem.gc() {alloc: int, frees: int, gcCPUFraction: number, gcSys: int, goroutines: int, heapAlloc: int, heapIdle: int, heapInuse: int, heapObjects: int, heapReleased: int, heapSys: int, liveObjects: int, mallocs: int, mcacheInuse: int, mspanInuse: int, nextGC: int, numGC: int, otherSys: int, pauseTotalNs: int, profileRate: int, stackInuse: int, stackSys: int, sys: int, totalAlloc: int}

gc runs a garbage collection and then returns mem.stats().

#mem.profileRate() int

profileRate returns how many bytes are allocated, on average, between samples in the heap profile; 0 means heap profiling is off.

#mem.serve(string address) result<*mem.Server, error>

serve starts a pprof HTTP server in the background on address (such as "127.0.0.1:6060", or ":0" for any free port) and returns its Server handle; profiles are under /debug/pprof/. Anyone who can reach the address can read them, so prefer a loopback address. Fails with NetworkError when the address can't be listened on.

#mem.setGCPercent(int percent) int

setGCPercent sets how much the heap may grow past the live data from the last garbage collection before the next one starts, as a percentage (Go's GOGC), and returns the previous setting. The default is 100, or the GOGC environment variable. A larger value collects less often and uses more memory, which suits a program with a small live heap that allocates a lot. A negative value turns garbage collection off, though mem.setMemoryLimit can still trigger it.

#mem.setMemoryLimit(int bytes) int

setMemoryLimit sets a soft limit in bytes on the memory the Go runtime uses (Go's GOMEMLIMIT), collecting garbage more often as it nears the limit, and returns the previous limit. Pass 0 for no limit; a previous value of 0 means there was none, so the result can be passed back to restore it. Throws ValueError for a negative limit.

#mem.setProfileRate(int bytes) int

setProfileRate sets the heap profile sampling rate in bytes (0 turns it off; 1 records every allocation) and returns the previous rate. Set it early, before the allocations to profile. Throws ValueError for a negative rate.

#mem.stats() {alloc: int, frees: int, gcCPUFraction: number, gcSys: int, goroutines: int, heapAlloc: int, heapIdle: int, heapInuse: int, heapObjects: int, heapReleased: int, heapSys: int, liveObjects: int, mallocs: int, mcacheInuse: int, mspanInuse: int, nextGC: int, numGC: int, otherSys: int, pauseTotalNs: int, profileRate: int, stackInuse: int, stackSys: int, sys: int, totalAlloc: int}

stats returns a snapshot of memory statistics with the keys alloc, totalAlloc, sys, mallocs, frees, liveObjects, heapAlloc, heapSys, heapIdle, heapInuse, heapReleased, heapObjects, stackInuse, stackSys, mspanInuse, mcacheInuse, gcSys, otherSys, nextGC (sizes in bytes), numGC, pauseTotalNs (nanoseconds), gcCPUFraction (0 to 1), goroutines and profileRate.

#mem.stop() boolean

Deprecated. Deprecated: use the Server from mem.serve(address) and server.close() instead.

stop stops the server started by the deprecated package-level serve and reports whether one was running.

*mem.Server

#server.address() string

address returns the address the server listens on, with the real port when it was started on port 0.

#server.close()

close stops the server and frees its port. Closing again does nothing.