blob: f89c000b02371808960fbd65b38bed4493ebc017 [file]
#!/usr/bin/env python3
############################################################################
# Copyright (C) SchedMD LLC.
############################################################################
"""
Generate a controlled memory-pressure curve: peak, valley, peak, ...
Each iteration prints `[prefix] <VmHWM> <VmRSS>` (kB) to stdout.
Progress markers "Allocating <MIB_PEAK> MiB..." and "Done." go to stderr.
"""
import argparse
import mmap
import sys
import time
def read_proc_status(key):
key = key.lower()
with open("/proc/self/status", "r") as status:
for line in status:
name, _, value = line.partition(":")
if name.lower() == key:
return int(value.split()[0])
raise RuntimeError(f"missing field in /proc/self/status: {key}")
def allocate_and_touch(mib):
"""Allocate `mib` MiB of anonymous memory and touch every page so it
becomes resident. Returns the mmap, or None when mib <= 0.
"""
if mib <= 0:
return None
nbytes = mib * 1024 * 1024
mapping = mmap.mmap(-1, nbytes)
for offset in range(0, nbytes, mmap.PAGESIZE):
mapping[offset] = 1
return mapping
def release(mapping):
if mapping is not None:
mapping.close()
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"mem_peak",
type=int,
metavar="MIB_PEAK",
help="MiB to allocate during the peak phase",
)
parser.add_argument(
"time_mem_peak",
type=float,
nargs="?",
default=5.0,
metavar="TIME_MEM_PEAK",
help="seconds to hold the peak (default: 5.0)",
)
parser.add_argument(
"--time-mem-valley",
type=float,
default=0.0,
help="seconds to hold the valley after release (default: 0 = no valley phase)",
)
parser.add_argument(
"--mem-valley",
type=int,
default=0,
help="MiB to hold during the valley phase (default: 0 = full release)",
)
parser.add_argument(
"--iterations",
type=int,
default=1,
help="number of peak-valley cycles to run (default: 1)",
)
parser.add_argument(
"--prefix",
default="",
help="string emitted before the numbers on each stdout report line "
"(default: empty = numbers only)",
)
args = parser.parse_args()
if args.mem_peak <= 0:
parser.error("MIB_PEAK must be greater than 0")
if args.iterations < 1:
parser.error("--iterations must be at least 1")
if args.time_mem_peak < 0:
parser.error("TIME_MEM_PEAK cannot be negative")
if args.time_mem_valley < 0:
parser.error("--time-mem-valley cannot be negative")
if args.mem_valley < 0:
parser.error("--mem-valley cannot be negative")
print(f"Allocating {args.mem_peak} MiB...", file=sys.stderr, flush=True)
for _ in range(args.iterations):
peak_mapping = allocate_and_touch(args.mem_peak)
if args.time_mem_peak > 0:
time.sleep(args.time_mem_peak)
release(peak_mapping)
if args.time_mem_valley > 0:
valley_mapping = allocate_and_touch(args.mem_valley)
time.sleep(args.time_mem_valley)
release(valley_mapping)
# VmHWM is the monotonic high-water peak (unchanged across iterations
# for a fixed mem_peak); VmRSS is the current resident set after release.
fields = []
if args.prefix:
fields.append(args.prefix)
fields.append(read_proc_status("VmHWM"))
fields.append(read_proc_status("VmRSS"))
print(*fields, flush=True)
print("Done.", file=sys.stderr, flush=True)
if __name__ == "__main__":
main()