Skip to content
Snippets Groups Projects
Unverified Commit da91e927 authored by Levente Pap's avatar Levente Pap
Browse files

Transition to Analysis Dashboard for visualization

 * Add FPC heartbeat changes to analysis plugin
 * Introduce Dashboard for analysis related visualization
 * Refactor Analysis Dashboard backend
 * Prepare base of frontend
 * Add skeleton of AutopeeringStore in frontend
parent 99da8900
No related branches found
No related tags found
No related merge requests found
Showing
with 11395 additions and 0 deletions
......@@ -50,3 +50,23 @@ func ConvertInt32Opinion(x int32) Opinion {
}
return Unknown
}
// ConvertOpinionToInt32 converts the given Opinion to an int32.
func ConvertOpinionToInt32(x Opinion) int32 {
switch {
case x == Like:
return 1
case x == Dislike:
return 2
}
return 4
}
// ConvertOpinionsToInts32 converts the given slice of Opinion to a slice of int32.
func ConvertOpinionsToInts32(opinions []Opinion) []int32 {
result := make([]int32, len(opinions))
for i, opinion := range opinions {
result[i] = ConvertOpinionToInt32(opinion)
}
return result
}
......@@ -4,6 +4,7 @@ import (
analysisclient "github.com/iotaledger/goshimmer/plugins/analysis/client"
analysisserver "github.com/iotaledger/goshimmer/plugins/analysis/server"
analysiswebinterface "github.com/iotaledger/goshimmer/plugins/analysis/webinterface"
analysisdashboard "github.com/iotaledger/goshimmer/plugins/analysis/dashboard"
"github.com/iotaledger/goshimmer/plugins/remotelog"
"github.com/iotaledger/hive.go/node"
)
......@@ -13,4 +14,5 @@ var PLUGINS = node.Plugins(
analysisserver.Plugin,
analysisclient.Plugin,
analysiswebinterface.Plugin,
analysisdashboard.Plugin,
)
package dashboard
import (
"fmt"
"github.com/gorilla/websocket"
"time"
"github.com/iotaledger/goshimmer/packages/shutdown"
"github.com/iotaledger/hive.go/daemon"
"github.com/iotaledger/hive.go/events"
"github.com/iotaledger/hive.go/workerpool"
analysisserver "github.com/iotaledger/goshimmer/plugins/analysis/server"
)
var (
autopeeringWorkerCount = 1
autopeeringWorkerQueueSize = 500
autopeeringWorkerPool *workerpool.WorkerPool
)
// JSON encoded websocket message for adding a node
type addNode struct {
ID string `json:"id"`
}
// JSON encoded websocket message for removing a node
type removeNode struct {
ID string `json:"id"`
}
// JSON encoded websocket message for connecting two nodes
type connectNodes struct {
Source string `json:"source"`
Target string `json:"target"`
}
// JSON encoded websocket message for disconnecting two nodes
type disconnectNodes struct {
Source string `json:"source"`
Target string `json:"target"`
}
func configureAutopeeringWorkerPool() {
// create a new worker pool for processing autopeering updates coming from analysis server
autopeeringWorkerPool = workerpool.New(func(task workerpool.Task) {
// determine what msg to send based on first parameter
// first parameter is always a letter denoting what to do with the following string or strings
x := fmt.Sprintf("%v",task.Param(0))
switch x {
case "A":
sendAddNode(task.Param(1).(string))
case "a":
sendRemoveNode(task.Param(1).(string))
case "C":
sendConnectNodes(task.Param(1).(string), task.Param(2).(string))
case "c":
sendDisconnectNodes(task.Param(1).(string), task.Param(2).(string))
}
task.Return(nil)
}, workerpool.WorkerCount(autopeeringWorkerCount), workerpool.QueueSize(autopeeringWorkerQueueSize))
}
// send and addNode msg to all connected ws clients
func sendAddNode(nodeID string) {
broadcastWsMessage(&wsmsg{MsgTypeAddNode, &addNode{nodeID}}, true)
}
// send a removeNode msg to all connected ws clients
func sendRemoveNode(nodeID string) {
broadcastWsMessage(&wsmsg{MsgTypeRemoveNode, &removeNode{nodeID}}, true)
}
// send a connectNodes msg to all connected ws clients
func sendConnectNodes(source string, target string) {
broadcastWsMessage(&wsmsg{MsgTypeConnectNodes, &connectNodes{
Source: source,
Target: target,
}}, true)
}
// send disconnectNodes to all connected ws clients
func sendDisconnectNodes(source string, target string) {
broadcastWsMessage(&wsmsg{MsgTypeDisconnectNodes, &disconnectNodes{
Source: source,
Target: target,
}}, true)
}
// runs autopeering feed to propagate autopeering events from analysis server to frontend
func runAutopeeringFeed() {
// closures for the different events
notifyAddNode := events.NewClosure(func(nodeID string) {
autopeeringWorkerPool.Submit("A", nodeID)
})
notifyRemoveNode := events.NewClosure(func(nodeID string) {
autopeeringWorkerPool.Submit("a", nodeID)
})
notifyConnectNodes := events.NewClosure(func(source string, target string) {
autopeeringWorkerPool.Submit("C", source, target)
})
notifyDisconnectNodes := events.NewClosure(func(source string, target string) {
autopeeringWorkerPool.Submit("c", source, target)
})
daemon.BackgroundWorker("Analysis-Dashboard[AutopeeringVisualizer]", func(shutdownSignal <-chan struct{}) {
// connect closures (submitting tasks) to events of the analysis server
analysisserver.Events.AddNode.Attach(notifyAddNode)
defer analysisserver.Events.AddNode.Detach(notifyAddNode)
analysisserver.Events.RemoveNode.Attach(notifyRemoveNode)
defer analysisserver.Events.RemoveNode.Detach(notifyRemoveNode)
analysisserver.Events.ConnectNodes.Attach(notifyConnectNodes)
defer analysisserver.Events.ConnectNodes.Detach(notifyConnectNodes)
analysisserver.Events.DisconnectNodes.Attach(notifyDisconnectNodes)
defer analysisserver.Events.DisconnectNodes.Detach(notifyDisconnectNodes)
autopeeringWorkerPool.Start()
<-shutdownSignal
log.Info("Stopping Analysis-Dashboard[AutopeeringVisualizer] ...")
autopeeringWorkerPool.Stop()
log.Info("Stopping Analysis-Dashboard[AutopeeringVisualizer] ... done")
}, shutdown.PriorityDashboard)
}
// creates event handlers for replaying autopeering events on them
func createAutopeeringEventHandlers(wsClient *websocket.Conn, nodeCallbackFactory func(*websocket.Conn, string) func(string), linkCallbackFactory func(*websocket.Conn, string) func(string, string)) *EventHandlers {
return &EventHandlers{
AddNode: nodeCallbackFactory(wsClient, "A"),
RemoveNode: nodeCallbackFactory(wsClient, "a"),
ConnectNodes: linkCallbackFactory(wsClient, "C"),
DisconnectNodes: linkCallbackFactory(wsClient, "c"),
}
}
// creates callback function for addNode and removeNode events
func createSyncNodeCallback(ws *websocket.Conn, msgType string) func(nodeID string) {
return func(nodeID string) {
var wsMessage *wsmsg
switch msgType {
case "A":
wsMessage = &wsmsg{MsgTypeAddNode, &addNode{nodeID}}
case "a":
wsMessage = &wsmsg{MsgTypeRemoveNode, &removeNode{nodeID}}
}
if err := ws.WriteJSON(wsMessage); err != nil {
return
}
if err := ws.SetWriteDeadline(time.Now().Add(webSocketWriteTimeout)); err != nil {
return
}
}
}
// creates callback function for connectNodes and disconnectNodes events
func createSyncLinkCallback(ws *websocket.Conn, msgType string) func(sourceID string, targetID string){
return func(sourceID string, targetID string) {
var wsMessage *wsmsg
switch msgType {
case "C":
wsMessage = &wsmsg{MsgTypeConnectNodes, &connectNodes{sourceID, targetID}}
case "c":
wsMessage = &wsmsg{MsgTypeDisconnectNodes, &disconnectNodes{sourceID, targetID}}
}
if err := ws.WriteJSON(wsMessage); err != nil {
return
}
if err := ws.SetWriteDeadline(time.Now().Add(webSocketWriteTimeout)); err != nil {
return
}
}
}
// +build !skippackr
// Code generated by github.com/gobuffalo/packr/v2. DO NOT EDIT.
// You can use the "packr clean" command to clean up this,
// and any other packr generated files.
package dashboard
import _ "github.com/iotaledger/goshimmer/plugins/analysis/dashboard/packrd"
package dashboard
import (
"time"
"github.com/iotaledger/goshimmer/packages/shutdown"
"github.com/iotaledger/goshimmer/packages/vote"
"github.com/iotaledger/goshimmer/plugins/analysis/packet"
analysis "github.com/iotaledger/goshimmer/plugins/analysis/server"
"github.com/iotaledger/hive.go/daemon"
"github.com/iotaledger/hive.go/events"
"github.com/iotaledger/hive.go/workerpool"
"github.com/mr-tron/base58"
)
var (
fpcLiveFeedWorkerCount = 1
fpcLiveFeedWorkerQueueSize = 50
fpcLiveFeedWorkerPool *workerpool.WorkerPool
conflicts map[string]Conflict
)
// Conflict defines the struct for the opinions of the nodes regarding a given conflict.
type Conflict struct {
NodesView map[string]voteContext `json:"nodesview"`
}
type voteContext struct {
NodeID string `json:"nodeid"`
Rounds int `json:"rounds"`
Opinions []int32 `json:"opinions"`
Like int32 `json:"like"`
}
// FPCMsg contains an FPC update
type FPCMsg struct {
Nodes int `json:"nodes"`
ConflictSet map[string]Conflict `json:"conflictset"`
}
func configureFPCLiveFeed() {
fpcLiveFeedWorkerPool = workerpool.New(func(task workerpool.Task) {
newMsg := task.Param(0).(*FPCMsg)
broadcastWsMessage(&wsmsg{MsgTypeFPC, newMsg})
task.Return(nil)
}, workerpool.WorkerCount(fpcLiveFeedWorkerCount), workerpool.QueueSize(fpcLiveFeedWorkerQueueSize))
}
func runFPCLiveFeed() {
daemon.BackgroundWorker("Analysis[FPCUpdater]", func(shutdownSignal <-chan struct{}) {
newMsgRateLimiter := time.NewTicker(time.Second / 10)
defer newMsgRateLimiter.Stop()
onFPCHeartbeatReceived := events.NewClosure(func(hb *packet.FPCHeartbeat) {
select {
case <-newMsgRateLimiter.C:
fpcLiveFeedWorkerPool.TrySubmit(createFPCUpdate(hb))
default:
}
})
analysis.Events.FPCHeartbeat.Attach(onFPCHeartbeatReceived)
fpcLiveFeedWorkerPool.Start()
defer fpcLiveFeedWorkerPool.Stop()
<-shutdownSignal
log.Info("Stopping Analysis[FPCUpdater] ...")
analysis.Events.FPCHeartbeat.Detach(onFPCHeartbeatReceived)
log.Info("Stopping Analysis[FPCUpdater] ... done")
}, shutdown.PriorityDashboard)
}
func createFPCUpdate(hb *packet.FPCHeartbeat) *FPCMsg {
update := make(map[string]Conflict)
for ID, context := range hb.RoundStats.ActiveVoteContexts {
update[ID] = newConflict()
nodeID := base58.Encode(hb.OwnID)
update[ID].NodesView[nodeID] = voteContext{
NodeID: nodeID,
Rounds: context.Rounds,
Opinions: vote.ConvertOpinionsToInts32(context.Opinions),
}
}
return &FPCMsg{
ConflictSet: update,
}
}
func newConflict() Conflict {
return Conflict{
NodesView: make(map[string]voteContext),
}
}
.vscode
.DS_STORE
node_modules
.module-cache
*.log*
build
dist
\ No newline at end of file
{
"arrowParens": "always",
"semi": true,
"useTabs": false,
"tabWidth": 2,
"bracketSpacing": true,
"singleQuote": true
}
# GoShimmer Dashboard
Programmed using modern web technologies.
### Dashboard in dev mode
1. Make sure to set `dashboard.dev` to true, to enable GoShimmer to serve assets
from the webpack-dev-server.
2. Install all needed npm modules via `yarn install`.
3. Run a webpack-dev-server instance by running `yarn start` within the `frontend` directory.
4. Using default port config, you should now be able to access the dashboard under http://127.0.0.1:8081
The Dashboard is hot-reload enabled.
### Pack your changes
We are using [packr2](https://github.com/gobuffalo/packr/tree/master/v2) to wrap all built frontend files into Go files.
1. [Install `packr2`](https://github.com/gobuffalo/packr/tree/master/v2#binary-installation) if not already done.
2. Build Dashboard by running `yarn build` within the `frontend` directory.
3. Change to the `plugins/spa` directory.
4. Run `packr2`.
5. `plugins/spa/packrd` should have been modified.
6. Done. Now you can build goShimmer and your Dashboard changes will be included within the binary.
This diff is collapsed.
{
"name": "goshimmer-dashboard",
"version": "1.0.0",
"private": true,
"description": "GoShimmer Dashboard",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server --mode development --hot --progress --colors --port 9090 --open",
"build": "webpack -p --progress --colors",
"prettier": "prettier --write \"src/**/*.{ts,tsx,css}\""
},
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.2.2",
"@jimp/custom": "^0.10.1",
"@jimp/plugin-blur": "^0.10.1",
"@jimp/plugin-color": "^0.10.1",
"@jimp/plugin-resize": "^0.10.1",
"@types/classnames": "^2.2.7",
"@types/react": "^16.7.20",
"@types/react-dom": "^16.0.11",
"@types/react-router": "^4.4.3",
"@types/webpack": "^4.4.23",
"babel-loader": "^8.0.5",
"css-loader": "^2.1.0",
"file-loader": "^3.0.1",
"html-loader": "^1.0.0-alpha.0",
"html-webpack-plugin": "^3.2.0",
"jquery": "^3.5.0",
"mini-css-extract-plugin": "^0.5.0",
"mobx-react-devtools": "^6.0.3",
"popper.js": "^1.16.1",
"postcss": "^7.0.13",
"postcss-browser-reporter": "^0.5.0",
"postcss-import": "^12.0.1",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.5.0",
"postcss-reporter": "^6.0.1",
"postcss-url": "^8.0.0",
"prettier": "^1.16.0",
"react-hot-loader": "^4.6.3",
"style-loader": "^0.23.1",
"ts-loader": "^5.3.3",
"tsx-control-statements": "2.17.1",
"typescript": "^3.2.4",
"url-loader": "^1.1.2",
"webpack": "^4.43.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.1.14",
"webpack-hot-middleware": "^2.24.3"
},
"resolutions": {
"html-loader/htmlnano/cssnano/postcss-svgo/svgo/js-yaml": "^3.13.1"
},
"dependencies": {
"@types/glob": "^7.1.1",
"apexcharts": "^3.10.1",
"apexcharts-react": "^1.0.0",
"bootstrap": "^4.3.1",
"chart.js": "^2.9.3",
"chartjs-plugin-streaming": "^1.8.0",
"classnames": "^2.2.6",
"dateformat": "^3.0.3",
"favicons-webpack-plugin": "^2.1.0",
"history": "^4.10.1",
"mobx": "^5.15.0",
"mobx-react": "^5.4.3",
"mobx-react-router": "^4.0.5",
"moment": "^2.24.0",
"prettysize": "^2.0.0",
"react": "^16.7.0",
"react-apexcharts": "^1.3.3",
"react-bootstrap": "^1.0.0-beta.16",
"react-chartjs-2": "^2.8.0",
"react-dom": "^16.7.0",
"react-icons": "^3.10.0",
"react-router": "^4.3.1",
"react-router-bootstrap": "^0.25.0",
"react-router-dom": "^5.1.2",
"vivagraphjs": "^0.12.0"
}
}
import * as React from 'react';
import {hot} from 'react-hot-loader/root';
import {Root} from 'app/components/Root';
// render react DOM
export const App = hot(({history}) => <Root history={history}/>);
import * as React from 'react';
import Container from "react-bootstrap/Container";
import Row from "react-bootstrap/Row";
import Col from "react-bootstrap/Col";
import Uptime from "app/components/Uptime";
import Version from "app/components/Version";
import NodeStore from "app/stores/NodeStore";
import {inject, observer} from "mobx-react";
import ListGroup from "react-bootstrap/ListGroup";
import Card from "react-bootstrap/Card";
interface Props {
nodeStore?: NodeStore;
}
@inject("nodeStore")
@observer
export class Autopeering extends React.Component<Props, any> {
render() {
return (
<Container>
<h3>Autopeering</h3>
<Row className={"mb-3"}>
<Col>
<Card>
<Card.Body>
<Card.Title>Node: {this.props.nodeStore.status.id}</Card.Title>
<Row>
<Col>
<ListGroup variant={"flush"}>
<ListGroup.Item><Uptime/></ListGroup.Item>
</ListGroup>
</Col>
<Col>
<ListGroup variant={"flush"}>
<ListGroup.Item><Version/></ListGroup.Item>
</ListGroup>
</Col>
</Row>
</Card.Body>
</Card>
</Col>
</Row>
</Container>
);
}
}
import * as React from 'react';
import NodeStore from "app/stores/NodeStore";
import {inject, observer} from "mobx-react";
import {FPCStore} from "app/stores/FPCStore";
import Row from "react-bootstrap/Row";
import Container from "react-bootstrap/Container";
interface Props {
nodeStore?: NodeStore;
fpcStore?: FPCStore;
}
@inject("nodeStore")
@inject("fpcStore")
@observer
export default class FPC extends React.Component<Props, any> {
render() {
let {nodeGrid} = this.props.fpcStore;
return (
<Container>
<Row>
{nodeGrid}
</Row>
</Container>
);
}
}
import * as React from 'react';
import {inject, observer} from "mobx-react";
import NodeStore from "app/stores/NodeStore";
import Navbar from "react-bootstrap/Navbar";
import Nav from "react-bootstrap/Nav";
import {Autopeering} from "app/components/Autopeering";
import {RouterStore} from 'mobx-react-router';
import {Redirect, Route, Switch} from 'react-router-dom';
import {LinkContainer} from 'react-router-bootstrap';
import FPC from "app/components/FPC";
interface Props {
history: any;
routerStore?: RouterStore;
nodeStore?: NodeStore;
}
@inject("nodeStore")
@inject("routerStore")
@observer
export class Root extends React.Component<Props, any> {
renderDevTool() {
if (process.env.NODE_ENV !== 'production') {
const DevTools = require('mobx-react-devtools').default;
return <DevTools/>;
}
}
componentDidMount(): void {
this.props.nodeStore.connect();
}
render() {
return (
<div className="container">
<Navbar expand="lg" bg="light" variant="light" className={"mb-4"}>
<Navbar.Brand>GoShimmer analyser</Navbar.Brand>
<Nav className="mr-auto">
<LinkContainer to="/dashboard">
<Nav.Link>Autopeering</Nav.Link>
</LinkContainer>
<LinkContainer to="/fpc-example">
<Nav.Link>
Consensus
</Nav.Link>
</LinkContainer>
</Nav>
</Navbar>
<Switch>
<Route exact path="/autopeering" component={Autopeering}/>
<Route exact path="/fpc-example" component={FPC}/>
<Redirect to="/autopeering"/>
</Switch>
{this.props.children}
{this.renderDevTool()}
</div>
);
}
}
import * as React from 'react';
import NodeStore from "app/stores/NodeStore";
import {inject, observer} from "mobx-react";
interface Props {
nodeStore?: NodeStore;
}
@inject("nodeStore")
@observer
export default class Uptime extends React.Component<Props, any> {
render() {
return (
<React.Fragment>
Uptime {this.props.nodeStore.uptime}
</React.Fragment>
);
}
}
import * as React from 'react';
import NodeStore from "app/stores/NodeStore";
import {inject, observer} from "mobx-react";
interface Props {
nodeStore?: NodeStore;
}
@inject("nodeStore")
@observer
export default class Version extends React.Component<Props, any> {
render() {
return (
<React.Fragment>
Version {this.props.nodeStore.status.version}
</React.Fragment>
);
}
}
export enum WSMsgType {
Status,
MPSMetrics,
Message,
NeighborStats,
Drng,
TipsMetrics,
Vertex,
TipInfo,
FPC,
AddNode,
RemoveNode,
ConnectNodes,
DisconnectNodes,
}
export interface WSMessage {
type: number;
data: any;
}
type DataHandler = (data: any) => void;
let handlers = {};
export function registerHandler(msgTypeID: number, handler: DataHandler) {
handlers[msgTypeID] = handler;
}
export function unregisterHandler(msgTypeID: number) {
delete handlers[msgTypeID];
}
export function connectWebSocket(path: string, onOpen, onClose, onError) {
let loc = window.location;
let uri = 'ws:';
if (loc.protocol === 'https:') {
uri = 'wss:';
}
uri += '//' + loc.host + path;
let ws = new WebSocket(uri);
ws.onopen = onOpen;
ws.onclose = onClose;
ws.onerror = onError;
ws.onmessage = (e) => {
let msg: WSMessage = JSON.parse(e.data);
let handler = handlers[msg.type];
if (!handler) {
return;
}
handler(msg.data);
};
}
import {RouterStore} from "mobx-react-router";
import {action} from "mobx";
//import * as React from "react";
import {registerHandler, WSMsgType} from "app/misc/WS";
export class AddNodeMessage {
id: string;
}
export class RemoveNodeMessage {
id: string;
}
export class ConnectNodesMessage {
source: string;
target: string
}
export class DisconnectNodesMessage {
source: string;
target: string
}
export class AutopeeringStore {
routerStore: RouterStore;
constructor(routerStore: RouterStore) {
this.routerStore = routerStore;
registerHandler(WSMsgType.AddNode, this.addNode);
registerHandler(WSMsgType.RemoveNode, this.removeNode);
registerHandler(WSMsgType.ConnectNodes, this.connectNodes);
registerHandler(WSMsgType.RemoveNode, this.disconnectNodes);
}
@action
addNode = (msg: AddNodeMessage) => {
console.log(msg)
}
@action
removeNode = (msg: RemoveNodeMessage) => {
console.log(msg)
}
@action
connectNodes = (msg: ConnectNodesMessage) => {
console.log(msg)
}
@action
disconnectNodes = (msg: DisconnectNodesMessage) => {
console.log(msg)
}
}
export default AutopeeringStore;
\ No newline at end of file
import {RouterStore} from "mobx-react-router";
import {action, computed, observable, ObservableMap} from "mobx";
import Col from "react-bootstrap/Col";
import * as React from "react";
import {registerHandler, WSMsgType} from "app/misc/WS";
export class Node {
id: number;
opinion: number = 0;
}
export function LightenDarkenColor(col, amt) {
var num = parseInt(col, 16);
var r = (num >> 16) + amt;
var b = ((num >> 8) & 0x00FF) + amt;
var g = (num & 0x0000FF) + amt;
var newColor = g | (b << 8) | (r << 16);
return newColor.toString(16);
}
function SetColor(opinion) {
if (opinion == 0) {
return "#BD0000"
}
return "#00BD00"
}
class VoteContext {
nodeid: string;
rounds: number;
opinions: number[];
like: number;
}
class Conflict {
nodesview: Map<string, VoteContext>
}
export class FPCMessage {
nodes: number;
conflictset: Map<string, Conflict>
}
export class FPCStore {
routerStore: RouterStore;
@observable msg: FPCMessage = null;
@observable n: number = 0;
@observable nodes = new ObservableMap<number, Node>();
constructor(routerStore: RouterStore) {
this.routerStore = routerStore;
setInterval(this.addNewNode, 100);
setInterval(this.updateNodeValue, 400);
registerHandler(WSMsgType.FPC, this.addLiveFeed);
}
@action
addLiveFeed = (msg: FPCMessage) => {
console.log(msg)
}
@action
addNewNode = () => {
const id = Math.floor(Math.random() * 1000);
let node = new Node();
node.id = id;
node.opinion = Math.floor(Math.random() * 100)%2;
this.nodes.set(id, node);
}
@action
updateNodeValue = () => {
let iter: IterableIterator<number> = this.nodes.keys();
for (const key of iter) {
let node = this.nodes.get(key);
node.opinion = Math.floor(Math.random() * 100)%2;
this.nodes.set(key, node);
}
}
@computed
get nodeGrid(){
let nodeSquares = [];
this.nodes.forEach((node: Node, id: number, obj: Map<number, Node>) => {
nodeSquares.push(
<Col xs={1} key={id.toString()} style={{
height: 50,
width: 50,
border: "1px solid #FFFFFF",
background: SetColor(node.opinion),
}}>
{/* {node.opinion} */}
</Col>
)
})
return nodeSquares;
}
}
export default FPCStore;
\ No newline at end of file
import {action, computed, observable, ObservableMap} from 'mobx';
import * as dateformat from 'dateformat';
import {connectWebSocket, registerHandler, unregisterHandler, WSMsgType} from "app/misc/WS";
class MPSMetric {
mps: number;
ts: string;
}
class Status {
id: string;
version: string;
uptime: number;
mem: MemoryMetrics = new MemoryMetrics();
}
class MemoryMetrics {
sys: number;
heap_sys: number;
heap_inuse: number;
heap_idle: number;
heap_released: number;
heap_objects: number;
m_span_inuse: number;
m_cache_inuse: number;
stack_sys: number;
last_pause_gc: number;
num_gc: number;
ts: string;
}
class TipsMetric {
tips: number;
ts: string;
}
class NetworkIO {
tx: number;
rx: number;
ts: string;
}
class NeighborMetrics {
@observable collected: Array<NeighborMetric> = [];
@observable network_io: Array<NetworkIO> = [];
addMetric(metric: NeighborMetric) {
metric.ts = dateformat(Date.now(), "HH:MM:ss");
this.collected.push(metric);
if (this.collected.length > maxMetricsDataPoints) {
this.collected.shift();
}
let netIO = this.currentNetIO;
if (netIO) {
if (this.network_io.length > maxMetricsDataPoints) {
this.network_io.shift();
}
this.network_io.push(netIO);
}
}
get current() {
return this.collected[this.collected.length - 1];
}
get secondLast() {
let index = this.collected.length - 2;
if (index < 0) {
return
}
return this.collected[index];
}
get currentNetIO(): NetworkIO {
if (this.current && this.secondLast) {
return {
tx: this.current.bytes_written - this.secondLast.bytes_written,
rx: this.current.bytes_read - this.secondLast.bytes_read,
ts: dateformat(new Date(), "HH:MM:ss"),
};
}
return null;
}
@computed
get netIOSeries() {
let tx = Object.assign({}, chartSeriesOpts,
series("Tx", 'rgba(53, 180, 219,1)', 'rgba(53, 180, 219,0.4)')
);
let rx = Object.assign({}, chartSeriesOpts,
series("Rx", 'rgba(235, 134, 52)', 'rgba(235, 134, 52,0.4)')
);
let labels = [];
for (let i = 0; i < this.network_io.length; i++) {
let metric: NetworkIO = this.network_io[i];
labels.push(metric.ts);
tx.data.push(metric.tx);
rx.data.push(-metric.rx);
}
return {
labels: labels,
datasets: [tx, rx],
};
}
}
class NeighborMetric {
id: string;
address: string;
connection_origin: number;
bytes_read: number;
bytes_written: number;
ts: number;
}
const chartSeriesOpts = {
label: "Incoming", data: [],
fill: true,
lineTension: 0,
backgroundColor: 'rgba(58, 60, 171,0.4)',
borderWidth: 1,
borderColor: 'rgba(58, 60, 171,1)',
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: 'rgba(58, 60, 171,1)',
pointBackgroundColor: '#fff',
pointBorderWidth: 1,
pointHoverBackgroundColor: 'rgba(58, 60, 171,1)',
pointHoverBorderColor: 'rgba(220,220,220,1)',
pointHoverBorderWidth: 2,
pointRadius: 0,
pointHitRadius: 20,
pointHoverRadius: 5,
};
function series(name: string, color: string, bgColor: string) {
return {
label: name, data: [],
backgroundColor: bgColor,
borderColor: color,
pointBorderColor: color,
pointHoverBackgroundColor: color,
pointHoverBorderColor: 'rgba(220,220,220,1)',
}
}
const statusWebSocketPath = "/ws";
const maxMetricsDataPoints = 900;
export class NodeStore {
@observable status: Status = new Status();
@observable websocketConnected: boolean = false;
@observable last_mps_metric: MPSMetric = new MPSMetric();
@observable collected_mps_metrics: Array<MPSMetric> = [];
@observable collected_mem_metrics: Array<MemoryMetrics> = [];
@observable neighbor_metrics = new ObservableMap<string, NeighborMetrics>();
@observable last_tips_metric: TipsMetric = new TipsMetric();
@observable collected_tips_metrics: Array<TipsMetric> = [];
@observable collecting: boolean = true;
constructor() {
this.registerHandlers();
}
registerHandlers = () => {
registerHandler(WSMsgType.Status, this.updateStatus);
registerHandler(WSMsgType.MPSMetrics, (mps: number) => {
this.addMPSMetric(this.updateLastMPSMetric(mps));
});
registerHandler(WSMsgType.NeighborStats, this.updateNeighborMetrics);
registerHandler(WSMsgType.TipsMetrics, this.updateLastTipsMetric);
this.updateCollecting(true);
}
unregisterHandlers = () => {
unregisterHandler(WSMsgType.Status);
registerHandler(WSMsgType.MPSMetrics, this.updateLastMPSMetric);
unregisterHandler(WSMsgType.NeighborStats);
unregisterHandler(WSMsgType.TipsMetrics);
this.updateCollecting(false);
}
@action
updateCollecting = (collecting: boolean) => {
this.collecting = collecting;
}
@action
reset() {
this.collected_mps_metrics = [];
this.collected_mem_metrics = [];
this.neighbor_metrics = new ObservableMap<string, NeighborMetrics>();
this.collected_tips_metrics = [];
}
connect() {
connectWebSocket(statusWebSocketPath,
() => this.updateWebSocketConnected(true),
() => this.updateWebSocketConnected(false),
() => this.updateWebSocketConnected(false))
}
@action
updateWebSocketConnected = (connected: boolean) => this.websocketConnected = connected;
@action
updateStatus = (status: Status) => {
status.mem.ts = dateformat(Date.now(), "HH:MM:ss");
if (this.collected_mem_metrics.length > maxMetricsDataPoints) {
this.collected_mem_metrics.shift();
}
this.collected_mem_metrics.push(status.mem);
this.status = status;
};
@action
updateNeighborMetrics = (neighborMetrics: Array<NeighborMetric>) => {
let updated = [];
for (let i = 0; i < neighborMetrics.length; i++) {
let metric = neighborMetrics[i];
let neighbMetrics: NeighborMetrics = this.neighbor_metrics.get(metric.id);
if (!neighbMetrics) {
neighbMetrics = new NeighborMetrics();
}
neighbMetrics.addMetric(metric);
this.neighbor_metrics.set(metric.id, neighbMetrics);
updated.push(metric.id);
}
// remove duplicates
for (const k of this.neighbor_metrics.keys()) {
if (!updated.includes(k)) {
this.neighbor_metrics.delete(k);
}
}
};
@action
updateLastMPSMetric = (mps: number) => {
let mpsMetric = new MPSMetric();
mpsMetric.mps = mps;
mpsMetric.ts = dateformat(Date.now(), "HH:MM:ss");
this.last_mps_metric = mpsMetric;
return mpsMetric;
};
@action
addMPSMetric = (metric: MPSMetric) => {
if (this.collected_mps_metrics.length > maxMetricsDataPoints) {
this.collected_mps_metrics.shift();
}
this.collected_mps_metrics.push(metric);
}
@action
updateLastTipsMetric = (tips: number) => {
let tipsMetric = new TipsMetric();
tipsMetric.tips = tips;
tipsMetric.ts = dateformat(Date.now(), "HH:MM:ss");
this.last_tips_metric = tipsMetric;
if (this.collected_tips_metrics.length > maxMetricsDataPoints) {
this.collected_tips_metrics.shift();
}
this.collected_tips_metrics.push(tipsMetric);
};
@computed
get mpsSeries() {
let mps = Object.assign({}, chartSeriesOpts,
series("MPS", 'rgba(67, 196, 99,1)', 'rgba(67, 196, 99,0.4)')
);
let labels = [];
for (let i = 0; i < this.collected_mps_metrics.length; i++) {
let metric: MPSMetric = this.collected_mps_metrics[i];
labels.push(metric.ts);
mps.data.push(metric.mps);
}
return {
labels: labels,
datasets: [mps],
};
}
@computed
get tipsSeries() {
let tips = Object.assign({}, chartSeriesOpts,
series("Tips", 'rgba(250, 140, 30,1)', 'rgba(250, 140, 30,0.4)')
);
let labels = [];
for (let i = 0; i < this.collected_tips_metrics.length; i++) {
let metric: TipsMetric = this.collected_tips_metrics[i];
labels.push(metric.ts);
tips.data.push(metric.tips);
}
return {
labels: labels,
datasets: [tips],
};
}
@computed
get neighborsSeries() {
return {};
}
@computed
get uptime() {
let day, hour, minute, seconds;
seconds = Math.floor(this.status.uptime / 1000);
minute = Math.floor(seconds / 60);
seconds = seconds % 60;
hour = Math.floor(minute / 60);
minute = minute % 60;
day = Math.floor(hour / 24);
hour = hour % 24;
let str = "";
if (day == 1) {
str += day + " Day, ";
}
if (day > 1) {
str += day + " Days, ";
}
if (hour >= 0) {
if (hour < 10) {
str += "0" + hour + ":";
} else {
str += hour + ":";
}
}
if (minute >= 0) {
if (minute < 10) {
str += "0" + minute + ":";
} else {
str += minute + ":";
}
}
if (seconds >= 0) {
if (seconds < 10) {
str += "0" + seconds;
} else {
str += seconds;
}
}
return str;
}
@computed
get memSeries() {
let heapAlloc = Object.assign({}, chartSeriesOpts,
series("Heap Alloc", 'rgba(168, 50, 76,1)', 'rgba(168, 50, 76,0.4)')
);
let heapInuse = Object.assign({}, chartSeriesOpts,
series("Heap In-Use", 'rgba(222, 49, 87,1)', 'rgba(222, 49, 87,0.4)')
);
let heapIdle = Object.assign({}, chartSeriesOpts,
series("Heap Idle", 'rgba(222, 49, 182,1)', 'rgba(222, 49, 182,0.4)')
);
let heapReleased = Object.assign({}, chartSeriesOpts,
series("Heap Released", 'rgba(250, 76, 252,1)', 'rgba(250, 76, 252,0.4)')
);
let stackAlloc = Object.assign({}, chartSeriesOpts,
series("Stack Alloc", 'rgba(54, 191, 173,1)', 'rgba(54, 191, 173,0.4)')
);
let sys = Object.assign({}, chartSeriesOpts,
series("Total Alloc", 'rgba(160, 50, 168,1)', 'rgba(160, 50, 168,0.4)')
);
let labels = [];
for (let i = 0; i < this.collected_mem_metrics.length; i++) {
let metric = this.collected_mem_metrics[i];
labels.push(metric.ts);
heapAlloc.data.push(metric.heap_sys);
heapInuse.data.push(metric.heap_inuse);
heapIdle.data.push(metric.heap_idle);
heapReleased.data.push(metric.heap_released);
stackAlloc.data.push(metric.stack_sys);
sys.data.push(metric.sys);
}
return {
labels: labels,
datasets: [sys, heapAlloc, heapInuse, heapIdle, heapReleased, stackAlloc],
};
}
}
export default NodeStore;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment