Changeset - a745bee5c419
[Not reviewed]
default
0 8 1
Drew Perttula - 6 years ago 2019-06-06 09:31:36
drewp@bigasterisk.com
new process stats visualizers
Ignore-this: 8e47516baad0bfb9cd02a712571c5a5f
9 files changed with 123 insertions and 8 deletions:
0 comments (0 inline, 0 general)
bin/ascoltami2
Show inline comments
 
#!bin/python
 
from run_local import log
 
from twisted.internet import reactor
 
import sys, optparse, logging
 
from rdflib import URIRef
 
import gi
 
gi.require_version('Gst', '1.0')
 
gi.require_version('Gtk', '3.0')
 

	
 
from light9.ascoltami.player import Player
 
from light9.ascoltami.playlist import Playlist, NoSuchSong
 
from light9.ascoltami.webapp import makeWebApp, songUri, songLocation
 
from light9 import networking, showconfig
 
from standardservice.scalessetup import gatherProcessStats
 

	
 
from gi.repository import GObject, Gst
 

	
 
gatherProcessStats()
 

	
 
class App(object):
 

	
 
    def __init__(self, graph, show):
 
        self.graph = graph
 
        self.player = Player(onEOS=self.onEOS)
 
        self.show = show
 
        self.playlist = Playlist.fromShow(graph, show)
 

	
 
    def onEOS(self, song):
 
        self.player.pause()
 
        self.player.seek(0)
 

	
 
        thisSongUri = songUri(graph, URIRef(song))
 

	
 
        try:
 
            nextSong = self.playlist.nextSong(thisSongUri)
 
        except NoSuchSong:  # we're at the end of the playlist
 
            return
 

	
 
        self.player.setSong(songLocation(graph, nextSong), play=False)
 

	
 

	
 
if __name__ == "__main__":
bin/collector
Show inline comments
 
@@ -4,65 +4,67 @@ Collector receives device attrs from mul
 
them, and sends output attrs to hardware. The combining part has
 
custom code for some attributes.
 

	
 
Input can be over http or zmq.
 
"""
 

	
 
from run_local import log
 

	
 
from twisted.internet import reactor, utils
 
import json
 
import logging
 
import optparse
 
import traceback
 
import cyclone.web, cyclone.websocket
 
from greplin import scales
 

	
 
from cycloneerr import PrettyErrorHandler
 
from light9 import networking
 
from light9.collector.collector import Collector
 
from light9.collector.weblisteners import WebListeners
 
from greplin.scales.cyclonehandler import StatsHandler
 
from light9.namespaces import L9
 
from light9.zmqtransport import parseJsonMessage, startZmq
 
from rdfdb.syncedgraph import SyncedGraph
 
from standardservice.scalessetup import gatherProcessStats
 

	
 
from light9.collector.output import Udmx, DummyOutput  # noqa
 

	
 

	
 
class Updates(cyclone.websocket.WebSocketHandler):
 

	
 
    def connectionMade(self, *args, **kwargs):
 
        log.info('socket connect %s', self)
 
        self.settings.listeners.addClient(self)
 

	
 
    def connectionLost(self, reason):
 
        self.settings.listeners.delClient(self)
 

	
 
    def messageReceived(self, message):
 
        json.loads(message)
 

	
 

	
 
gatherProcessStats()
 
stats = scales.collection(
 
    '/webServer',
 
    scales.PmfStat('setAttr', recalcPeriod=1),
 
    scales.RecentFpsStat('setAttrFps'),
 
)
 

	
 

	
 
class Attrs(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def put(self):
 
        stats.setAttrFps.mark()
 
        with stats.setAttr.time():
 
            client, clientSession, settings, sendTime = parseJsonMessage(
 
                self.request.body)
 
            self.settings.collector.setAttrs(client, clientSession, settings,
 
                                             sendTime)
 
            self.set_status(202)
 

	
 

	
 
def launch(graph, doLoadTest=False):
 
    try:
 
        # todo: drive outputs with config files
 
        outputs = [
 
            Udmx(L9['output/dmxA/'], bus=None, address=None,
bin/effecteval
Show inline comments
 
#!bin/python
 

	
 
from run_local import log
 
from twisted.internet import reactor
 
from twisted.internet.defer import inlineCallbacks, returnValue
 
import cyclone.web, cyclone.websocket, cyclone.httpclient
 
import sys, optparse, logging, json, itertools
 
from rdflib import URIRef, Literal
 

	
 
from light9 import networking, showconfig
 
from light9.effecteval.effect import EffectNode
 
from light9.effect.edit import getMusicStatus, songNotePatch
 
from light9.effecteval.effectloop import makeEffectLoop
 
from greplin.scales.cyclonehandler import StatsHandler
 
from light9.namespaces import L9
 
from rdfdb.patch import Patch
 
from rdfdb.syncedgraph import SyncedGraph
 
from greplin import scales
 
from standardservice.scalessetup import gatherProcessStats
 

	
 
from cycloneerr import PrettyErrorHandler
 
from light9.coffee import StaticCoffee
 

	
 
gatherProcessStats()
 

	
 
class EffectEdit(PrettyErrorHandler, cyclone.web.RequestHandler):
 

	
 
    def get(self):
 
        self.set_header('Content-Type', 'text/html')
 
        self.write(open("light9/effecteval/effect.html").read())
 

	
 
    def delete(self):
 
        graph = self.settings.graph
 
        uri = URIRef(self.get_argument('uri'))
 
        with graph.currentState(tripleFilter=(None, L9['effect'], uri)) as g:
 
            song = ctx = list(g.subjects(L9['effect'], uri))[0]
 
        self.settings.graph.patch(
 
            Patch(delQuads=[
 
                (song, L9['effect'], uri, ctx),
 
            ]))
 

	
 

	
 
@inlineCallbacks
 
def currentSong():
 
    s = (yield getMusicStatus())['song']
 
    if s is None:
 
        raise ValueError("no current song")
 
    returnValue(URIRef(s))
bin/vidref
Show inline comments
 
@@ -9,55 +9,57 @@ bin/vidref main
 
light9/vidref/videorecorder.py capture frames and save them
 
light9/vidref/replay.py backend for vidref.js playback element- figures out which frames go with the current song and time
 
light9/vidref/index.html web ui for watching current stage and song playback
 
light9/vidref/setup.html web ui for setup of camera params and frame crop
 
light9/web/light9-vidref-live.js LitElement for live video frames
 
light9/web/light9-vidref-playback.js LitElement for video playback
 

	
 
"""
 
from run_local import log
 

	
 
from typing import cast
 
import logging, optparse, json, base64, os, glob
 

	
 
from greplin import scales
 
from greplin.scales.cyclonehandler import StatsHandler
 
from rdflib import URIRef
 
from twisted.internet import reactor, defer
 
import cyclone.web, cyclone.httpclient, cyclone.websocket
 

	
 
from cycloneerr import PrettyErrorHandler
 
from light9 import networking, showconfig
 
from light9.newtypes import Song
 
from light9.vidref import videorecorder
 
from rdfdb.syncedgraph import SyncedGraph
 
from standardservice.scalessetup import gatherProcessStats
 

	
 
parser = optparse.OptionParser()
 
parser.add_option("-v", "--verbose", action="store_true", help="logging.DEBUG")
 
(options, args) = parser.parse_args()
 

	
 
log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
 

	
 
gatherProcessStats()
 
stats = scales.collection(
 
    '/webServer',
 
    scales.RecentFpsStat('liveWebsocketFrameFps'),
 
    scales.IntStat('liveClients'),
 
)
 

	
 

	
 
class Snapshot(cyclone.web.RequestHandler):
 

	
 
    @defer.inlineCallbacks
 
    def post(self):
 
        # save next pic
 
        # return /snapshot/path
 
        try:
 
            snapshotDir = 'todo'
 
            outputFilename = yield self.settings.gui.snapshot()
 

	
 
            assert outputFilename.startswith(snapshotDir)
 
            out = networking.vidref.path(
 
                "snapshot/%s" % outputFilename[len(snapshotDir):].lstrip('/'))
 

	
 
            self.write(json.dumps({'snapshot': out}))
 
            self.set_header("Location", out)
 
            self.set_status(303)
light9/effect/sequencer.py
Show inline comments
 
'''
 
copies from effectloop.py, which this should replace
 
'''
 

	
 
from louie import dispatcher
 
from rdflib import URIRef
 
from twisted.internet import reactor
 
from twisted.internet import defer
 
from twisted.internet.inotify import INotify
 
from twisted.python.filepath import FilePath
 
import cyclone.sse
 
import logging, bisect, time
 
import traceback
 
from typing import Any, Callable, Dict, List, Tuple, cast, Union
 

	
 
from light9.ascoltami.musictime_client import MusicTime
 
from light9.effect import effecteval
 
from light9.effect.settings import DeviceSettings
 
from light9.effect.simple_outputs import SimpleOutputs
 
from light9.namespaces import L9, RDF
 
from light9.newtypes import DeviceUri, DeviceAttr, NoteUri, Curve, Song
 
from rdfdb.syncedgraph import SyncedGraph
 
from standardservice.scalessetup import gatherProcessStats
 

	
 
from greplin import scales
 
import imp
 

	
 
log = logging.getLogger('sequencer')
 

	
 
gatherProcessStats()
 
updateStats = scales.collection(
 
    '/update/',
 
    scales.PmfStat('s0_getMusic', recalcPeriod=1),
 
    scales.PmfStat('s1_eval', recalcPeriod=1),
 
    scales.PmfStat('s2_sendToWeb', recalcPeriod=1),
 
    scales.PmfStat('s3_send', recalcPeriod=1),
 
    scales.PmfStat('sendPhase', recalcPeriod=1),
 
    scales.PmfStat('updateLoopLatency', recalcPeriod=1),
 
    scales.DoubleStat('updateLoopLatencyGoal'),
 
    scales.RecentFpsStat('updateFps'),
 
    scales.DoubleStat('goalFps'),
 
)
 
compileStats = scales.collection(
 
    '/compile/',
 
    scales.PmfStat('graph', recalcPeriod=1),
 
    scales.PmfStat('song', recalcPeriod=1),
 
)
 

	
 

	
 
class Note(object):
 

	
 
    def __init__(self, graph: SyncedGraph, uri: NoteUri, effectevalModule,
 
                 simpleOutputs):
 
        g = self.graph = graph
light9/web/index.html
Show inline comments
 
<!doctype html>
 
<html>
 
  <head>
 
    <title>light9 home</title>
 
    <meta charset="utf-8" />
 
    <script src="/node_modules/@webcomponents/webcomponentsjs/webcomponents-lite.js"></script>
 
    <link rel="stylesheet" href="/style.css">
 
    <link rel="import" href="/lib/polymer/polymer.html">
 
  </head>
 
  <body>
 
      <script type="module"  src="stats-line.js"></script>
 
      <script type="module"  src="stats-process.js"></script>
 

	
 

	
 
     
 
    <dom-module id="service-button-row">
 
      <template>
 
        <style>
 
         :host { padding-bottom: 10px;  }
 
         a {
 
             color: #7d7dec;
 
         }
 
         div {
 
             display: flex;
 
             justify-content: space-between;
 
             padding: 2px 3px;
 
         }
 
         .left {
 
             display: inline-block;
 
             margin-right: 3px;
 
             flex-grow: 1;
 
         }
 
         .window {
 
         }
 
         .serviceGrid > td {
 
             border: 5px solid red;
light9/web/stats-line.js
Show inline comments
 
import { LitElement, TemplateResult, html, css } from '/node_modules/lit-element/lit-element.js';
 
import { rounding }  from '/node_modules/significant-rounding/index.js';
 

	
 
class StatsLine extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            name: {
 
                type: String,
 
                reflect: true,
 
                
 
            },
 
            stats: Object // to be refreshed with ws
 
        };
 
    }
 

	
 
    updated(changedProperties) {
 
        changedProperties.forEach((oldValue, propName) => {
 
            if (propName == 'name') {
 
                const reload = () => {
 
                    fetch(this.name + '/stats/?format=json').then((resp) => {
 
                        if (resp.ok) {
 
                    resp.json().then((msg) => {
 
                        
 
                        this.stats = msg;
 
                    setTimeout(reload, 1000);
 
                            resp.json().then((msg) => {
 
                                this.stats = msg;
 
                                setTimeout(reload, 1000);
 
                            });
 
                        }
 
                    });
 
                        }
 
                });
 
                }
 
                reload();
 
            }
 
        });
 
    }
 
    
 
    static get styles() {
 
        return css`
 
        :host {
 
            border: 2px solid #46a79f;
 
            display: inline-block;
 
        }
 
        table { 
 
            border-collapse: collapse;
 
            background: #000;
 
            color: #ccc;
 
            font-family: sans-serif;
 
        }
 
        th, td { 
 
            outline: 1px solid #000; 
 
        }
 
        th {
 
            padding: 2px 4px;
 
            background: #2f2f2f;
 
@@ -57,95 +56,113 @@ class StatsLine extends LitElement {
 
            vertical-align: top;
 
            text-align: center;
 
        }
 
        td.val {
 
            padding: 2px 4px;
 
            background: #3b5651;
 
        }
 
        .recents { 
 
            display: flex;
 
            align-items: flex-end;
 
            height: 30px;
 
        }
 
        .recents > div {
 
            width: 3px;
 
            background: red;
 
            border-right: 1px solid black;
 
        }
 
        .bigInt {
 
            min-width: 6em;
 
        }
 
        `;
 
    }
 
    
 
    render() {
 
        const now = Date.now() / 1000;
 
        const table = (d, path) => {
 

	
 
            const cols = Object.keys(d);
 
            let cols = Object.keys(d);
 
            cols.sort();
 

	
 
            if (path.length == 0) {
 
                ['webServer', 'process'].forEach((earlyKey) => {
 
                    let i = cols.indexOf(earlyKey);
 
                    if (i != -1) {
 
                        cols = [earlyKey].concat(cols.slice(0, i), cols.slice(i + 1));
 
                    }
 
                });
 
            }
 
            
 
            const th = (col) =>  {
 
                return html`<th>${col}</th>`;
 
            };
 
            const td = (col)  => {
 
                const cell = d[col];
 
                return html`${drawLevel(cell, path.concat(col))}`;
 
            };
 
            return html`
 
             <table>
 
               <tr>
 
                 ${cols.map(th)}
 
               </tr>
 
                 <tr>
 
                   ${cols.map(td)}
 
                 </tr>
 
             </table>`;
 
        };
 
        const tdWrap = (content) => {
 
            return html`<td>${content}</td>`;
 
        }
 
        const recents = (d, path) => {
 
            const hi = Math.max.apply(null, d.recents);
 
            const scl = 30 / hi;
 
            
 
            const bar = (y) => {
 
                let color;
 
                if (y < d.average) {
 
                    color="#6a6aff";
 
                } else {
 
                    color="#d09e4c";
 
                }
 
                return html`<div class="bar" style="height: ${y * scl}px; background: ${color};"></div>`;
 
            };
 
            return html`<td>
 
               <div class="recents">${d.recents.map(bar)}</div>
 
               <div>avg=${rounding(d.average ,3)}</div>`;
 

	
 
        };
 
        const pmf = (d, path) => {
 
            return tdWrap(table({
 
                count: d.count,
 
                'values [ms]': html`
 
                   <div>mean=${rounding(d.mean*1000, 3)}</div>
 
                   <div>sd=${rounding(d.stddev*1000, 3)}</div>
 
                   <div>99=${rounding(d['99percentile']*1000, 3)}</div>
 
                 `
 
            }, path));
 
        };
 
        const drawLevel = (d, path) => {           
 
        const drawLevel = (d, path) => {
 
            if (path.length == 1 && path[0] === 'process') {
 
                 const elem = this.shadowRoot.querySelector('#proc');
 
                if (elem) {
 
                    elem.data = d;
 
                }
 
                return html`<stats-process id="proc"></stats-process>`;
 
            }
 
            if (typeof d === 'object') {
 
                if (d instanceof TemplateResult) {
 
                    return html`<td class="val">${d}</td>`;
 
                } else if (d.count !== undefined && d.min !== undefined) {
 
                    return pmf(d, path);
 
                } else if (d.average !== undefined && d.recents !== undefined) {
 
                    return recents(d, path);
 
                } else {
 
                    return tdWrap(table(d, path));
 
                }
 
            } else {             
 
                return html`<td class="val bigInt">${d}</td>`;
 
            }
 
        };
 
        return table(this.stats || {}, []);
 
    }
 
}
 
customElements.define('stats-line', StatsLine);
light9/web/stats-process.js
Show inline comments
 
new file 100644
 
import { LitElement, TemplateResult, html, css } from '/node_modules/lit-element/lit-element.js';
 
import debug from '/lib/debug/debug-build-es6.js';
 
import { rounding }  from '/node_modules/significant-rounding/index.js';
 

	
 
const log = debug('process');
 

	
 
const remap = (x, lo, hi, outLo, outHi) => {
 
    return outLo + (outHi - outLo) * Math.max(0, Math.min(1, (x - lo) / (hi - lo)));
 
};
 

	
 
class StatsProcess extends LitElement {
 
    
 
    static get properties() {
 
        return {
 
            data: { type: Object },
 
        };
 
    }
 

	
 
    firstUpdated() {
 
        // inspired by https://codepen.io/qiruiyin/pen/qOopQx
 
        var context = this.shadowRoot.firstElementChild,
 
	    ctx = context.getContext('2d'),
 
	    w = 64,
 
	    h = 64,
 
	    revs = 0;   
 
	
 
	context.width = w;
 
	context.height = h;
 

	
 
        let prev = Date.now() / 1000;
 

	
 
        var animate = () => {
 
	    requestAnimationFrame( animate );
 

	
 
            const now = Date.now() / 1000;
 
            ctx.beginPath();
 
            // wrong type of fade- never goes to 0
 
            ctx.fillStyle = '#00000003';
 
            ctx.fillRect(0, 0, w, h);
 
            if (this.data.time < now - 2) {
 
                return;
 
            }
 
            const dt = now - prev;
 
            prev = now;
 

	
 
            const size = remap(this.data.memMb, /*in*/ 20, 600, /*out*/ 3, 30);
 
	    revs += dt * remap(this.data.cpuPercent, /*in*/ 0, 100, /*out*/ 4, 120);
 
            const rad  = remap(size, /*in*/ 3, 30, /*out*/ 14, 5);
 

	
 
	    var x = w/2 + rad * Math.cos(revs / 6.28),
 
		y = h/2 + rad * Math.sin(revs / 6.28);
 

	
 
	    ctx.save();
 
	    ctx.beginPath();
 
	    ctx.fillStyle = "hsl(194, 100%, 42%)";
 
	    ctx.arc(x, y, size, 0, 2*Math.PI);
 
	    ctx.fill();
 
	    ctx.restore();
 
	    
 
        };
 
        animate();
 
    }
 
    
 
    updated(changedProperties) {
 
        if (changedProperties.has('data')) {
 
            this.shadowRoot.firstElementChild.setAttribute('title', `cpu ${this.data.cpuPercent}% mem ${this.data.memMb}MB`);
 
        }
 
    }
 

	
 
    static get styles() {
 
        return css`
 
        :host {
 
           display: inline-block;
 
           width: 64px;
 
           height: 64px;
 
        }
 
        `;
 
    }
 
    
 
    render() {
 
        return html`<canvas></canvas>`;
 

	
 
    }
 
}
 
customElements.define('stats-process', StatsProcess);
 

	
requirements.txt
Show inline comments
 
@@ -24,24 +24,25 @@ statprof==0.1.2
 
toposort==1.5
 
treq==18.6.0
 
txzmq==0.8.0
 
typing==3.6.1
 
watchdog==0.8.3
 
webcolors==1.7
 
udmx-pyusb==2.0.0
 
yapf==0.27.0
 

	
 
coverage==4.3.4
 
flake8
 
hunter
 
ipdb==0.10.2
 
ipython==5.3.0
 
mypy==0.701
 
typing_extensions
 

	
 
git+http://github.com/drewp/scales.git@4b011434f7469a442c3fc1d7e81685c0bfa56eeb#egg=scales
 
git+http://github.com/11craft/louie.git@f18bb71010c114eca9c6b88c96453340e3b39454#egg=louie
 
git+http://github.com/webpy/webpy.git@ace0f8954c28311004b33c7a513c6e40a3c02470#egg=web
 
https://github.com/drewp/cyclone/archive/python3.zip#egg=cyclone
 

	
 
cycloneerr==0.3.0
 
rdfdb==0.19.0
 
standardservice==0.6.0
0 comments (0 inline, 0 general)