From 76e9155823352acc4129c35249f8f90588a4cabc Mon Sep 17 00:00:00 2001 From: Maciej Hirsz <1096222+maciejhirsz@users.noreply.github.com> Date: Mon, 1 Oct 2018 11:41:40 +0200 Subject: [PATCH] Better Sparklines (#72) * Cleaner renders * Add timestamps to Sparklines * Keep track of chart history up to 1h in the past --- packages/backend/src/MeanList.ts | 62 +++++++++++++++++++ packages/backend/src/Node.ts | 28 +++------ packages/common/src/index.ts | 2 +- packages/common/src/types.ts | 2 +- packages/frontend/src/components/Node/Row.tsx | 34 +++++++--- .../frontend/src/components/Sparkline.css | 2 +- .../frontend/src/components/Sparkline.tsx | 13 ++-- packages/frontend/src/state.ts | 4 +- 8 files changed, 110 insertions(+), 37 deletions(-) create mode 100644 packages/backend/src/MeanList.ts diff --git a/packages/backend/src/MeanList.ts b/packages/backend/src/MeanList.ts new file mode 100644 index 0000000..3a8136c --- /dev/null +++ b/packages/backend/src/MeanList.ts @@ -0,0 +1,62 @@ +import { Maybe, Types, timestamp } from '@dotstats/common'; + +export class MeanList { + private periodIndex = 0; + private period = Array(32).fill(0 as T); + private meanIndex = 0; + private means = Array(20).fill(0 as T); + private ticksPerMean = 1; + + public push(val: T) { + this.period[this.periodIndex++] = val; + + if (this.periodIndex === this.ticksPerMean) { + this.pushMean(); + } + } + + public get(): Array { + if (this.meanIndex === 20) { + return this.means; + } else { + return this.means.slice(0, this.meanIndex); + } + } + + private pushMean() { + let sum = 0; + + for (let i = 0; i < this.periodIndex; i++) { + sum += this.period[i] as number; + } + + const mean = (sum / this.periodIndex) as T; + + if (this.meanIndex === 20) { + if (this.ticksPerMean === 32) { + this.means.copyWithin(0, 1); + this.means[20] = mean; + } else { + this.squashMeans(); + this.means[this.meanIndex++] = mean; + } + } else { + this.means[this.meanIndex++] = mean; + } + + this.periodIndex = 0; + } + + private squashMeans() { + this.ticksPerMean *= 2; + + const means = this.means; + + for (let i = 0; i < 10; i++) { + let i2 = i * 2; + means[i] = (((means[i2] as number) + (means[i2 + 1] as number)) / 2) as T; + } + + this.meanIndex = 10; + } +} diff --git a/packages/backend/src/Node.ts b/packages/backend/src/Node.ts index ab967c8..ac5881e 100644 --- a/packages/backend/src/Node.ts +++ b/packages/backend/src/Node.ts @@ -5,6 +5,7 @@ import { noop, timestamp, Maybe, Types, NumStats } from '@dotstats/common'; import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message'; import { locate, Location } from './location'; import { getId, refreshId } from './nodeId'; +import { MeanList } from './MeanList'; const BLOCK_TIME_HISTORY = 10; const MEMORY_RECORDS = 20; @@ -39,8 +40,9 @@ export default class Node { private peers = 0 as Types.PeerCount; private txcount = 0 as Types.TransactionCount; - private memory = Array(); - private cpu = Array(); + private memory = new MeanList(); + private cpu = new MeanList(); + private chartstamps = new MeanList(); private readonly ip: string; private readonly socket: WebSocket; @@ -177,7 +179,7 @@ export default class Node { } public nodeStats(): Types.NodeStats { - return [this.peers, this.txcount, this.memory, this.cpu]; + return [this.peers, this.txcount, this.memory.get(), this.cpu.get(), this.chartstamps.get()]; } public blockDetails(): Types.BlockDetails { @@ -232,22 +234,10 @@ export default class Node { this.peers = peers; this.txcount = txcount; - if (cpu) { - if (this.cpu.length === CPU_RECORDS) { - this.cpu.copyWithin(0, 1); - this.cpu[CPU_RECORDS-1] = cpu; - } else { - this.cpu.push(cpu); - } - } - - if (memory) { - if (this.memory.length === MEMORY_RECORDS) { - this.memory.copyWithin(0, 1); - this.memory[MEMORY_RECORDS-1] = memory; - } else { - this.memory.push(memory); - } + if (cpu != null && memory != null) { + this.cpu.push(cpu); + this.memory.push(memory); + this.chartstamps.push(timestamp()); } this.events.emit('stats'); diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index be20f22..a7d0330 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,4 +8,4 @@ import * as FeedMessage from './feed'; export { Types, FeedMessage }; // Increment this if breaking changes were made to types in `feed.ts` -export const VERSION: Types.FeedVersion = 17 as Types.FeedVersion; +export const VERSION: Types.FeedVersion = 18 as Types.FeedVersion; diff --git a/packages/common/src/types.ts b/packages/common/src/types.ts index feda6c1..dc089eb 100644 --- a/packages/common/src/types.ts +++ b/packages/common/src/types.ts @@ -25,5 +25,5 @@ export type CPUUse = Opaque; export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp, Maybe]; export type NodeDetails = [NodeName, NodeImplementation, NodeVersion, Maybe
]; -export type NodeStats = [PeerCount, TransactionCount, Array, Array]; +export type NodeStats = [PeerCount, TransactionCount, Array, Array, Array]; export type NodeLocation = [Latitude, Longitude, City]; diff --git a/packages/frontend/src/components/Node/Row.tsx b/packages/frontend/src/components/Node/Row.tsx index d3a78c9..efd9417 100644 --- a/packages/frontend/src/components/Node/Row.tsx +++ b/packages/frontend/src/components/Node/Row.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import Identicon from 'polkadot-identicon'; -import { Types } from '@dotstats/common'; +import { Types, Maybe, timestamp } from '@dotstats/common'; import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils'; import { State as AppState, Node } from '../../state'; import { PersistentSet } from '../../persist'; @@ -55,23 +55,37 @@ function Truncate(props: { text: string, position?: 'left' | 'right' | 'center' ); } -function formatMemory(kbs: number): string { +function formatStamp(stamp: Types.Timestamp): string { + const passed = (timestamp() - stamp) / 1000 | 0; + + const hours = Math.round(passed / 3600); + const minutes = Math.round((passed % 3600) / 60); + const seconds = (passed % 60) | 0; + + return hours ? `${hours}h ago` + : minutes ? `${minutes}m ago` + : `${seconds}s ago`; +} + +function formatMemory(kbs: number, stamp: Maybe): string { + const ago = stamp ? ` (${formatStamp(stamp)})` : ''; const mbs = kbs / 1024 | 0; if (mbs >= 1000) { - return `${(mbs / 1024).toFixed(1)} GB`; + return `${(mbs / 1024).toFixed(1)} GB${ago}`; } else { - return `${mbs} MB`; + return `${mbs} MB${ago}`; } } -function formatCPU(cpu: number): string { +function formatCPU(cpu: number, stamp: Maybe): string { + const ago = stamp ? ` (${formatStamp(stamp)})` : ''; const fractionDigits = cpu > 100 ? 0 : cpu > 10 ? 1 : cpu > 1 ? 2 : 3; - return `${cpu.toFixed(fractionDigits)}%`; + return `${cpu.toFixed(fractionDigits)}%${ago}`; } export default class Row extends React.Component { @@ -134,13 +148,13 @@ export default class Row extends React.Component { icon: cpuIcon, width: 40, setting: 'cpu', - render: ({ cpu }) => { + render: ({ cpu, chartstamps }) => { if (cpu.length < 3) { return '-'; } return ( - + ); } }, @@ -149,13 +163,13 @@ export default class Row extends React.Component { icon: memoryIcon, width: 40, setting: 'mem', - render: ({ mem }) => { + render: ({ mem, chartstamps }) => { if (mem.length < 3) { return '-'; } return ( - + ); } }, diff --git a/packages/frontend/src/components/Sparkline.css b/packages/frontend/src/components/Sparkline.css index bb27399..4f7e82b 100644 --- a/packages/frontend/src/components/Sparkline.css +++ b/packages/frontend/src/components/Sparkline.css @@ -2,5 +2,5 @@ fill: currentcolor; /* rgba(255,255,255,0.5); */ fill-opacity: 0.5; stroke: currentcolor; - margin: 0 -14px 0 -4px; + margin: 0 -1px -3px -1px; } diff --git a/packages/frontend/src/components/Sparkline.tsx b/packages/frontend/src/components/Sparkline.tsx index cd92d83..af36c48 100644 --- a/packages/frontend/src/components/Sparkline.tsx +++ b/packages/frontend/src/components/Sparkline.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { Types, Maybe } from "@dotstats/common"; import sparkline from "@fnando/sparkline"; import { Tooltip } from './'; @@ -10,8 +11,9 @@ export namespace Sparkline { width: number; height: number; values: number[]; + stamps?: Types.Timestamp[]; minScale?: number; - format?: (value: number) => string; + format?: (value: number, stamp: Maybe) => string; } } @@ -21,6 +23,8 @@ export class Sparkline extends React.Component { public componentDidMount() { sparkline(this.el, this.props.values, { + spotRadius: 0.1, + minScale: this.props.minScale, interactive: true, onmousemove: this.onMouseMove, }); @@ -35,6 +39,7 @@ export class Sparkline extends React.Component { if (this.props.values !== nextProps.values) { sparkline(this.el, nextProps.values, { + spotRadius: 0.1, minScale, interactive: true, onmousemove: this.onMouseMove, @@ -62,9 +67,9 @@ export class Sparkline extends React.Component { this.update = update; } - private onMouseMove = (event: MouseEvent, data: { value: number }) => { - const { format } = this.props; - const str = format ? format(data.value) : `${data.value}`; + private onMouseMove = (event: MouseEvent, data: { value: number, index: number }) => { + const { format, stamps } = this.props; + const str = format ? format(data.value, stamps ? stamps[data.index] : null) : `${data.value}`; this.update(str); } } diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index 9dfc095..91d4507 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -29,6 +29,7 @@ export class Node { public txs: Types.TransactionCount; public mem: Types.MemoryUse[]; public cpu: Types.CPUUse[]; + public chartstamps: Types.Timestamp[]; public height: Types.BlockNumber; public hash: Types.BlockHash; @@ -67,12 +68,13 @@ export class Node { } public updateStats(stats: Types.NodeStats) { - const [peers, txs, mem, cpu] = stats; + const [peers, txs, mem, cpu, chartstamps] = stats; this.peers = peers; this.txs = txs; this.mem = mem; this.cpu = cpu; + this.chartstamps = chartstamps; } public updateBlock(block: Types.BlockDetails) {