diff --git a/light9/collector/collector_client_asyncio.py b/light9/collector/collector_client_asyncio.py --- a/light9/collector/collector_client_asyncio.py +++ b/light9/collector/collector_client_asyncio.py @@ -4,15 +4,19 @@ from light9.collector.collector_client i 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() diff --git a/light9/effect/sequencer/eval_faders.py b/light9/effect/sequencer/eval_faders.py --- a/light9/effect/sequencer/eval_faders.py +++ b/light9/effect/sequencer/eval_faders.py @@ -3,6 +3,8 @@ 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 @@ -16,6 +18,8 @@ from light9.typedgraph import typedValue log = logging.getLogger('seq.fader') +COMPILE=Summary('compile_graph_fader', '') + @dataclass class Fader: graph: SyncedGraph @@ -46,7 +50,7 @@ class FaderEval: 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 = [] diff --git a/light9/homepage/ServiceButtonRow.ts b/light9/homepage/ServiceButtonRow.ts --- a/light9/homepage/ServiceButtonRow.ts +++ b/light9/homepage/ServiceButtonRow.ts @@ -5,10 +5,12 @@ 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; @@ -50,10 +52,10 @@ export class ServiceButtonRow extends Li
${this.name}
-
metrics
+ ${this.hasMetrics ? html`
metrics
` : ""}
-
+ ${this.hasMetrics ? html`
` : ""} `; } diff --git a/light9/homepage/StatsLine.ts b/light9/homepage/StatsLine.ts --- a/light9/homepage/StatsLine.ts +++ b/light9/homepage/StatsLine.ts @@ -2,19 +2,38 @@ import { css, html, LitElement, Template 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 = "?"; @@ -113,119 +132,124 @@ export class StatsLine extends LitElemen `, ]; + tdWrap(content: TemplateResult): TemplateResult { + return html`${content}`; + } + + recents(d: any, path: string[]): TemplateResult { + const hi = Math.max.apply(null, d.recents); + const scl = 30 / hi; + + const bar = (y: number) => { + let color; + if (y < d.average) { + color = "#6a6aff"; + } else { + color = "#d09e4c"; + } + return html`
`; + }; + return html` +
${d.recents.map(bar)}
+
avg=${d.average.toPrecision(3)}
+ `; + } + + table(d: Metrics, path: string[]): TemplateResult { + const byName = new Map(); + 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`${col}`; + }; + const td = (col: string): TemplateResult => { + const cell = byName.get(col)!; + return html`${this.drawLevel(cell, path.concat(col))}`; + }; + return html` + + ${cols.map(th)} + + + ${cols.map(td)} + +
`; + } + + drawLevel(d: Metric, path: string[]) { + return html`[NEW ${JSON.stringify(d)} ${path}]`; + } + + + 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 { + throw m.type; + } + } + + private histoDisplay(b: { [value: string]: string }) { + const lines: TemplateResult[] = []; + let firstLevel; + let lastLevel; + let prev = 0; + + 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`
` + ); + } + return html`${firstLevel} ${lines} ${lastLevel}`; + } + render() { const now = Date.now() / 1000; - const table = (d: Metrics, path: string[]): TemplateResult => { - const byName = new Map(); - 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`${col}`; - }; - const td = (col: string): TemplateResult => { - const cell = byName.get(col)!; - return html`${drawLevel(cell, path.concat(col))}`; - }; - return html` - - ${cols.map(th)} - - - ${cols.map(td)} - -
`; - }; - - const tdWrap = (content: TemplateResult): TemplateResult => { - return html`${content}`; - }; - - 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"; - } else { - color = "#d09e4c"; - } - return html`
`; - }; - return html` -
${d.recents.map(bar)}
-
avg=${rounding(d.average, 3)}
- `; - }; - - const pmf = (d: Metrics, path: string[]) => { - return tdWrap( - table( - { - count: d.count, - "values [ms]": html` -
mean=${rounding(d.mean * 1000, 3)}
-
sd=${rounding(d.stddev * 1000, 3)}
-
99=${rounding(d["99percentile"] * 1000, 3)}
- `, - }, - path - ) - ); - }; - - 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``; - } - if (typeof d === "object") { - if (d.strings) { - //} instanceof TemplateResult) { - return html`${d}`; - } 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`${d}`; - } - }; - - 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" - ); - }; - const displayedStats = this.stats.filter(nonBoring); return html`
@@ -233,14 +257,14 @@ export class StatsLine extends LitElemen ${displayedStats.map( (row, rowNum) => html` - ${row.name} + ${row.type.slice(0, 1)} ${row.name} ${row.metrics.map( (v) => html` - + ` )} diff --git a/light9/homepage/StatsProcess.ts b/light9/homepage/StatsProcess.ts --- a/light9/homepage/StatsProcess.ts +++ b/light9/homepage/StatsProcess.ts @@ -59,7 +59,7 @@ export class StatsProcess extends LitEle 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); diff --git a/light9/homepage/index.html b/light9/homepage/index.html --- a/light9/homepage/index.html +++ b/light9/homepage/index.html @@ -1,34 +1,21 @@ - +light9 home - +

light9 home page

- -
- -
- - -
- - - - - - - - + + + + + +
- diff --git a/light9/web/floating_color_picker.ts b/light9/web/floating_color_picker.ts --- a/light9/web/floating_color_picker.ts +++ b/light9/web/floating_color_picker.ts @@ -8,7 +8,7 @@ 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)); }
${JSON.stringify(v.labels)}${v.value}${this.valueDisplay(row, v)}