144 lines
4.8 KiB
Python
Executable File
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")
|