Source code for pymemtrace.util.gnuplot
"""
Provides gnuplot support to command line tools.
"""
import argparse
import logging
import os
import subprocess
import typing
from functools import reduce
logger = logging.getLogger(__file__)
[docs]
def add_gnuplot_to_argument_parser(parser: argparse.ArgumentParser) -> None:
"""Adds ``--gnuplot=<DIRECTORY_FOR_GNUPLOT_OUTPUT>`` to the argument parser as ``args.gnuplot``."""
v = version()
logger.info(f'gnuplot version: "{v}"')
print(f'gnuplot version: "{v}"')
if not v:
raise ValueError('--gnuplot option is requested but gnuplot is not installed.')
parser.add_argument('--gnuplot', type=str, help='Directory to write the gnuplot data.')
[docs]
def version() -> bytes:
"""
For example: b'gnuplot 5.2 patchlevel 6'
"""
with subprocess.Popen(['gnuplot', '--version'], stdout=subprocess.PIPE) as proc:
return proc.stdout.read().strip()
[docs]
def _num_columns(table: typing.Sequence[typing.Sequence[typing.Any]]) -> int:
"""
Returns the number of columns of the table.
Will raise a ValueError if the table is uneven.
"""
num_colums_set = set(len(r) for r in table)
if len(num_colums_set) != 1:
raise ValueError(f'Not rectangular: {num_colums_set}.')
return num_colums_set.pop()
[docs]
def create_gnuplot_dat(table: typing.Sequence[typing.Sequence[typing.Any]]) -> str:
"""
Returns a pretty formatted string of the data in the given table suitable for use as a gnuplot ``.dat`` file.
"""
num_columns = _num_columns(table)
column_widths = reduce(
lambda l, rows: [max(l, len(str(r)) + 2) for l, r in zip(l, rows)], table, [0,] * num_columns,
)
result: typing.List[str] = []
for row in table:
result.append(' '.join(f'{str(row[i]):<{column_widths[i]}}' for i in range(num_columns)))
return '\n'.join(result)
[docs]
def invoke_gnuplot(path: str, name: str, table: typing.Sequence[typing.Sequence[typing.Any]], plt: str) -> int:
"""
Create the plot for name.
path - the directory to write the data and plot files to.
name - the name of those files.
table - the table of values to write to the data file.
Returns the gnuplot error code.
"""
logger.info('Writing gnuplot data "{}" in path {}'.format(name, path))
os.makedirs(path, exist_ok=True)
with open(os.path.join(path, f'{name}.dat'), 'w') as outfile:
outfile.write(create_gnuplot_dat(table))
with open(os.path.join(path, f'{name}.plt'), 'w') as outfile:
outfile.write(plt)
proc = subprocess.Popen(
args=['gnuplot', '-p', f'{name}.plt'],
shell=False,
cwd=path,
)
try:
# Timeout 10 seconds as curve fitting can take a while.
stdout, stderr = proc.communicate(timeout=10)
except subprocess.TimeoutExpired as err:
logger.exception(str(err))
proc.kill()
stdout, stderr = proc.communicate()
logging.info(f'gnuplot stdout: {stdout}')
if proc.returncode or stderr:
logging.error(f'gnuplot stderr: {stdout}')
return proc.returncode
[docs]
def write_test_file(path: str, typ: str) -> int:
"""Writes out a Gnuplot test file."""
test_stdin = '\n'.join(
[
f'set terminal {typ}',
f'set output "test.{typ}"',
'test',
]
)
proc = subprocess.Popen(
args=['gnuplot'],
shell=False,
cwd=path,
stdin=subprocess.PIPE,
)
try:
proc.stdin.write(bytes(test_stdin, 'ascii'))
# proc.stdin.close()
stdout, stderr = proc.communicate(timeout=1, )
except subprocess.TimeoutExpired as err:
logger.exception()
proc.kill()
stdout, stderr = proc.communicate()
logging.info(f'gnuplot stdout: {stdout}')
if stderr:
logging.error(f'gnuplot stderr: {stdout}')
return proc.returncode
# Gnuplot fragments
PLOT = """set grid
set title "{title}"
set pointsize 1
set datafile separator whitespace#" "
set datafile missing "NaN"
"""
X_LOG = """set logscale x
set xlabel "{label}"
# set mxtics 5
# set xrange [0:3000]
# set xtics
# set format x
"""
Y_LOG = """set logscale y
set ylabel "{label}"
# set yrange [1:1e5]
# set ytics 20
# set mytics 2
# set ytics 8,35,3
"""
Y2_LOG = """set logscale y2
set y2label "{label}"
#set y2range [1e5:1e9]
set y2tics
"""