Skip to content

Commit

Permalink
feat(cli): add -r/--record and -s/--sample
Browse files Browse the repository at this point in the history
On my machine, the CPU usage with 25 ms sampling
is over 50% of one core.
This kind of load on the CPU
(and the corresponding battery drain on mobile devices)
is hard to justify as a default.
It is better to let the user determine the sampling rate.

Let's choose 200 ms as the new default sampling interval.
This reduces the load to around 10% of a core on the same machine.
memsparkline should still catch most of the same spikes at 200 ms.
In programs that run multiple seconds,
large memory spikes usually seem to last longer and/or repeat.
  • Loading branch information
dbohdan committed Oct 15, 2024
1 parent f8bf4e1 commit 25e846b
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 16 deletions.
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,17 @@ It has been tested on Debian, Ubuntu, FreeBSD, NetBSD, and OpenBSD.
It seems to work on Windows, although Windows support has received little testing.
The sparkline displays incorrectly in the Command Prompt and [ConEmu](https://conemu.github.io/) on Windows 7 with the stock console fonts but correctly on Windows 10 with the font NSimSun.

memsparkline tries to measure run time and to sample memory usage close to every 25 ms.
The interval between record creation (`-w`/`--wait` argument) must be a multiple of 50 ms.
memsparkline has a separate sampling and reporting interval setting.
The sampling interval determines how frequently it measures memory usage.
The reporting interval determines how it prints the memory usage and adds it to history for the `--dump` ooption.
When sampling is more frequent than reporting (the default setting),
memsparkline uses the highest sampled value for each reporting interval

A short sample interval like 25 ms can result in high CPU usage, up to 100% of one CPU core.
To reduce CPU usage, sample less frequently.
The default sample interval of 200 ms results in memsparkline using around 10% of a 2019 x86-64 core on the developer's machine.
A record interval that is shorter than the sample interval
records duplicate samples and should be avoided.


## Installation
Expand Down Expand Up @@ -107,7 +116,7 @@ doas pkg_add py3-psutil

```none
usage: memsparkline [-h] [-v] [-d path] [-l n] [-m fmt] [-n] [-o path] [-q]
[-t fmt] [-w ms]
[-r ms] [-s ms] [-t fmt] [-w ms]
command ...
Track the RAM usage (resident set size) of a process and its descendants in
Expand All @@ -130,10 +139,14 @@ options:
-o path, --output path
output file to append to ("-" for standard error)
-q, --quiet do not print sparklines, only final report
-r ms, --record ms how frequently to record/report memory usage (default:
every 1000 ms)
-s ms, --sample ms how frequently to sample memory usage (default: every
200 ms)
-t fmt, --time-format fmt
format string for run time (default: "%d:%02d:%04.1f")
-w ms, --wait ms how frequently to record memory usage (default: every
1000 ms)
-w ms, --wait ms set sample and record time simultaneously (that both
options override)
```


Expand Down
48 changes: 37 additions & 11 deletions src/memsparkline/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@

__version__ = "0.6.0"

SAMPLE_INTERVAL = 25

DEFAULT_RECORD_INTERVAL = 1000
DEFAULT_SAMPLE_INTERVAL = 200
SPARKLINE_TICKS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
USAGE_DIVISOR = 1 << 20 # Report memory usage in binary megabytes.

Expand All @@ -55,7 +57,8 @@ def main() -> None:
output,
newlines=args.newlines,
sparkline_length=args.length,
wait=args.wait,
wait_record=args.record,
wait_sample=args.sample,
mem_format=args.mem_format,
quiet=args.quiet,
)
Expand Down Expand Up @@ -213,6 +216,28 @@ def cli(argv: Sequence[str]) -> argparse.Namespace:
action="store_true",
help="do not print sparklines, only final report",
)
parser.add_argument(
"-r",
"--record",
default=None,
help=(
"how frequently to record/report memory usage "
f"(default: every {DEFAULT_RECORD_INTERVAL} ms)"
),
metavar="ms",
type=int,
)
parser.add_argument(
"-s",
"--sample",
default=None,
help=(
"how frequently to sample memory usage "
f"(default: every {DEFAULT_SAMPLE_INTERVAL} ms)"
),
metavar="ms",
type=int,
)
parser.add_argument(
"-t",
"--time-format",
Expand All @@ -225,18 +250,18 @@ def cli(argv: Sequence[str]) -> argparse.Namespace:
parser.add_argument(
"-w",
"--wait",
default=1000,
dest="wait",
help="how frequently to record memory usage (default: every %(default)d ms)",
default=None,
help="set sample and record time simultaneously (that both options override)",
metavar="ms",
type=int,
)

args = parser.parse_args(argv[1:])

wait_factor = SAMPLE_INTERVAL * 2
if args.wait % wait_factor != 0:
parser.error(f"wait time must be a multiple of {wait_factor} ms")
if args.record is None:
args.record = args.wait or DEFAULT_RECORD_INTERVAL
if args.sample is None:
args.sample = args.wait or DEFAULT_SAMPLE_INTERVAL

return args

Expand All @@ -260,7 +285,8 @@ def track(
*,
newlines: bool = False,
sparkline_length: int = 20,
wait: int = 1000,
wait_sample: int = 100,
wait_record: int = 1000,
mem_format: str = "0.1f%",
quiet: bool = False,
) -> tuple[int, list[int], list[int]]:
Expand All @@ -281,7 +307,7 @@ def track(
def add_record(current_time: int) -> None:
nonlocal last_record_time

if current_time - last_record_time < wait * 1_000_000:
if current_time - last_record_time < wait_record * 1_000_000:
return

history.append(record_maximum)
Expand Down Expand Up @@ -311,7 +337,7 @@ def add_record(current_time: int) -> None:

delta = (current_time - last_sample_time) // 1_000_000
last_sample_time = current_time
time.sleep(max(0, (SAMPLE_INTERVAL - delta) / 1000))
time.sleep(max(0, (wait_sample - delta) / 1000))

add_record(time.time_ns())
except (KeyboardInterrupt, psutil.NoSuchProcess):
Expand Down
5 changes: 5 additions & 0 deletions tests/test_memsparkline.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ def test_wait_2(self) -> None:

assert len(stderr.split("\n")) >= 9

def test_sample_and_record(self) -> None:
stderr = run("-r", "500", "-s", "100", *sleep_command())

assert len(stderr.split("\n")) == 5

def test_quiet(self) -> None:
stderr = run("-q", *sleep_command())

Expand Down

0 comments on commit 25e846b

Please sign in to comment.