diff --git a/daemon.py b/daemon.py index 062852a14..73e2b0bc2 100644 --- a/daemon.py +++ b/daemon.py @@ -348,6 +348,8 @@ from speaker import getSSMLbox from city import getSpoofedCity from fitnessFunctions import fitnessPerformance from fitnessFunctions import fitnessThread +from fitnessFunctions import sortedWatchPoints +from fitnessFunctions import htmlWatchPointsGraph import os @@ -12675,6 +12677,33 @@ class PubServer(BaseHTTPRequestHandler): '_GET', 'rss3 done', self.server.debug) + # show a performance graph + if authorized and self.path.startswith('/performance?graph='): + graph = self.path.split('?graph=')[1] + if htmlGET and not graph.endswith('.json'): + msg = \ + htmlWatchPointsGraph(self.server.baseDir, + self.server.fitness, + graph, 16) + msglen = len(msg) + self._set_headers('text/html', msglen, + cookie, callingDomain, False) + self._write(msg) + fitnessPerformance(GETstartTime, self.server.fitness, + '_GET', 'graph', + self.server.debug) + return + else: + graph = graph.replace('.json', '') + watchPointsJson = sortedWatchPoints(self.server.fitness, graph) + msg = json.dumps(watchPointsJson, + ensure_ascii=False).encode('utf-8') + msglen = len(msg) + self._set_headers('application/json', + msglen, + None, callingDomain, False) + self._write(msg) + # show the main blog page if htmlGET and (self.path == '/blog' or self.path == '/blog/' or diff --git a/epicyon-graph.css b/epicyon-graph.css new file mode 100644 index 000000000..9961f1352 --- /dev/null +++ b/epicyon-graph.css @@ -0,0 +1,141 @@ +body, table, input, select, textarea { + +} + +.graph { + margin-bottom:1em; + font:normal 100%/150% arial,helvetica,sans-serif; +} + +.graph caption { + font:bold 150%/120% arial,helvetica,sans-serif; + padding-bottom:0.33em; +} + +.graph tbody th { + text-align:right; +} + +@supports (display:grid) { + + @media (min-width:32em) { + + .graph { + display:block; + width:600px; + height:300px; + } + + .graph caption { + display:block; + } + + .graph thead { + display:none; + } + + .graph tbody { + position:relative; + display:grid; + grid-template-columns:repeat(auto-fit, minmax(2em, 1fr)); + column-gap:2.5%; + align-items:end; + height:100%; + margin:3em 0 1em 2.8em; + padding:0 1em; + border-bottom:2px solid rgba(0,0,0,0.5); + background:repeating-linear-gradient( + 180deg, + rgba(170,170,170,0.7) 0, + rgba(170,170,170,0.7) 1px, + transparent 1px, + transparent 20% + ); + } + + .graph tbody:before, + .graph tbody:after { + position:absolute; + left:-3.2em; + width:2.8em; + text-align:right; + font:bold 80%/120% arial,helvetica,sans-serif; + } + + .graph tbody:before { + content:"100%"; + top:-0.6em; + } + + .graph tbody:after { + content:"0%"; + bottom:-0.6em; + } + + .graph tr { + position:relative; + display:block; + } + + .graph tr:hover { + z-index:999; + } + + .graph th, + .graph td { + display:block; + text-align:center; + } + + .graph tbody th { + position:absolute; + top:-3em; + left:0; + width:100%; + font-weight:normal; + text-align:center; + white-space:nowrap; + text-indent:0; + transform:rotate(-45deg); + } + + .graph tbody th:after { + content:""; + } + + .graph td { + width:100%; + height:100%; + background:#F63; + border-radius:0.5em 0.5em 0 0; + transition:background 0.5s; + } + + .graph tr:hover td { + opacity:0.7; + } + + .graph td span { + overflow:hidden; + position:absolute; + left:50%; + top:50%; + width:0; + padding:0.5em 0; + margin:-1em 0 0; + font:normal 85%/120% arial,helvetica,sans-serif; + /* background:white; */ + /* box-shadow:0 0 0.25em rgba(0,0,0,0.6); */ + font-weight:bold; + opacity:0; + transition:opacity 0.5s; + color:white; + } + .toggleGraph:checked + table td span, + .graph tr:hover td span { + width:4em; + margin-left:-2em; + opacity:1; + } + } +} diff --git a/fitnessFunctions.py b/fitnessFunctions.py index 0299515e5..95ee9b630 100644 --- a/fitnessFunctions.py +++ b/fitnessFunctions.py @@ -7,7 +7,11 @@ __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Core" +import os import time +from webapp_utils import htmlHeaderWithExternalStyle +from webapp_utils import htmlFooter +from utils import getConfigParam from utils import saveJson @@ -41,6 +45,74 @@ def fitnessPerformance(startTime, fitnessState: {}, watchPoint + '/' + str(total * 1000 / ctr)) +def sortedWatchPoints(fitness: {}, fitnessId: str) -> []: + """Returns a sorted list of watchpoints + """ + if not fitness.get(fitnessId): + return [] + result = [] + for watchPoint, item in fitness[fitnessId].items(): + if not item.get('total'): + continue + averageTime = item['total'] / item['ctr'] + result.append(str(averageTime) + ' ' + watchPoint) + result.sort(reverse=True) + return result + + +def htmlWatchPointsGraph(baseDir: str, fitness: {}, fitnessId: str, + maxEntries: int) -> str: + """Returns the html for a graph of watchpoints + """ + watchPointsList = sortedWatchPoints(fitness, fitnessId) + + cssFilename = baseDir + '/epicyon-graph.css' + if os.path.isfile(baseDir + '/graph.css'): + cssFilename = baseDir + '/graph.css' + + instanceTitle = \ + getConfigParam(baseDir, 'instanceTitle') + htmlStr = htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + htmlStr += \ + '
Item | \n' + \ + 'Percent | \n' + \ + '
---|---|
' + name + ' | \n' + \ + '' + str(timeMS) + 'mS | \n' + \ + '