dashboard: send current block to the dashboard client

This commit is contained in:
Kurkó Mihály 2019-06-20 13:11:33 +03:00
parent abc3abb4ab
commit 9bc4793401
12 changed files with 294 additions and 38 deletions

View file

@ -0,0 +1,53 @@
// @flow
// Copyright 2019 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
import React, {Component} from 'react';
import type {Chain as ChainType} from '../types/content';
export const inserter = () => (update: ChainType, prev: ChainType) => {
if (!update.currentBlock) {
return;
}
if (!prev.currentBlock) {
prev.currentBlock = {};
}
prev.currentBlock.number = update.currentBlock.number;
prev.currentBlock.timestamp = update.currentBlock.timestamp;
return prev;
};
// styles contains the constant styles of the component.
const styles = {};
// themeStyles returns the styles generated from the theme for the component.
const themeStyles = theme => ({});
export type Props = {
content: Content,
};
type State = {};
// Logs renders the log page.
class Chain extends Component<Props, State> {
render() {
return <></>;
}
}
export default Chain;

View file

@ -25,6 +25,7 @@ import Header from 'Header';
import Body from 'Body';
import {inserter as logInserter, SAME} from 'Logs';
import {inserter as peerInserter} from 'Network';
import {inserter as chainInserter} from 'Chain';
import {MENU} from '../common';
import type {Content} from '../types/content';
@ -83,17 +84,24 @@ const appender = <T>(limit: number, mapper = replacer) => (update: Array<T>, pre
// the execution of unnecessary operations (e.g. copy of the log array).
const defaultContent: () => Content = () => ({
general: {
version: null,
commit: null,
version: null,
genesis: '',
},
home: {},
chain: {},
chain: {
currentBlock: {
number: 0,
timestamp: 0,
},
},
txpool: {},
network: {
peers: {
bundles: {},
},
diff: [],
activePeerCount: 0,
},
system: {
activeMemory: [],
@ -121,9 +129,10 @@ const updaters = {
general: {
version: replacer,
commit: replacer,
genesis: replacer,
},
home: null,
chain: null,
chain: chainInserter(),
txpool: null,
network: peerInserter(200),
system: {
@ -241,6 +250,7 @@ class Dashboard extends Component<Props, State> {
<div className={this.props.classes.dashboard} style={styles.dashboard}>
<Header
switchSideBar={this.switchSideBar}
content={this.state.content}
/>
<Body
opened={this.state.sideBar}

View file

@ -32,6 +32,9 @@ import ChartRow from 'ChartRow';
import CustomTooltip, {bytePlotter, bytePerSecPlotter, percentPlotter, multiplier} from 'CustomTooltip';
import {chartStrokeWidth, styles as commonStyles} from '../common';
import type {General, System} from '../types/content';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faNetworkWired} from "@fortawesome/free-solid-svg-icons";
import Toolbar from "@material-ui/core/Toolbar";
const FOOTER_SYNC_ID = 'footerSyncId';
@ -154,6 +157,23 @@ class Footer extends Component<Props, State> {
render() {
const {general, system} = this.props;
let network = '';
switch (general.genesis) {
case '0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3':
network = 'main';
break;
case '0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d':
network = 'ropsten';
break;
case '0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177':
network = 'rinkeby';
break;
case '0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a':
network = 'görli';
break;
default:
network = `unknown (${general.genesis.substring(0, 8)})`;
}
return (
<Grid container className={this.props.classes.footer} direction='row' alignItems='center' style={styles.footer}>
@ -202,6 +222,9 @@ class Footer extends Component<Props, State> {
</a>
</Typography>
)}
<Typography style={styles.headerText}>
<span style={commonStyles.light}>Network</span> {network}
</Typography>
</Grid>
</Grid>
);

View file

@ -23,16 +23,25 @@ import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import IconButton from '@material-ui/core/IconButton';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBars} from '@fortawesome/free-solid-svg-icons';
import {faBars, faSortAmountUp, faClock, faUsers, faSync} from '@fortawesome/free-solid-svg-icons';
import Typography from '@material-ui/core/Typography';
import type {Content} from '../types/content';
const magnitude = [31536000, 604800, 86400, 3600, 60, 1];
const label = ['y', 'w', 'd', 'h', 'm', 's'];
// styles contains the constant styles of the component.
const styles = {
header: {
height: '8%',
},
headerText: {
marginRight: 15,
},
toolbar: {
height: '100%',
minHeight: 'unset',
},
};
@ -50,16 +59,52 @@ const themeStyles = (theme: Object) => ({
title: {
paddingLeft: theme.spacing.unit,
fontSize: 3 * theme.spacing.unit,
flex: 1,
},
});
export type Props = {
classes: Object, // injected by withStyles()
switchSideBar: () => void,
content: Content,
networkID: number,
};
type State = {
since: string,
}
// Header renders the header of the dashboard.
class Header extends Component<Props> {
class Header extends Component<Props, State> {
constructor(props) {
super(props);
this.state = {since: ''};
}
componentDidMount() {
this.interval = setInterval(() => this.setState(() => {
// time (seconds) since last block.
let timeDiff = Math.floor((Date.now() - this.props.content.chain.currentBlock.timestamp * 1000) / 1000);
let since = '';
let i = 0;
for (; i < magnitude.length && timeDiff < magnitude[i]; i++);
for (let j = 2; i < magnitude.length && j > 0; j--, i++) {
const t = Math.floor(timeDiff / magnitude[i]);
if (t > 0) {
since += `${t}${label[i]} `;
timeDiff %= magnitude[i];
}
}
if (since === '') {
since = 'now';
}
this.setState({since: since});
}), 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
const {classes} = this.props;
@ -72,6 +117,15 @@ class Header extends Component<Props> {
<Typography type='title' color='inherit' noWrap className={classes.title}>
Go Ethereum Dashboard
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faSortAmountUp} /> {this.props.content.chain.currentBlock.number}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faClock} /> {this.state.since}
</Typography>
<Typography style={styles.headerText}>
<FontAwesomeIcon icon={faUsers} /> {this.props.content.network.activePeerCount}
</Typography>
</Toolbar>
</AppBar>
);

View file

@ -20,6 +20,7 @@ import React, {Component} from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import Chain from 'Chain';
import Network from 'Network';
import Logs from 'Logs';
import Footer from 'Footer';
@ -95,7 +96,9 @@ class Main extends Component<Props, State> {
children = <div>Work in progress.</div>;
break;
case MENU.get('chain').id:
children = <div>Work in progress.</div>;
children = <Chain
content={this.props.content.chain}
/>;
break;
case MENU.get('txpool').id:
children = <div>Work in progress.</div>;

View file

@ -379,6 +379,17 @@ export const inserter = (sampleLimit: number) => (update: NetworkType, prev: Net
}
});
}
prev.activePeerCount = 0;
Object.entries(prev.peers.bundles).forEach(([addr, bundle]) => {
if (!bundle.knownPeers || Object.keys(bundle.knownPeers).length < 1) {
return;
}
Object.entries(bundle.knownPeers).forEach(([enode, peer]) => {
if (peer.active === true) {
prev.activePeerCount++;
}
});
});
return prev;
};

View file

@ -35,6 +35,7 @@ export type ChartEntry = {
export type General = {
version: ?string,
commit: ?string,
genesis: ?string,
};
export type Home = {
@ -42,16 +43,22 @@ export type Home = {
};
export type Chain = {
/* TODO (kurkomisi) */
currentBlock: Block,
};
export type Block = {
number: number,
timestamp: number,
}
export type TxPool = {
/* TODO (kurkomisi) */
};
export type Network = {
peers: Peers,
diff: Array<PeerEvent>
diff: Array<PeerEvent>,
activePeerCount: number,
};
export type PeerEvent = {

View file

@ -2219,9 +2219,9 @@ d3-collection@1:
integrity sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==
d3-color@1:
version "1.2.3"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.3.tgz#6c67bb2af6df3cc8d79efcc4d3a3e83e28c8048f"
integrity sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==
version "1.2.5"
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.2.5.tgz#5810ea1808f2f993d04508cb2fad764f48134788"
integrity sha512-u4CaFaqQKRofuhr9uo/xLdaGvvzdsMX7MgP42XgQJHLBRWnn0C0T+48rvj80cN9KXAauHEMEfe7ehacIoxmP/g==
d3-format@1:
version "1.3.2"
@ -2559,9 +2559,9 @@ ee-first@1.1.1:
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
electron-to-chromium@^1.3.164:
version "1.3.174"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.174.tgz#011c68d4130e995cbb69c533e147d7f4856e9f39"
integrity sha512-OEh3EARo2B07ZRtxB0u9GqWyWmTeNS+diMp5bjw4kqMjgpzqM0w1zUOyErDsyWxTdArbvZ79T/w5n3WsBVHLfA==
version "1.3.175"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.175.tgz#a269716af5e549f9f3989ae38ba484881dcb3702"
integrity sha512-cQ0o7phcPA0EYkN4juZy/rq4XVxl/1ywQnytDT9hZTC0cHfaOBqWK2XlWp9Mk8xRGntgnmxlHTOux4HLb2ZNnA==
elliptic@^6.0.0:
version "6.5.0"
@ -3498,9 +3498,9 @@ got@^8.3.2:
url-to-options "^1.0.1"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
version "4.2.0"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.0.tgz#8d8fdc73977cb04104721cb53666c1ca64cd328b"
integrity sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==
handle-thing@^2.0.0:
version "2.0.0"

77
dashboard/chain.go Normal file
View file

@ -0,0 +1,77 @@
package dashboard
import (
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/log"
)
type block struct {
Number int64 `json:"number,omitempty"`
Time uint64 `json:"timestamp,omitempty"`
}
func (db *Dashboard) collectChainData() {
defer db.wg.Done()
var (
currentBlock *block
chainCh chan core.ChainHeadEvent
chainSub event.Subscription
)
switch {
case db.ethServ != nil:
chain := db.ethServ.BlockChain()
currentBlock = &block{
Number: chain.CurrentHeader().Number.Int64(),
Time: chain.CurrentHeader().Time,
}
chainCh = make(chan core.ChainHeadEvent)
chainSub = chain.SubscribeChainHeadEvent(chainCh)
case db.lesServ != nil:
chain := db.lesServ.BlockChain()
currentBlock = &block{
Number: chain.CurrentHeader().Number.Int64(),
Time: chain.CurrentHeader().Time,
}
chainCh = make(chan core.ChainHeadEvent)
chainSub = chain.SubscribeChainHeadEvent(chainCh)
default:
errc := <-db.quit
errc <- nil
return
}
defer chainSub.Unsubscribe()
db.chainLock.Lock()
db.history.Chain = &ChainMessage{
CurrentBlock: currentBlock,
}
db.chainLock.Unlock()
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
for {
select {
case e := <-chainCh:
currentBlock := &block{
Number: e.Block.Number().Int64(),
Time: e.Block.Time(),
}
db.chainLock.Lock()
db.history.Chain = &ChainMessage{
CurrentBlock: currentBlock,
}
db.chainLock.Unlock()
db.sendToAll(&Message{Chain: &ChainMessage{CurrentBlock: currentBlock}})
case err := <-chainSub.Err():
log.Warn("Chain subscription error", "err", err)
errc := <-db.quit
errc <- nil
return
case errc := <-db.quit:
errc <- nil
return
}
}
}

View file

@ -27,6 +27,7 @@ package dashboard
import (
"fmt"
"github.com/ethereum/go-ethereum/common"
"io"
"net"
"net/http"
@ -46,6 +47,7 @@ import (
const (
sampleLimit = 200 // Maximum number of data samples
dataCollectorCount = 4
)
// Dashboard contains the dashboard internals.
@ -59,6 +61,7 @@ type Dashboard struct {
history *Message // Stored historical data
lock sync.Mutex // Lock protecting the dashboard's internals
chainLock sync.RWMutex // Lock protecting the stored blockchain data
sysLock sync.RWMutex // Lock protecting the stored system data
peerLock sync.RWMutex // Lock protecting the stored peer data
logLock sync.RWMutex // Lock protecting the stored log data
@ -95,6 +98,12 @@ func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, comm
if len(params.VersionMeta) > 0 {
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
}
var genesis common.Hash
if ethServ != nil {
genesis = ethServ.BlockChain().Genesis().Hash()
} else if lesServ != nil {
genesis = lesServ.BlockChain().Genesis().Hash()
}
return &Dashboard{
conns: make(map[uint32]*client),
config: config,
@ -103,6 +112,7 @@ func New(config *Config, ethServ *eth.Ethereum, lesServ *les.LightEthereum, comm
General: &GeneralMessage{
Commit: commit,
Version: fmt.Sprintf("v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
Genesis: genesis,
},
System: &SystemMessage{
ActiveMemory: emptyChartEntries(sampleLimit),
@ -143,7 +153,8 @@ func (db *Dashboard) APIs() []rpc.API { return nil }
func (db *Dashboard) Start(server *p2p.Server) error {
log.Info("Starting dashboard")
db.wg.Add(3)
db.wg.Add(dataCollectorCount)
go db.collectChainData()
go db.collectSystemData()
go db.streamLogs()
go db.collectPeerData()
@ -175,8 +186,8 @@ func (db *Dashboard) Stop() error {
errs = append(errs, err)
}
// Close the collectors.
errc := make(chan error, 1)
for i := 0; i < 3; i++ {
errc := make(chan error, dataCollectorCount)
for i := 0; i < dataCollectorCount; i++ {
db.quit <- errc
if err := <-errc; err != nil {
errs = append(errs, err)
@ -250,20 +261,21 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
}()
// Send the past data.
db.chainLock.RLock()
db.sysLock.RLock()
db.peerLock.RLock()
db.logLock.RLock()
h := deepcopy.Copy(db.history).(*Message)
db.chainLock.RUnlock()
db.sysLock.RUnlock()
db.peerLock.RUnlock()
db.logLock.RUnlock()
client.msg <- h
// Start tracking the connection and drop at connection loss.
db.lock.Lock()
client.msg <- h
db.conns[id] = client
db.lock.Unlock()
defer func() {

View file

@ -18,6 +18,7 @@ package dashboard
import (
"encoding/json"
"github.com/ethereum/go-ethereum/common"
)
type Message struct {
@ -39,6 +40,7 @@ type ChartEntry struct {
type GeneralMessage struct {
Version string `json:"version,omitempty"`
Commit string `json:"commit,omitempty"`
Genesis common.Hash `json:"genesis,omitempty"`
}
type HomeMessage struct {
@ -46,7 +48,7 @@ type HomeMessage struct {
}
type ChainMessage struct {
/* TODO (kurkomisi) */
CurrentBlock *block `json:"currentBlock,omitempty"`
}
type TxPoolMessage struct {

View file

@ -339,6 +339,8 @@ func (db *Dashboard) collectPeerData() {
db.geodb, err = openGeoDB()
if err != nil {
log.Warn("Failed to open geodb", "err", err)
errc := <-db.quit
errc <- nil
return
}
defer db.geodb.close()
@ -517,6 +519,8 @@ func (db *Dashboard) collectPeerData() {
newPeerEvents = newPeerEvents[:0]
case err := <-db.subPeer.Err():
log.Warn("Peer subscription error", "err", err)
errc := <-db.quit
errc <- nil
return
case errc := <-db.quit:
errc <- nil