-
Notifications
You must be signed in to change notification settings - Fork 5
Adding Benchmark instrument #157
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
Open
tharittk
wants to merge
11
commits into
PyLops:main
Choose a base branch
from
tharittk:benchmark_instrument
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
3dbecfe
first version benhmark wrapper
tharittk 626006f
Merge branch 'PyLops:main' into benchmark_instrument
tharittk 7c31951
kirchhoff example for benchmark decorator
tharittk 6e66898
Fixed benchmark.py, allows to decorate without params, handle nested …
tharittk 2d5b019
Merge branch 'PyLops:main' into benchmark_instrument
tharittk d760ba2
Handled nested benchmark text output with tree parsing
tharittk a04d1ff
benchmark with logging, improve text output formatting, fix _check_lo…
tharittk c2fd2e0
slightly cleaner logger logic
tharittk cc3cfdc
move the logging logic out of benchmark, fix documentations typos
tharittk 6b84048
fix flake8 error
tharittk 8f12b78
fix bug & flake8 f-string
tharittk File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import functools | ||
import logging | ||
import time | ||
from typing import Callable, Optional, List | ||
from mpi4py import MPI | ||
|
||
|
||
# TODO (tharitt): later move to env file or something | ||
ENABLE_BENCHMARK = True | ||
|
||
# Stack of active mark functions for nested support | ||
_mark_func_stack = [] | ||
_markers = [] | ||
|
||
|
||
def _parse_output_tree(markers: List[str]): | ||
"""This function parses the list of strings gathered during the benchmark call and output them | ||
as one properly formatted string. The format of output string follows the hierachy of function calls | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hierachy -> hierarchy |
||
i.e., the nested funtion calls are indented. | ||
|
||
Parameters | ||
---------- | ||
markers: :obj:`list`, optional | ||
A list of markers/labels generated from the benchmark call | ||
""" | ||
output = [] | ||
stack = [] | ||
i = 0 | ||
while i < len(markers): | ||
label, time, level = markers[i] | ||
if label.startswith("[decorator]"): | ||
indent = "\t" * (level - 1) | ||
output.append(f"{indent}{label}: total runtime: {time:6f} s\n") | ||
else: | ||
if stack: | ||
prev_label, prev_time, prev_level = stack[-1] | ||
if prev_level == level: | ||
indent = "\t" * level | ||
output.append(f"{indent}{prev_label}-->{label}: {time - prev_time:6f} s\n") | ||
stack.pop() | ||
|
||
# Push to the stack only if it is going deeper or still at the same level | ||
if i + 1 <= len(markers) - 1: | ||
_, _ , next_level = markers[i + 1] | ||
if next_level >= level: | ||
stack.append(markers[i]) | ||
i += 1 | ||
return output | ||
|
||
|
||
def mark(label: str): | ||
"""This function allows users to measure time arbitary lines of the function | ||
|
||
Parameters | ||
---------- | ||
label: :obj:`str` | ||
A label of the mark. This signifies both 1) the end of the | ||
previous mark 2) the beginning of the new mark | ||
""" | ||
if not _mark_func_stack: | ||
raise RuntimeError("mark() called outside of a benchmarked region") | ||
_mark_func_stack[-1](label) | ||
|
||
|
||
def benchmark(func: Optional[Callable] = None, | ||
description: Optional[str] = "", | ||
logger: Optional[logging.Logger] = None, | ||
): | ||
"""A wrapper for code injection for time measurement. | ||
|
||
This wrapper measures the start-to-end time of the wrapped function when | ||
decorated without any argument. | ||
|
||
It also allows users to put a call to mark() anywhere inside the wrapped function | ||
tharittk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
for fine-grain time benchmark. This wrapper defines the local_mark() and pushes it | ||
to the _mark_func_stack for isolation in case of nested call. | ||
The user-facing mark() will always call the function at the top of the _mark_func_stack. | ||
|
||
Parameters | ||
---------- | ||
func : :obj:`callable`, optional | ||
Function to be decorated. Defaults to ``None``. | ||
description : :obj:`str`, optional | ||
Description for the output text. Defaults to ``''``. | ||
logger: :obj:`logging.Logger`, optional | ||
A `logging.Logger` object for logging the benchmark text output. This logger must be setup before | ||
passing to this function to either writing output to a file or log to stdout. If `logger` | ||
is not provided, the output is printed to stdout. | ||
""" | ||
|
||
# Zero-overhead | ||
if not ENABLE_BENCHMARK: | ||
return func | ||
|
||
@functools.wraps(func) | ||
def decorator(func): | ||
def wrapper(*args, **kwargs): | ||
rank = MPI.COMM_WORLD.Get_rank() | ||
|
||
level = len(_mark_func_stack) + 1 | ||
# The header is needed for later tree parsing. Here it is allocating its spot. | ||
# the tuple at this index will be replaced after elapsed time is calculated. | ||
_markers.append((f"[decorator]{description or func.__name__}", None, level)) | ||
header_index = len(_markers) - 1 | ||
|
||
def local_mark(label): | ||
_markers.append((label, time.perf_counter(), level)) | ||
|
||
_mark_func_stack.append(local_mark) | ||
|
||
start_time = time.perf_counter() | ||
# the mark() called in wrapped function will now call local_mark | ||
result = func(*args, **kwargs) | ||
end_time = time.perf_counter() | ||
|
||
elapsed = end_time - start_time | ||
_markers[header_index] = (f"[decorator]{description or func.__name__}", elapsed, level) | ||
|
||
# In case of nesting, the wrapped callee must pop its closure from stack so that | ||
# when the callee returns, the wrapped caller operates on its closure (and its level label), which now becomes | ||
# the top of the stack. | ||
_mark_func_stack.pop() | ||
|
||
# all the calls have fininshed | ||
if not _mark_func_stack: | ||
if rank == 0: | ||
output = _parse_output_tree(_markers) | ||
if logger: | ||
logger.info("".join(output)) | ||
else: | ||
print("".join(output)) | ||
return result | ||
return wrapper | ||
if func is not None: | ||
return decorator(func) | ||
|
||
return decorator |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be done for sure and documented in the new doc page I suggested 😄
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it.