Skip to content

Commit

Permalink
v0.0.23 (#26)
Browse files Browse the repository at this point in the history
perf/valgrind/etc
  • Loading branch information
PhilipDeegan authored Jul 13, 2024
1 parent 3f8cb24 commit 09fb1e9
Show file tree
Hide file tree
Showing 27 changed files with 370 additions and 70 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/build_nix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
architecture: x64

- run: |
python3 -m pip install numpy
python3 -m pip install -r requirements.txt
./sh/lint.sh
export PATH="$PWD:$PATH"
./sh/test.sh
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
.phlop
massif.phlop
.mkn
bin
__pycache__
Expand Down
2 changes: 2 additions & 0 deletions phlop/app/cmake.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,13 @@ def list_tests(build_dir=None):
if s
]
)

return [
CTest_test(**test)
for test in json.loads(decode_bytes(run(cmd, capture_output=True).stdout))[
"tests"
]
if "command" in test
]


Expand Down
5 changes: 4 additions & 1 deletion phlop/app/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import atexit
import subprocess

from phlop.logger import getLogger
from phlop.proc import run
from phlop.string import decode_bytes

logger = getLogger(__name__)


def current_branch():
return decode_bytes(run("git branch --show-current").stdout)
Expand All @@ -28,7 +31,7 @@ def log(N=10, use_short=False):
def branch_exists(branch):
try:
run(f"git show-branch {branch}", check=True)
except subprocess.CalledProcessError as e:
except subprocess.CalledProcessError:
return False # exit failure means branch does not exist
return True

Expand Down
60 changes: 55 additions & 5 deletions phlop/app/perf.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
#
#
#
#
#


import csv
import os

from phlop.dict import ValDict
from phlop.proc import run, run_mp

# can be modified
perf_events = [
"duration_time",
"cycles",
"instructions",
"cache-references",
"cache-misses",
"L1-dcache-loads",
"L1-dcache-load-misses",
]
# "perf stat" can support more events than "perf record"
stat_events = perf_events + ["bus-cycles"]


def version():
# validated on perf version: 5.19
Expand Down Expand Up @@ -41,6 +51,8 @@ def parse_key(key, force_kernel_space):


def parse_stat_csv(file, force_kernel_space=False):
import csv

comments_lines = 2 # validate
row_val_idx, row_id_idx = 0, 2
with open(file, newline="") as csvfile:
Expand All @@ -51,6 +63,13 @@ def parse_stat_csv(file, force_kernel_space=False):
}


def parse_stat_json(file):
import json

with open(file, newline="") as f:
return json.loads(f)


# https://perf.wiki.kernel.org/index.php/Tutorial
# http://www.brendangregg.com/perf.html
# or run "perf list"
Expand All @@ -65,10 +84,11 @@ def out_str(output_file):


def stat_cmd(exe, events, output_file, options=""):
return f"perf stat -j {options} {out_str(output_file)} {events_str(events)} {exe}"
return f"perf stat -x , {options} {out_str(output_file)} {events_str(events)} {exe}"


def stat(exe, events, output_file=None):
def stat(exe, events=stat_events, output_file=None):
return run(stat_cmd(exe, events, output_file), check=True)


Expand All @@ -82,3 +102,33 @@ def record(exe, events, output_file=None):

def stat_mp(exe, events, output_files):
return run_mp([stat_cmd(exe, events, out) for out in output_files])


def cli_args_parser():
import argparse

_help = ValDict(
dir="working directory",
quiet="Redirect output to /dev/null",
cores="Parallism core/thread count",
infiles="infiles",
print_only="Print only, no execution",
regex="Filter out non-matching execution strings",
logging="0=off, 1=on non zero exit code, 2=always",
)

parser = argparse.ArgumentParser()
parser.add_argument("remaining", nargs=argparse.REMAINDER)
parser.add_argument("-d", "--dir", default=".", help=_help.dir)
parser.add_argument("-c", "--cores", type=int, default=1, help=_help.cores)
parser.add_argument(
"-p", "--print_only", action="store_true", default=False, help=_help.print_only
)
parser.add_argument("-i", "--infiles", default=None, help=_help.infiles)
parser.add_argument("-r", "--regex", default=None, help=_help.regex)
parser.add_argument("--logging", type=int, default=1, help=_help.logging)
return parser


def verify_cli_args(cli_args):
return cli_args
27 changes: 27 additions & 0 deletions phlop/app/valgrind.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# valgrind.py
#

from phlop.proc import run


def run_massif(cli_args):
outfile = cli_args.outfile if cli_args.outfile else "massif.phlop"
cmd = f"valgrind --tool=massif --massif-out-file={outfile} " + " ".join(
cli_args.remaining
)
run(cmd, capture_output=True)


def run_memcheck(cli_args):
cmd = "valgrind " + " ".join(cli_args.remaining)
run(cmd, capture_output=True)


def run_valgrind(cli_args):
tool = cli_args.tool
if tool == "massif":
run_massif(cli_args)
elif tool == "memcheck" or not tool:
run_memcheck(cli_args)
elif tool:
raise RuntimeError(f"Phlop ERROR: Unhandled valgrind tool: {tool}")
18 changes: 14 additions & 4 deletions phlop/logger.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
#
#
#
#


import logging
import os

LOG_LEVEL = os.environ.get("PHLOP_LOG_LEVEL", "INFO")
log_levels = {"INFO": 20, "WARNING": 30, "ERROR": 40, "DEBUG": 10}

FORMAT = "[log: %(filename)s:%(lineno)s - %(funcName)30s() ] %(message)s"
logging.basicConfig(format=FORMAT)
# logger.setLevel(log_levels[LOG_LEVEL])


logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def getLogger(name, level=LOG_LEVEL):
logger = logging.getLogger(__name__)
level = log_levels[level] if isinstance(level, str) else level
logger.setLevel(level)
return logger
10 changes: 5 additions & 5 deletions phlop/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@
#
#
#
#

import subprocess
import sys

from phlop.os import pushd, write_to_file
from phlop.procs.runtimer import RunTimer
from phlop.string import decode_bytes


class ProcessNonZeroExitCode(RuntimeError): ...
Expand Down Expand Up @@ -51,7 +48,10 @@ def run_mp(cmds, N_CORES=None, **kwargs):
raise future.exception()
except Exception as exc:
if kwargs.get("check", False):
executor.shutdown(wait=False, cancel_futures=True)
dic = {"wait": False}
if sys.version_info > (3, 8):
dic["cancel_futures"] = True
executor.shutdown(**dic)
raise exc
else:
print(f"run_mp generated an exception: {exc}")
Expand Down
2 changes: 1 addition & 1 deletion phlop/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def classes_in_file(file_path, subclasses_only=None, fail_on_import_error=False)
for name, cls in inspect.getmembers(
importlib.import_module(module), inspect.isclass
):
should_add = subclasses_only == None or any(
should_add = subclasses_only is None or any(
[issubclass(cls, sub) for sub in subclasses_only]
)
if should_add:
Expand Down
4 changes: 3 additions & 1 deletion phlop/run/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
available_modules = """Available:
phlop.run.test_cases -h
phlop.run.stats_man -h
phlop.run.mpirun_stats_man -h"""
phlop.run.mpirun_stats_man -h
phlop.run.perf -h
phlop.run.valgrind -h"""

print(available_modules)
40 changes: 40 additions & 0 deletions phlop/run/mpirun_perf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
#

import logging
import os
import sys

from phlop.app import perf as p

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

MPI_RANK = os.environ.get("OMPI_COMM_WORLD_RANK")


def verify_cli_args(cli_args):
try:
cli_args.interval = int(cli_args.interval)
except ValueError:
raise ValueError("Interval must be an integer") from None
if cli_args.yaml:
cli_args.yaml = f"{cli_args.yaml}.{MPI_RANK}.yaml"
cli_args.summary = False
sys.argv = [sys.argv[0]] # drop everything!
return cli_args


def main():
parser = p.cli_args_parser()
# cli_args = verify_cli_args(parser.parse_args())
try:
...
except (Exception, SystemExit) as e:
logger.exception(e)
parser.print_help()
sys.exit(1)


if __name__ == "__main__":
main()
4 changes: 1 addition & 3 deletions phlop/run/mpirun_stats_man.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@ def main():
except (Exception, SystemExit) as e:
logger.exception(e)
parser.print_help()
except:
e = sys.exc_info()[0]
print(f"Error: Unknown Error {e}")
sys.exit(1)


if __name__ == "__main__":
Expand Down
85 changes: 85 additions & 0 deletions phlop/run/perf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#
#

import sys
from pathlib import Path

from phlop.app import perf as p
from phlop.logger import getLogger
from phlop.testing import parallel_processor as pp
from phlop.testing import test_cases as tc

logger = getLogger(__name__)
logpath = Path(".phlop") / "perf"

"""
if you get an error about js-d3-flamegraph: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=996839
mkdir /usr/share/d3-flame-graph
wget -O /usr/share/d3-flame-graph/d3-flamegraph-base.html https://cdn.jsdelivr.net/npm/d3-flame-graph@4/dist/templates/d3-flamegraph-base.html
"""


def perf_stat_cmd(cli_args, path, line):
file = Path(line.split(" ")[-1]).stem
outpath = logpath / path.stem
outpath.mkdir(parents=True, exist_ok=True)
return p.stat_cmd(line, p.stat_events, outpath / f"{file}.json")


def get_from_files(cli_args):
test_batches = {}
file_paths = list(Path(cli_args.dir).glob(cli_args.infiles))

if not file_paths:
raise ValueError("No load files found")

for path in file_paths:
with open(path, "r") as file:
while line := file.readline():
test_case = tc.determine_cores_for_test_case(
tc.TestCase(cmd=perf_stat_cmd(cli_args, path, line.rstrip()))
)
if test_case.cores not in test_batches:
test_batches[test_case.cores] = []
test_batches[test_case.cores].append(test_case)

return [tc.TestBatch(v, k) for k, v in test_batches.items()]


def get_remaining(cli_args):
test_batches = {}
path = Path(cli_args.remaining[-1]).parent
test_case = tc.determine_cores_for_test_case(
tc.TestCase(cmd=perf_stat_cmd(cli_args, path, " ".join(cli_args.remaining)))
)
test_batches[test_case.cores] = [test_case]
return [tc.TestBatch(v, k) for k, v in test_batches.items()]


def main():
logpath.mkdir(parents=True, exist_ok=True)

parser = p.cli_args_parser()
cli_args = p.verify_cli_args(parser.parse_args())
try:
test_batches = []
if cli_args.infiles:
test_batches = get_from_files(cli_args)
else:
test_batches = get_remaining(cli_args)

pp.process(
test_batches,
n_cores=cli_args.cores,
print_only=cli_args.print_only,
logging=cli_args.logging,
)

except (Exception, SystemExit) as e:
logger.exception(e)
parser.print_help()
sys.exit(1)


if __name__ == "__main__":
main()
Loading

0 comments on commit 09fb1e9

Please sign in to comment.