Changeset - e462853f1ef6
[Not reviewed]
default
0 7 0
drewp@bigasterisk.com - 20 months ago 2023-05-30 02:35:37
drewp@bigasterisk.com
redo homepage and metrics calcs. still a mess
7 files changed with 123 insertions and 102 deletions:
0 comments (0 inline, 0 general)
light9/collector/collector_client_asyncio.py
Show inline comments
 
import logging
 
from light9 import networking
 
from light9.collector.collector_client import toCollectorJson
 
from light9.effect.settings import DeviceSettings
 
import aiohttp
 

	
 
from prometheus_client import Summary
 

	
 
log = logging.getLogger('coll_client')
 

	
 
SESS = Summary('coll_client_new_session', 'aiohttp.ClientSession')
 

	
 

	
 
class _Sender:
 

	
 
    def __init__(self):
 
        self.reconnect()
 

	
 
    @SESS.time()
 
    def reconnect(self):
 
        self.http_session = aiohttp.ClientSession()
 

	
 
    async def send(self, client: str, session: str, settings: DeviceSettings):
 
        msg = toCollectorJson(client, session, settings).encode('utf8')
 

	
 
        async def put():
 
            async with self.http_session.put(networking.collector.path('attrs'), data=msg, timeout=.2) as resp:
 
                if resp.status != 202:
 
                    body = await resp.text()
 
                    self.reconnect()
 
                    raise ValueError(f'collector returned {resp.status}: {body}')
light9/effect/sequencer/eval_faders.py
Show inline comments
 
import logging
 
import time
 
from dataclasses import dataclass
 
from typing import List, Optional, cast
 

	
 
from prometheus_client import Summary
 

	
 
from rdfdb import SyncedGraph
 
from rdflib import URIRef
 
from rdflib.term import Node
 

	
 
from light9.effect import effecteval
 
from light9.effect.settings import DeviceSettings, EffectSettings
 
from light9.metrics import metrics
 
from light9.namespaces import L9, RDF
 
from light9.newtypes import EffectAttr, EffectUri, UnixTime
 
from light9.typedgraph import typedValue
 

	
 
log = logging.getLogger('seq.fader')
 

	
 
COMPILE=Summary('compile_graph_fader', '')
 

	
 
@dataclass
 
class Fader:
 
    graph: SyncedGraph
 
    uri: URIRef
 
    effect: EffectUri
 
    setEffectAttr: EffectAttr
 

	
 
    value: Optional[float]=None # mutable
 

	
 
    def __post_init__(self):
 
        self.ee = effecteval.EffectEval2(self.graph, self.effect)
 

	
 
@@ -37,25 +41,25 @@ class FaderEval:
 
                 ):
 
        self.graph = graph
 
        self.faders: List[Fader] = []
 

	
 
        log.info('fader adds handler')
 
        self.graph.addHandler(self._compile)
 
        self.lastLoopSucceeded = False
 

	
 
    def onCodeChange(self):
 
        log.debug('seq.onCodeChange')
 
        self.graph.addHandler(self._compile)
 

	
 
    @metrics('compile_graph_fader').time()
 
    @COMPILE.time()
 
    def _compile(self) -> None:
 
        """rebuild our data from the graph"""
 
        self.faders = []
 
        for fader in self.graph.subjects(RDF.type, L9['Fader']):
 
            effect = typedValue(EffectUri, self.graph, fader, L9['effect'])
 
            setting = typedValue(Node, self.graph, fader, L9['setting'])
 
            setAttr = typedValue(EffectAttr,  self.graph, setting, L9['effectAttr'])
 
            self.faders.append(Fader(self.graph, cast(URIRef, fader), effect, setAttr))
 

	
 
        # this could go in a second, smaller addHandler call to avoid rebuilding Fader objs constantly
 
        for f in self.faders:
 
            setting = typedValue(Node, self.graph, f.uri, L9['setting'])            
light9/homepage/ServiceButtonRow.ts
Show inline comments
 
import { LitElement, html, css } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
export { StatsLine } from "./StatsLine";
 

	
 
@customElement("service-button-row")
 
export class ServiceButtonRow extends LitElement {
 
  @property() name: string = "?";
 
  @property({ attribute: "metrics" }) hasMetrics: boolean = false;
 
  static styles = [
 
    css`
 
      :host {
 
        padding-bottom: 10px;
 
        border-bottom: 1px solid #333;
 
      }
 
      a {
 
        color: #7d7dec;
 
      }
 
      div {
 
        display: flex;
 
        justify-content: space-between;
 
        padding: 2px 3px;
 
      }
 
      .left {
 
        display: inline-block;
 
        margin-right: 3px;
 
@@ -41,23 +43,23 @@ export class ServiceButtonRow extends Li
 
      }
 
      :host > div:nth-child(2) {
 
        width: 9em;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <div>
 
        <div class="left"><a class="big" href="${this.name}/">${this.name}</a></div>
 
        <div class="window"><button @click="${this.click}">window</button></div>
 
        <div><a href="${this.name}/metrics">metrics</a></div>
 
        ${this.hasMetrics ? html`<div><a href="${this.name}/metrics">metrics</a></div>` : ""}
 
      </div>
 

	
 
      <div id="stats"><stats-line name="${this.name}"></div>
 
      ${this.hasMetrics ? html`<div id="stats"><stats-line name="${this.name}"></div>` : ""}
 
      `;
 
  }
 

	
 
  click() {
 
    window.open(this.name + "/", "_blank", "scrollbars=1,resizable=1,titlebar=0,location=0");
 
  }
 
}
light9/homepage/StatsLine.ts
Show inline comments
 
import { css, html, LitElement, TemplateResult } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
export { StatsProcess } from "./StatsProcess";
 
import parsePrometheusTextFormat from "parse-prometheus-text-format";
 
import debug from "debug";
 
import { clamp } from "../web/floating_color_picker";
 
const log = debug("home");
 

	
 
interface Value {
 
  labels: { string: string };
 
  value: string;
 
  value?: string;
 
  count?: number;
 
  sum?: number;
 
  buckets?: { [value: string]: string };
 
}
 
interface Metric {
 
  name: string;
 
  help: string;
 
  type: "GAUGE" | "SUMMARY" | "COUNTER";
 
  type: "GAUGE" | "SUMMARY" | "COUNTER" | "HISTOGRAM" | "UNTYPED";
 
  metrics: Value[];
 
}
 
type Metrics = Metric[];
 

	
 
function nonBoring(m: Metric) {
 
  return (
 
    !m.name.endsWith("_created") && //
 
    !m.name.startsWith("python_gc_") &&
 
    m.name != "python_info" &&
 
    m.name != "process_max_fds" &&
 
    m.name != "process_virtual_memory_bytes" &&
 
    m.name != "process_resident_memory_bytes" &&
 
    m.name != "process_start_time_seconds" &&
 
    m.name != "process_cpu_seconds_total"
 
  );
 
}
 

	
 
@customElement("stats-line")
 
export class StatsLine extends LitElement {
 
  @property() name = "?";
 
  @property() stats: Metrics = [];
 

	
 
  prevCpuNow = 0;
 
  prevCpuTotal = 0;
 
  @property() cpu = 0;
 
  @property() mem = 0;
 

	
 
  updated(changedProperties: any) {
 
    changedProperties.forEach((oldValue: any, propName: string) => {
 
@@ -104,152 +123,157 @@ export class StatsLine extends LitElemen
 
      }
 
      .recents > div {
 
        width: 3px;
 
        background: red;
 
        border-right: 1px solid black;
 
      }
 
      .bigInt {
 
        min-width: 6em;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    const now = Date.now() / 1000;
 
  tdWrap(content: TemplateResult): TemplateResult {
 
    return html`<td>${content}</td>`;
 
  }
 

	
 
  recents(d: any, path: string[]): TemplateResult {
 
    const hi = Math.max.apply(null, d.recents);
 
    const scl = 30 / hi;
 

	
 
    const table = (d: Metrics, path: string[]): TemplateResult => {
 
    const bar = (y: number) => {
 
      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=${d.average.toPrecision(3)}</div>
 
    </td>`;
 
  }
 

	
 
  table(d: Metrics, path: string[]): TemplateResult {
 
      const byName = new Map<string, Metric>();
 
      d.forEach((row) => {
 
        byName.set(row.name, row);
 
      });
 
      let cols = d.map((row) => row.name);
 
      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: string): TemplateResult => {
 
        return html`<th>${col}</th>`;
 
      };
 
      const td = (col: string): TemplateResult => {
 
        const cell = byName.get(col)!;
 
        return html`${drawLevel(cell, path.concat(col))}`;
 
      return html`${this.drawLevel(cell, path.concat(col))}`;
 
      };
 
      return html` <table>
 
        <tr>
 
          ${cols.map(th)}
 
        </tr>
 
        <tr>
 
          ${cols.map(td)}
 
        </tr>
 
      </table>`;
 
    };
 
  }
 

	
 
    const tdWrap = (content: TemplateResult): TemplateResult => {
 
      return html`<td>${content}</td>`;
 
    };
 
  drawLevel(d: Metric, path: string[]) {
 
    return html`[NEW ${JSON.stringify(d)} ${path}]`;
 
  }
 

	
 

	
 
    const recents = (d: any, path: string[]) => {
 
      const hi = Math.max.apply(null, d.recents);
 
      const scl = 30 / hi;
 

	
 
      const bar = (y: number) => {
 
        let color;
 
        if (y < d.average) {
 
          color = "#6a6aff";
 
  valueDisplay(m: Metric, v: Value): TemplateResult {
 
    if (m.type == "GAUGE") {
 
      return html`${v.value}`;
 
    } else if (m.type == "COUNTER") {
 
      return html`${v.value}`;
 
    } else if (m.type == "HISTOGRAM") {
 
      return this.histoDisplay(v.buckets!);
 
    } else if (m.type == "UNTYPED") {
 
      return html`${v.value}`;
 
    } else if (m.type == "SUMMARY") {
 
      log(v);
 
      if (!v.count) {
 
        return html`err: summary without count`;
 
      }
 
      return html`c=${v.count} percall=${((v.count && v.sum ? v.sum / v.count : 0) * 1000).toPrecision(3)}ms`;
 
        } else {
 
          color = "#d09e4c";
 
      throw m.type;
 
    }
 
        }
 
        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>
 
      </td>`;
 
    };
 

	
 
  private histoDisplay(b: { [value: string]: string }) {
 
    const lines: TemplateResult[] = [];
 
    let firstLevel;
 
    let lastLevel;
 
    let prev = 0;
 

	
 
    const pmf = (d: Metrics, path: string[]) => {
 
      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
 
        )
 
    let maxDelta = 0;
 
    for (let level in b) {
 
      if (firstLevel === undefined) firstLevel = level;
 
      lastLevel = level;
 
      let count = parseFloat(b[level]);
 
      let delta = count - prev;
 
      prev = count;
 
      if (delta > maxDelta) maxDelta = delta;
 
    }
 
    prev = 0;
 
    const maxBarH = 60;
 
    for (let level in b) {
 
      let count = parseFloat(b[level]);
 
      let delta = count - prev;
 
      prev = count;
 
      let levelf = parseFloat(level);
 
      const h = clamp((delta / maxDelta) * maxBarH, 1, maxBarH);
 
      lines.push(
 
        html`<div
 
          title="bucket=${level} count=${count}"
 
          style="background: yellow; margin-right: 1px; width: 8px; height: ${h}px; display: inline-block"
 
        ></div>`
 
      );
 
    };
 

	
 
    const drawLevel = (d: Metric, path: string[]) => {
 
      if (path.length == 1 && path[0] === "process") {
 
        const elem = this.shadowRoot!.querySelector("#proc");
 
        if (elem) {
 
          (elem as StatsProcess).data = d;
 
        }
 
        return html`<stats-process id="proc"></stats-process>`;
 
    return html`${firstLevel} ${lines} ${lastLevel}`;
 
      }
 
      if (typeof d === "object") {
 
        if (d.strings) {
 
          //} 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>`;
 
      }
 
    };
 

	
 
    const nonBoring = (m: Metric) => {
 
      return (
 
        !m.name.endsWith("_created") && //
 
        !m.name.startsWith("python_gc_") &&
 
        m.name != "python_info" &&
 
        m.name != "process_max_fds" &&
 
        m.name != "process_virtual_memory_bytes" &&
 
        m.name != "process_resident_memory_bytes" &&
 
        m.name != "process_start_time_seconds" &&
 
        m.name != "process_cpu_seconds_total"
 
      );
 
    };
 
  render() {
 
    const now = Date.now() / 1000;
 

	
 
    const displayedStats = this.stats.filter(nonBoring);
 
    return html`
 
      <div>
 
        <table>
 
          ${displayedStats.map(
 
            (row, rowNum) => html`
 
              <tr>
 
                <th>${row.name}</th>
 
                <th>${row.type.slice(0, 1)} ${row.name}</th>
 
                <td>
 
                  <table>
 
                    ${row.metrics.map(
 
                      (v) => html`
 
                        <tr>
 
                          <td>${JSON.stringify(v.labels)}</td>
 
                          <td>${v.value}</td>
 
                          <td>${this.valueDisplay(row, v)}</td>
 
                        </tr>
 
                      `
 
                    )}
 
                  </table>
 
                </td>
 
                ${rowNum == 0
 
                  ? html`
 
                      <td rowspan="${displayedStats.length}">
 
                        <stats-process id="proc" cpu="${this.cpu}" mem="${this.mem}"></stats-process>
 
                      </td>
 
                    `
 
                  : ""}
light9/homepage/StatsProcess.ts
Show inline comments
 
@@ -50,25 +50,25 @@ export class StatsProcess extends LitEle
 
    this.canvas!.setAttribute("title", 
 
    `cpu ${new Number(this.cpu).toPrecision(3)}% mem ${new Number(this.mem).toPrecision(3)}MB`);
 

	
 
    const now = Date.now() / 1000;
 
    const ctx = this.ctx;
 
    ctx.beginPath();
 
    // wrong type of fade- never goes to 0
 
    ctx.fillStyle = "#00000003";
 
    ctx.fillRect(0, 0, this.w, this.h);
 
    const dt = now - this.prev;
 
    this.prev = now;
 

	
 
    const size = remap(this.mem.valueOf() / 1024 / 1024, /*in*/ 20, 600, /*out*/ 3, 30);
 
    const size = remap(this.mem.valueOf() / 1024 / 1024, /*in*/ 20, 80, /*out*/ 3, 30);
 
    this.revs += dt * remap(this.cpu.valueOf(), /*in*/ 0, 100, /*out*/ 4, 120);
 
    const rad = remap(size, /*in*/ 3, 30, /*out*/ 14, 5);
 

	
 
    var x = this.w / 2 + rad * Math.cos(this.revs / 6.28),
 
      y = this.h / 2 + rad * Math.sin(this.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();
light9/homepage/index.html
Show inline comments
 
<!doctype html>
 
<!DOCTYPE html>
 
<html>
 
  <head>
 
    <title>light9 home</title>
 
    <meta charset="utf-8" />
 
    <link rel="stylesheet" href="style.css">
 
    <link rel="stylesheet" href="style.css" />
 
    <script type="module" src="./ServiceButtonRow.ts"></script>
 
  </head>
 
  <body>
 
    <h1>light9 home page</h1>
 

	
 
    <div style="display: grid">
 
      <service-button-row name="rdfdb"></service-button-row>
 
      <hr>
 
      <service-button-row name="ascoltami"></service-button-row>
 
      <hr>
 
      <!--  <service-button-row name="picamserve"></service-button-row> -->  
 
      <service-button-row name="vidref"></service-button-row>
 
      <hr>
 
      <service-button-row name="collector"></service-button-row>
 
      <service-button-row name="effectSequencer"></service-button-row>
 
      <service-button-row name="live"></service-button-row>
 
      <service-button-row name="effects"></service-button-row>
 
      <service-button-row name="timeline"></service-button-row>
 
      <service-button-row name="paint"></service-button-row>
 
      <service-button-row name="effectEval"></service-button-row> 
 
<!--       <hr>
 
      <service-button-row name="subServer"></service-button-row>
 
      <service-button-row name="subComposer"></service-button-row>
 
      -->
 
      <service-button-row name="ascoltami" metrics="1"></service-button-row>
 
      <service-button-row name="fade"></service-button-row>
 
      <service-button-row name="effectListing"></service-button-row>
 
      <service-button-row name="effectSequencer" metrics="1"></service-button-row>
 
      <service-button-row name="collector" metrics="1"></service-button-row>
 
      <service-button-row name="rdfdb" metrics="1"></service-button-row>
 
    </div>
 
    
 
  </body>
 
</html>
light9/web/floating_color_picker.ts
Show inline comments
 
// Note that this file deals only with hue+sat. See Light9ColorPicker for the value component.
 

	
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement, query } from "lit/decorators.js";
 
import color from "onecolor";
 
import { SubEvent } from "sub-events";
 

	
 
const log = debug("control.color.pick");
 

	
 
function clamp(x: number, lo: number, hi: number) {
 
export function clamp(x: number, lo: number, hi: number) {
 
  return Math.max(lo, Math.min(hi, x));
 
}
 

	
 
class RainbowCoord {
 
  // origin is rainbow top-lefft
 
  constructor(public x: number, public y: number) {}
 
}
 

	
 
export class ClientCoord {
 
  //  origin is top-left of client viewport (regardless of page scroll)
 
  constructor(public x: number, public y: number) {}
 
}
0 comments (0 inline, 0 general)