Files
tryton/trytond/bin/trytond-stat
T

144 lines
4.8 KiB
Python
Executable File

#!/usr/bin/env python3
# PYTHON_ARGCOMPLETE_OK
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import curses
import datetime as dt
import math
import os
import sys
from collections import defaultdict
try:
import argcomplete
except ImportError:
argcomplete = None
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__,
'..', '..', 'trytond')))
if os.path.isdir(DIR):
sys.path.insert(0, os.path.dirname(DIR))
import trytond.commandline as commandline
from trytond.config import config
parser = commandline.get_parser_stat()
if argcomplete:
argcomplete.autocomplete(parser)
options = parser.parse_args()
config.update_etc(options.configfile)
import trytond.status as status
def main(stdscr):
global reverse
stdscr.nodelay(1)
reverse = True
processes = {}
status_pad = curses.newpad(1, 1)
cache_pad = curses.newpad(1, 1)
def refresh_status():
now = dt.datetime.now()
height, width = stdscr.getmaxyx()
def expired(process):
return process['expire'] > now
def format_status(since, id_, request):
since = str(dt.timedelta(seconds=int(since)))
pid, node = id_.split('@', 1)
if len(node) > 12:
node = node[:5] + '…' + node[:6]
return f"{pid:>5} {node:<12} {since:>18} {request}"
status_pad.clear()
status = [format_status(*i) for i in sorted(
((msg['since'], p['id'], msg['request'])
for p in filter(expired, processes.values())
for msg in p['status']),
reverse=reverse)]
prow = min(len(status) + 1, height // 2)
pcol = max(max(map(len, status), default=0), width)
status_pad.resize(len(status) + 1, pcol + 1)
for i, line in enumerate(status, 1):
status_pad.addnstr(i, 0, line.ljust(pcol), pcol)
status_pad.addnstr(
0, 0, "{pid:>5} {node:^12} {since:>18} {request} ({n})".format(
pid="pid",
node="node",
since="TIME" + ('↑' if reverse else '↓'),
request="request",
n=len(status),
).upper().ljust(pcol), pcol, curses.A_REVERSE)
status_pad.noutrefresh(0, 0, 0, 0, prow, width - 1)
def ratio(cache):
if cache['hit'] or cache['miss']:
return cache['hit'] / (cache['hit'] + cache['miss'])
return 0
def format_cache(name, hit, miss, ratio, size):
return f"{hit:{size}d} {miss:{size}d} {ratio * 100:6.2f} {name}"
cache_pad.clear()
cache_stats = defaultdict(lambda: defaultdict(lambda: 0))
for p in filter(expired, processes.values()):
for cache in p['caches']:
stats = cache_stats[cache['name']]
stats['name'] = cache['name']
stats['hit'] += cache['hit']
stats['miss'] += cache['miss']
for cache in cache_stats.values():
cache['ratio'] = ratio(cache)
try:
size = math.ceil(math.log10(
max(s['hit'] + s['miss'] for s in cache_stats.values())))
except ValueError:
size = 1
size = max(size, 4)
caches = [format_cache(size=size, **cache) for cache in sorted(
cache_stats.values(), key=lambda c: (c['ratio'], c['miss']),
reverse=reverse)]
crow = max(len(caches) + 1, (height - prow))
ccol = max(max(map(len, caches), default=0), width)
cache_pad.resize(crow + 1, ccol + 1)
for i, line in enumerate(caches, 1):
cache_pad.addnstr(i, 0, line.ljust(ccol), ccol)
cache_pad.addstr(
0, 0,
"{hit:>{size}} {miss:>{size}} {ratio:>6} {name} ({n})".format(
size=size,
hit="hit",
miss="miss",
ratio="% " + ('↑' if reverse else '↓'),
name="name",
n=len(caches),
).upper().ljust(ccol), curses.A_REVERSE)
cache_pad.noutrefresh(0, 0, prow, 0, height - 1, width - 1)
def refresh():
global reverse
refresh_status()
stdscr.refresh()
key = stdscr.getch()
if key == ord('q'):
sys.exit()
elif key == ord('r'):
reverse = not reverse
refresh()
def update(data=None):
if data:
pid = data['id']
data['expire'] = dt.datetime.now() + dt.timedelta(seconds=10)
processes[pid] = data
refresh()
refresh()
return status.listen(config.get('database', 'path'), update)
if not curses.wrapper(main):
sys.stderr.write("status not supported on this platform\n")