mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-05-09 04:37:58 +00:00
Added ago timers
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import * as EventEmitter from 'events';
|
||||
import Node from './node';
|
||||
import Feed from './feed';
|
||||
import { Types, IdSet, FeedMessage } from '@dotstats/common';
|
||||
import { timestamp, Types, IdSet, FeedMessage } from '@dotstats/common';
|
||||
|
||||
export default class Aggregator extends EventEmitter {
|
||||
private nodes = new IdSet<Types.NodeId, Node>();
|
||||
@@ -9,6 +9,7 @@ export default class Aggregator extends EventEmitter {
|
||||
private messages: Array<FeedMessage.Message> = [];
|
||||
|
||||
public height = 0 as Types.BlockNumber;
|
||||
public blockTimestamp = 0 as Types.Timestamp;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -34,7 +35,7 @@ export default class Aggregator extends EventEmitter {
|
||||
public addFeed(feed: Feed) {
|
||||
this.feeds.add(feed);
|
||||
|
||||
const messages = [Feed.bestBlock(this.height)];
|
||||
const messages = [Feed.timeSync(), Feed.bestBlock(this.height, this.blockTimestamp)];
|
||||
|
||||
for (const node of this.nodes.values()) {
|
||||
messages.push(Feed.addedNode(node));
|
||||
@@ -69,18 +70,21 @@ export default class Aggregator extends EventEmitter {
|
||||
}
|
||||
|
||||
private timeoutCheck() {
|
||||
const now = Date.now();
|
||||
const now = timestamp();
|
||||
|
||||
for (const node of this.nodes.values()) {
|
||||
node.timeoutCheck(now);
|
||||
}
|
||||
|
||||
this.broadcast(Feed.timeSync());
|
||||
}
|
||||
|
||||
private updateBlock(node: Node) {
|
||||
if (node.height > this.height) {
|
||||
this.height = node.height;
|
||||
this.blockTimestamp = node.blockTimestamp;
|
||||
|
||||
this.broadcast(Feed.bestBlock(this.height));
|
||||
this.broadcast(Feed.bestBlock(this.height, this.blockTimestamp));
|
||||
|
||||
console.log(`New block ${this.height}`);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as WebSocket from 'ws';
|
||||
import * as EventEmitter from 'events';
|
||||
import Node from './node';
|
||||
import { Opaque, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
||||
import { timestamp, Opaque, FeedMessage, Types, idGenerator } from '@dotstats/common';
|
||||
|
||||
const nextId = idGenerator<Types.FeedId>();
|
||||
const { Actions } = FeedMessage;
|
||||
@@ -21,10 +21,10 @@ export default class Feed extends EventEmitter {
|
||||
socket.on('close', () => this.disconnect());
|
||||
}
|
||||
|
||||
public static bestBlock(height: Types.BlockNumber): FeedMessage.Message {
|
||||
public static bestBlock(height: Types.BlockNumber, ts: Types.Timestamp): FeedMessage.Message {
|
||||
return {
|
||||
action: Actions.BestBlock,
|
||||
payload: height
|
||||
payload: [height, ts]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ export default class Feed extends EventEmitter {
|
||||
};
|
||||
}
|
||||
|
||||
public static timeSync(): FeedMessage.Message {
|
||||
return {
|
||||
action: Actions.TimeSync,
|
||||
payload: timestamp()
|
||||
};
|
||||
}
|
||||
|
||||
public sendData(data: FeedMessage.Data) {
|
||||
this.socket.send(data);
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import * as WebSocket from 'ws';
|
||||
import * as express from 'express';
|
||||
import { createServer } from 'http';
|
||||
import Node from './node';
|
||||
import Feed from './feed';
|
||||
import Aggregator from './aggregator';
|
||||
|
||||
const aggregator = new Aggregator;
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
|
||||
// WebSocket for Nodes feeding telemetry data to the server
|
||||
const incomingTelemetry = new WebSocket.Server({ port: 1024 });
|
||||
|
||||
// WebSocket for web clients listening to the telemetry data aggregate
|
||||
const telemetryFeed = new WebSocket.Server({ server });
|
||||
const telemetryFeed = new WebSocket.Server({ port: 8080 });
|
||||
|
||||
app.get('/', function (req, res) {
|
||||
res.send('See live listing at <a href="http://telemetry.polkadot.io/">telemetry.polkadot.io/<a>');
|
||||
});
|
||||
console.log('Telemetry server listening on port 1024');
|
||||
console.log('Feed server listening on port 8080');
|
||||
|
||||
incomingTelemetry.on('connection', async (socket: WebSocket) => {
|
||||
try {
|
||||
@@ -31,5 +26,3 @@ telemetryFeed.on('connection', (socket: WebSocket) => {
|
||||
aggregator.addFeed(new Feed(socket));
|
||||
});
|
||||
|
||||
console.log('Starting server on port 8080');
|
||||
server.listen(8080);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as WebSocket from 'ws';
|
||||
import * as EventEmitter from 'events';
|
||||
import { Maybe, Types, idGenerator } from '@dotstats/common';
|
||||
import { timestamp, Maybe, Types, idGenerator } from '@dotstats/common';
|
||||
import { parseMessage, getBestBlock, Message, BestBlock, SystemInterval } from './message';
|
||||
|
||||
const BLOCK_TIME_HISTORY = 10;
|
||||
const TIMEOUT = 1000 * 60 * 1; // 1 minute
|
||||
const TIMEOUT = (1000 * 60 * 1) as Types.Milliseconds; // 1 minute
|
||||
|
||||
const nextId = idGenerator<Types.NodeId>();
|
||||
|
||||
export default class Node extends EventEmitter {
|
||||
public lastMessage: number;
|
||||
public lastMessage: Types.Timestamp;
|
||||
public id: Types.NodeId;
|
||||
public name: Types.NodeName;
|
||||
public implementation: Types.NodeImplementation;
|
||||
@@ -19,6 +19,7 @@ export default class Node extends EventEmitter {
|
||||
public height = 0 as Types.BlockNumber;
|
||||
public latency = 0 as Types.Milliseconds;
|
||||
public blockTime = 0 as Types.Milliseconds;
|
||||
public blockTimestamp = 0 as Types.Timestamp;
|
||||
|
||||
private peers = 0 as Types.PeerCount;
|
||||
private txcount = 0 as Types.TransactionCount;
|
||||
@@ -36,7 +37,7 @@ export default class Node extends EventEmitter {
|
||||
) {
|
||||
super();
|
||||
|
||||
this.lastMessage = Date.now();
|
||||
this.lastMessage = timestamp();
|
||||
this.id = nextId();
|
||||
this.socket = socket;
|
||||
this.name = name;
|
||||
@@ -53,7 +54,7 @@ export default class Node extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastMessage = Date.now();
|
||||
this.lastMessage = timestamp();
|
||||
this.updateLatency(message.ts);
|
||||
|
||||
const update = getBestBlock(message);
|
||||
@@ -111,7 +112,7 @@ export default class Node extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
public timeoutCheck(now: number) {
|
||||
public timeoutCheck(now: Types.Timestamp) {
|
||||
if (this.lastMessage + TIMEOUT < now) {
|
||||
this.disconnect();
|
||||
}
|
||||
@@ -126,7 +127,7 @@ export default class Node extends EventEmitter {
|
||||
}
|
||||
|
||||
public blockDetails(): Types.BlockDetails {
|
||||
return [this.height, this.best, this.blockTime];
|
||||
return [this.height, this.best, this.blockTime, this.blockTimestamp];
|
||||
}
|
||||
|
||||
public get average(): number {
|
||||
@@ -147,6 +148,14 @@ export default class Node extends EventEmitter {
|
||||
return sum / accounted;
|
||||
}
|
||||
|
||||
public get localBlockAt(): Types.Milliseconds {
|
||||
if (!this.lastBlockAt) {
|
||||
return 0 as Types.Milliseconds;
|
||||
}
|
||||
|
||||
return +(this.lastBlockAt || 0) as Types.Milliseconds;
|
||||
}
|
||||
|
||||
private disconnect() {
|
||||
this.socket.removeAllListeners();
|
||||
this.socket.close();
|
||||
@@ -177,6 +186,7 @@ export default class Node extends EventEmitter {
|
||||
|
||||
this.best = best;
|
||||
this.height = height;
|
||||
this.blockTimestamp = timestamp();
|
||||
this.lastBlockAt = time;
|
||||
this.blockTimes[height % BLOCK_TIME_HISTORY] = blockTime;
|
||||
this.blockTime = blockTime;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Opaque } from './helpers';
|
||||
import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails } from './types';
|
||||
import { NodeId, NodeDetails, NodeStats, BlockNumber, BlockDetails, Timestamp } from './types';
|
||||
|
||||
export const Actions = {
|
||||
BestBlock: 0 as 0,
|
||||
@@ -7,6 +7,7 @@ export const Actions = {
|
||||
RemovedNode: 2 as 2,
|
||||
ImportedBlock: 3 as 3,
|
||||
NodeStats: 4 as 4,
|
||||
TimeSync: 5 as 5,
|
||||
};
|
||||
|
||||
export type Action = typeof Actions[keyof typeof Actions];
|
||||
@@ -19,7 +20,7 @@ export namespace Variants {
|
||||
|
||||
export interface BestBlockMessage extends MessageBase {
|
||||
action: typeof Actions.BestBlock;
|
||||
payload: BlockNumber;
|
||||
payload: [BlockNumber, Timestamp];
|
||||
}
|
||||
|
||||
export interface AddedNodeMessage extends MessageBase {
|
||||
@@ -40,7 +41,12 @@ export namespace Variants {
|
||||
export interface NodeStatsMessage extends MessageBase {
|
||||
action: typeof Actions.NodeStats;
|
||||
payload: [NodeId, NodeStats];
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimeSyncMessage extends MessageBase {
|
||||
action: typeof Actions.TimeSync;
|
||||
payload: Timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
export type Message =
|
||||
@@ -48,7 +54,8 @@ export type Message =
|
||||
| Variants.AddedNodeMessage
|
||||
| Variants.RemovedNodeMessage
|
||||
| Variants.ImportedBlockMessage
|
||||
| Variants.NodeStatsMessage;
|
||||
| Variants.NodeStatsMessage
|
||||
| Variants.TimeSyncMessage;
|
||||
|
||||
/**
|
||||
* Opaque data type to be sent to the feed. Passing through
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Milliseconds } from './types';
|
||||
import { Milliseconds, Timestamp } from './types';
|
||||
|
||||
/**
|
||||
* PhantomData akin to Rust, because sometimes you need to be smarter than
|
||||
* the compiler.
|
||||
*/
|
||||
export abstract class PhantomData<P> { private __PHANTOM__: P }
|
||||
export abstract class PhantomData<P> { public __PHANTOM__: P }
|
||||
|
||||
/**
|
||||
* Opaque type, similar to `opaque type` in Flow, or new types in Rust/C.
|
||||
@@ -31,3 +31,5 @@ export function sleep(time: Milliseconds): Promise<void> {
|
||||
setTimeout(() => resolve(), time);
|
||||
});
|
||||
}
|
||||
|
||||
export const timestamp = Date.now as () => Timestamp;
|
||||
|
||||
@@ -9,9 +9,10 @@ export type NodeVersion = Opaque<string, 'NodeVersion'>;
|
||||
export type BlockNumber = Opaque<number, 'BlockNumber'>;
|
||||
export type BlockHash = Opaque<string, 'BlockHash'>;
|
||||
export type Milliseconds = Opaque<number, 'Milliseconds'>;
|
||||
export type Timestamp = Opaque<Milliseconds, 'Timestamp'>;
|
||||
export type PeerCount = Opaque<number, 'PeerCount'>;
|
||||
export type TransactionCount = Opaque<number, 'TransactionCount'>;
|
||||
|
||||
export type BlockDetails = [BlockNumber, BlockHash, Milliseconds];
|
||||
export type BlockDetails = [BlockNumber, BlockHash, Milliseconds, Timestamp];
|
||||
export type NodeDetails = [NodeName, NodeImplementation, NodeVersion];
|
||||
export type NodeStats = [PeerCount, TransactionCount];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
font-family: monospace, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Node } from './Node';
|
||||
import { Icon } from './Icon';
|
||||
import { Node, Icon, Tile, Ago } from './components';
|
||||
import { Connection } from './message';
|
||||
import { State } from './state';
|
||||
import { formatNumber } from './utils';
|
||||
@@ -14,10 +13,13 @@ import transactionsIcon from './icons/inbox.svg';
|
||||
import blockIcon from './icons/package.svg';
|
||||
import blockHashIcon from './icons/file-binary.svg';
|
||||
import blockTimeIcon from './icons/history.svg';
|
||||
import lastTimeIcon from './icons/dashboard.svg';
|
||||
|
||||
export default class App extends React.Component<{}, State> {
|
||||
public state: State = {
|
||||
best: 0 as Types.BlockNumber,
|
||||
blockTimestamp: 0 as Types.Timestamp,
|
||||
timeDiff: 0 as Types.Milliseconds,
|
||||
nodes: new Map()
|
||||
};
|
||||
|
||||
@@ -28,11 +30,14 @@ export default class App extends React.Component<{}, State> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { best, blockTimestamp, timeDiff } = this.state;
|
||||
|
||||
Ago.timeDiff = timeDiff;
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<div className="App-header">
|
||||
<Icon src={blockIcon} alt="Best Block" /> #{formatNumber(this.state.best)}
|
||||
</div>
|
||||
<Tile icon={blockIcon} title="Best Block">#{formatNumber(best)}</Tile>
|
||||
<Tile icon={lastTimeIcon} title="Last Block"><Ago when={blockTimestamp} /></Tile>
|
||||
<table className="App-list">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -43,6 +48,7 @@ export default class App extends React.Component<{}, State> {
|
||||
<th><Icon src={blockIcon} alt="Block" /></th>
|
||||
<th><Icon src={blockHashIcon} alt="Block Hash" /></th>
|
||||
<th><Icon src={blockTimeIcon} alt="Block Time" /></th>
|
||||
<th><Icon src={lastTimeIcon} alt="Last Block Time" /></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
import './Tile.css';
|
||||
import { timestamp, Types } from '@dotstats/common';
|
||||
|
||||
export namespace Ago {
|
||||
export interface Props {
|
||||
when: Types.Timestamp,
|
||||
}
|
||||
|
||||
export interface State {
|
||||
now: Types.Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
const tickers = new Map<Ago, (ts: Types.Timestamp) => void>();
|
||||
|
||||
function tick() {
|
||||
const now = timestamp();
|
||||
|
||||
for (const ticker of tickers.values()) {
|
||||
ticker(now);
|
||||
}
|
||||
|
||||
setTimeout(tick, 100);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
export namespace Ago {
|
||||
export interface State {
|
||||
now: Types.Timestamp
|
||||
}
|
||||
}
|
||||
|
||||
export class Ago extends React.Component<Ago.Props, Ago.State> {
|
||||
public static timeDiff = 0 as Types.Milliseconds;
|
||||
|
||||
public state: Ago.State;
|
||||
|
||||
constructor(props: Ago.Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
now: (timestamp() + Ago.timeDiff) as Types.Timestamp
|
||||
};
|
||||
}
|
||||
|
||||
public componentWillMount() {
|
||||
tickers.set(this, (now) => {
|
||||
this.setState({
|
||||
now: (now + Ago.timeDiff) as Types.Timestamp
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
tickers.delete(this);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const ago = Math.max(this.state.now - this.props.when, 0) / 1000;
|
||||
|
||||
let agoStr: string;
|
||||
|
||||
if (ago < 10) {
|
||||
agoStr = `${ago.toFixed(1)}s`;
|
||||
} else if (ago < 60) {
|
||||
agoStr = `${ago | 0}s`;
|
||||
} else {
|
||||
agoStr = `${ ago / 60 | 0}m`;
|
||||
}
|
||||
|
||||
return <span title={new Date(this.props.when).toUTCString()}>{agoStr} ago</span>
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { formatNumber, trimHash } from './utils';
|
||||
import { formatNumber, trimHash } from '../utils';
|
||||
import { Ago } from './Ago';
|
||||
import { Types } from '@dotstats/common';
|
||||
|
||||
export namespace Node {
|
||||
@@ -13,7 +14,7 @@ export namespace Node {
|
||||
|
||||
export function Node(props: Node.Props) {
|
||||
const [name, implementation, version] = props.nodeDetails;
|
||||
const [height, hash, blockTime] = props.blockDetails;
|
||||
const [height, hash, blockTime, blockTimestamp] = props.blockDetails;
|
||||
const [peers, txcount] = props.nodeStats;
|
||||
|
||||
return (
|
||||
@@ -24,7 +25,8 @@ export function Node(props: Node.Props) {
|
||||
<td>{txcount}</td>
|
||||
<td>#{formatNumber(height)}</td>
|
||||
<td><span title={hash}>{trimHash(hash, 16)}</span></td>
|
||||
<td>{blockTime / 1000}s</td>
|
||||
<td>{(blockTime / 1000).toFixed(3)}s</td>
|
||||
<td><Ago when={blockTimestamp} /></td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
.Tile {
|
||||
font-size: 2.5em;
|
||||
padding: 20px;
|
||||
text-align: left;
|
||||
width: 7em;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from 'react';
|
||||
import './Tile.css';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export namespace Tile {
|
||||
export interface Props {
|
||||
title: string,
|
||||
icon: string,
|
||||
children?: React.ReactNode,
|
||||
}
|
||||
}
|
||||
|
||||
export function Tile(props: Tile.Props) {
|
||||
return (
|
||||
<div className="Tile">
|
||||
<Icon src={props.icon} alt={props.title} /> {props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './Icon';
|
||||
export * from './Node';
|
||||
export * from './Tile';
|
||||
export * from './Ago';
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||
import { timestamp, FeedMessage, Types, Maybe, sleep } from '@dotstats/common';
|
||||
import { State, Update } from './state';
|
||||
|
||||
const { Actions } = FeedMessage;
|
||||
@@ -13,7 +13,6 @@ export class Connection {
|
||||
|
||||
private static readonly address = `ws://${window.location.hostname}:8080`;
|
||||
|
||||
|
||||
private static async socket(): Promise<WebSocket> {
|
||||
let socket = await Connection.trySocket();
|
||||
let timeout = TIMEOUT_BASE;
|
||||
@@ -61,11 +60,11 @@ export class Connection {
|
||||
constructor(socket: WebSocket, update: Update) {
|
||||
this.socket = socket;
|
||||
this.update = update;
|
||||
this.state = update(null);
|
||||
this.bindSocket();
|
||||
}
|
||||
|
||||
private bindSocket() {
|
||||
this.state = this.update({ nodes: new Map() });
|
||||
this.socket.addEventListener('message', this.handleMessages);
|
||||
this.socket.addEventListener('close', this.handleDisconnect);
|
||||
this.socket.addEventListener('error', this.handleDisconnect);
|
||||
@@ -85,10 +84,13 @@ export class Connection {
|
||||
messages: for (const message of FeedMessage.deserialize(data)) {
|
||||
switch (message.action) {
|
||||
case Actions.BestBlock: {
|
||||
this.state = this.update({ best: message.payload });
|
||||
const [best, blockTimestamp] = message.payload;
|
||||
|
||||
this.state = this.update({ best, blockTimestamp });
|
||||
|
||||
continue messages;
|
||||
}
|
||||
|
||||
case Actions.AddedNode: {
|
||||
const [id, nodeDetails, nodeStats, blockDetails] = message.payload;
|
||||
const node = { id, nodeDetails, nodeStats, blockDetails };
|
||||
@@ -97,14 +99,15 @@ export class Connection {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.RemovedNode: {
|
||||
nodes.delete(message.payload);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.ImportedBlock: {
|
||||
const [id, blockDetails] = message.payload;
|
||||
|
||||
const node = nodes.get(id);
|
||||
|
||||
if (!node) {
|
||||
@@ -115,9 +118,9 @@ export class Connection {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.NodeStats: {
|
||||
const [id, nodeStats] = message.payload;
|
||||
|
||||
const node = nodes.get(id);
|
||||
|
||||
if (!node) {
|
||||
@@ -128,8 +131,17 @@ export class Connection {
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case Actions.TimeSync: {
|
||||
this.state = this.update({
|
||||
timeDiff: (timestamp() - message.payload) as Types.Milliseconds
|
||||
});
|
||||
|
||||
continue messages;
|
||||
}
|
||||
|
||||
default: {
|
||||
return;
|
||||
continue messages;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Node } from './Node';
|
||||
import { Node } from './components/Node';
|
||||
|
||||
export interface State {
|
||||
best: Types.BlockNumber,
|
||||
blockTimestamp: Types.Timestamp,
|
||||
timeDiff: Types.Milliseconds,
|
||||
nodes: Map<Types.NodeId, Node.Props>
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
},
|
||||
"rules": {
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"no-console": false,
|
||||
"no-unused-variable": [true, {"ignore-pattern": "^_"}],
|
||||
"no-empty": false,
|
||||
|
||||
Reference in New Issue
Block a user