Added ago timers

This commit is contained in:
maciejhirsz
2018-07-05 17:04:00 +02:00
parent 81ef3ee14e
commit 01da7dfc47
19 changed files with 200 additions and 48 deletions
+8 -4
View File
@@ -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}`);
}
+10 -3
View File
@@ -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);
}
+3 -10
View File
@@ -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);
+17 -7
View File
@@ -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;
+11 -4
View File
@@ -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
+4 -2
View File
@@ -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;
+2 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
.App {
text-align: center;
text-align: left;
font-family: monospace, sans-serif;
}
+11 -5
View File
@@ -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>
+75
View File
@@ -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;
}
+19
View File
@@ -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';
+19 -7
View File
@@ -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;
}
}
}
+3 -1
View File
@@ -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>
}
+1
View File
@@ -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,