| #!/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() |