Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DM-33331: Monitor memory usage at select steps of BPS submission #112

Merged
merged 2 commits into from
Feb 22, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/DM-33331.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Context manager time_this can now include memory usage in its report.
70 changes: 68 additions & 2 deletions python/lsst/utils/timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from __future__ import annotations

__all__ = ["logInfo", "timeMethod", "time_this"]
__all__ = ["logInfo", "timeMethod", "time_this", "get_mem_usage"]

import datetime
import functools
Expand All @@ -36,6 +36,8 @@
Tuple,
)

import psutil

if TYPE_CHECKING:
from .logging import LsstLoggers

Expand Down Expand Up @@ -111,6 +113,34 @@ def _find_outside_stacklevel() -> int:
return stacklevel


def get_mem_usage(include_children: bool = False) -> int:
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
"""Report current memory usage.

Parameters
----------
include_children : `bool`, optional
Flag indicating if memory usage by child processes (if any) should be
included in the report. Be default, only memory usage of the calling
process is reported.

Returns
-------
mem : `int`
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
Memory usage expressed in bytes.

Notes
-----
Function reports current memory usage using resident set size as a proxy.
As such the value it reports is capped at available physical RAM and may
not reflect the actual memory allocated to the process.
"""
proc = psutil.Process()
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
usage = proc.memory_info().rss
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
if include_children:
usage += sum([child.memory_info().rss for child in proc.children()])
return usage


def logPairs(
obj: Any,
pairs: Collection[Tuple[str, Any]],
Expand Down Expand Up @@ -363,6 +393,9 @@ def time_this(
level: int = logging.DEBUG,
prefix: Optional[str] = "timer",
args: Iterable[Any] = (),
mem_usage: bool = False,
mem_child: bool = False,
mem_units: str = "B",
) -> Iterator[None]:
"""Time the enclosed block and issue a log message.

Expand All @@ -384,6 +417,13 @@ def time_this(
args : iterable of any
Additional parameters passed to the log command that should be
written to ``msg``.
mem_usage : `bool`, optional
Flag indicating whether to include the memory usage in the report.
Defaults, to False.
mem_child : `bool`, optional
Flag indication whether to include memory usage of the child processes.
mem_units : {"B", "KB", "MB", "GB"}, optional
Units to use when reporting the memory usage. Defaults to bytes ("B").
"""
if log is None:
log = logging.getLogger()
Expand All @@ -393,11 +433,15 @@ def time_this(

success = False
start = time.time()
if mem_usage:
mem_start = get_mem_usage(include_children=mem_child)
try:
yield
success = True
finally:
end = time.time()
if mem_usage:
mem_end = get_mem_usage(include_children=mem_child)
mxk62 marked this conversation as resolved.
Show resolved Hide resolved

# The message is pre-inserted to allow the logger to expand
# the additional args provided. Make that easier by converting
Expand All @@ -412,4 +456,26 @@ def time_this(

# Specify stacklevel to ensure the message is reported from the
# caller (1 is this file, 2 is contextlib, 3 is user)
log.log(level, msg + "%sTook %.4f seconds", *args, ": " if msg else "", end - start, stacklevel=3)
if mem_usage:
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
scales = {
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
"B": (1.0, "%d"),
"KB": (float(2**10), "%.1f"),
"MB": (float(2**20), "%.2f"),
"GB": (float(2**30), "%.3f"),
}
if mem_units not in scales:
mem_units = "B"
scale, fmt = scales[mem_units]
report = f"%sTook %.4f seconds; memory usage {fmt} {mem_units} (increment: {fmt} {mem_units})"
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
log.log(
level,
msg + report,
*args,
": " if msg else "",
end - start,
mem_end / scale,
(mem_end - mem_start) / scale,
stacklevel=3,
)
else:
log.log(level, msg + "%sTook %.4f seconds", *args, ": " if msg else "", end - start, stacklevel=3)
32 changes: 32 additions & 0 deletions tests/test_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,38 @@ def testTimer(self):
self.assertIn("Took", cm.output[0])
self.assertEqual(cm.records[0].filename, THIS_FILE)

# Report memory usage.
with self.assertLogs(level="DEBUG") as cm:
with time_this(level=logging.DEBUG, prefix=None, mem_usage=True):
pass
self.assertEqual(cm.records[0].name, "root")
self.assertEqual(cm.records[0].levelname, "DEBUG")
self.assertIn("Took", cm.output[0])
mxk62 marked this conversation as resolved.
Show resolved Hide resolved
self.assertIn("memory", cm.output[0])
self.assertIn("increment", cm.output[0])

# Report memory usage, use different, but known memory units.
with self.assertLogs(level="DEBUG") as cm:
with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_units="GB"):
pass
self.assertEqual(cm.records[0].name, "root")
self.assertEqual(cm.records[0].levelname, "DEBUG")
self.assertIn("Took", cm.output[0])
self.assertIn("memory", cm.output[0])
self.assertIn("increment", cm.output[0])
self.assertIn(" GB", cm.output[0])

# Report memory usage, use unknown memory units.
with self.assertLogs(level="DEBUG") as cm:
with time_this(level=logging.DEBUG, prefix=None, mem_usage=True, mem_units="foo"):
pass
self.assertEqual(cm.records[0].name, "root")
self.assertEqual(cm.records[0].levelname, "DEBUG")
self.assertIn("Took", cm.output[0])
self.assertIn("memory", cm.output[0])
self.assertIn("increment", cm.output[0])
self.assertIn(" B", cm.output[0])

# Change logging level
with self.assertLogs(level="INFO") as cm:
with time_this(level=logging.INFO, prefix=None):
Expand Down