Turbo Render (#298)

* More responsive React updates on scroll

* `Icon`s now use shadow dom

* Faster Sparkline

* Recycle table rows

* Separate Header from Chain to avoid vdom diffing

* Separate THead from Row.HEADER to avoid vdom diffing

* Throttle rendering updates on chain tabs, also styles

* Minor tweaks and fixes

* Created components for all columns

* Wrapping up Column refactor

* Rename Row--td to Column

* Lazy `Ago`

* Update styles for faster layouting

* Minor cleanup

* Fix Connection

* Use shadow DOM in `PolkadotIcon`

* Comments and tweaks for the List component

* Faster Tooltip and Truncate

* Minor tweaks

* Tooltiped columns can now be copied

* Future-proof Connection

* Remove the <div> wrapper from Icon

* Fix dash on missing graph data

* Clean up some SVGs

* Cleanup and comments

* Localize the use of `previousKeys` to `recalculateKeys`

* Custom appState disjoint from React component state

* Make appState and appUpdate refs readonly

* Cleanup
This commit is contained in:
Maciej Hirsz
2020-11-11 13:41:01 +01:00
committed by GitHub
parent 675776c3e1
commit ebb01c1a7d
72 changed files with 1863 additions and 1118 deletions
+78 -77
View File
@@ -1,18 +1,21 @@
import { VERSION, timestamp, FeedMessage, Types, Maybe, sleep } from './common';
import {
State,
Update,
Node,
UpdateBound,
ChainData,
PINNED_CHAINS,
} from './state';
import { State, Update, Node, ChainData, PINNED_CHAINS } from './state';
import { PersistentSet } from './persist';
import { getHashData, setHashData } from './utils';
import { AfgHandling } from './AfgHandling';
import { VIS_AUTHORITIES_LIMIT } from './components/Consensus';
import { Column } from './components/List';
import { ACTIONS } from './common/feed';
import {
Column,
LocationColumn,
PeersColumn,
TxsColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
} from './components/List';
const TIMEOUT_BASE = (1000 * 5) as Types.Milliseconds; // 5 seconds
const TIMEOUT_MAX = (1000 * 60 * 5) as Types.Milliseconds; // 5 minutes
@@ -26,9 +29,10 @@ declare global {
export class Connection {
public static async create(
pins: PersistentSet<Types.NodeName>,
update: Update
appState: Readonly<State>,
appUpdate: Update
): Promise<Connection> {
return new Connection(await Connection.socket(), update, pins);
return new Connection(await Connection.socket(), appState, appUpdate, pins);
}
private static readonly utf8decoder = new TextDecoder('utf-8');
@@ -102,27 +106,22 @@ export class Connection {
private resubscribeTo: Maybe<Types.ChainLabel> = getHashData().chain;
// flag whether or not FE should subscribe to consensus updates on reconnect
private resubscribeSendFinality: boolean = getHashData().tab === 'consensus';
// flag used to throttle DOM updates to window frame rate
private isUpdating = false;
private socket: WebSocket;
private state: Readonly<State>;
private readonly update: Update;
private readonly pins: PersistentSet<Types.NodeName>;
constructor(
socket: WebSocket,
update: Update,
pins: PersistentSet<Types.NodeName>
private socket: WebSocket,
private readonly appState: Readonly<State>,
private readonly appUpdate: Update,
private readonly pins: PersistentSet<Types.NodeName>
) {
this.socket = socket;
this.update = update;
this.pins = pins;
this.bindSocket();
}
public subscribe(chain: Types.ChainLabel) {
if (this.state.subscribed != null && this.state.subscribed !== chain) {
this.state = this.update({
if (
this.appState.subscribed != null &&
this.appState.subscribed !== chain
) {
this.appUpdate({
tab: 'list',
});
setHashData({ chain, tab: 'list' });
@@ -134,7 +133,7 @@ export class Connection {
}
public subscribeConsensus(chain: Types.ChainLabel) {
if (this.state.authorities.length <= VIS_AUTHORITIES_LIMIT) {
if (this.appState.authorities.length <= VIS_AUTHORITIES_LIMIT) {
setHashData({ chain });
this.resubscribeSendFinality = true;
this.socket.send(`send-finality:${chain}`);
@@ -142,7 +141,7 @@ export class Connection {
}
public resetConsensus() {
this.state = this.update({
this.appUpdate({
consensusInfo: new Array() as Types.ConsensusInfo,
displayConsensusLoadingScreen: true,
authorities: [] as Types.Address[],
@@ -156,14 +155,10 @@ export class Connection {
}
public handleMessages = (messages: FeedMessage.Message[]) => {
const { nodes, chains, sortBy, selectedColumns } = this.state;
const { nodes, chains, sortBy, selectedColumns } = this.appState;
const nodesStateRef = nodes.ref;
const updateState: UpdateBound = (state) => {
this.state = this.update(state);
};
const getState = () => this.state;
const afg = new AfgHandling(updateState, getState);
const afg = new AfgHandling(this.appUpdate, this.appState);
let sortByColumn: Maybe<Column> = null;
@@ -176,13 +171,7 @@ export class Connection {
switch (message.action) {
case ACTIONS.FeedVersion: {
if (message.payload !== VERSION) {
this.state = this.update({ status: 'upgrade-requested' });
this.clean();
// Force reload from the server
setTimeout(() => window.location.reload(true), 3000);
return;
return this.newVersion();
}
break;
@@ -193,7 +182,7 @@ export class Connection {
nodes.mutEach((node) => node.newBestBlock());
this.state = this.update({ best, blockTimestamp, blockAverage });
this.appUpdate({ best, blockTimestamp, blockAverage });
break;
}
@@ -201,7 +190,7 @@ export class Connection {
case ACTIONS.BestFinalized: {
const [finalized /*, hash */] = message.payload;
this.state = this.update({ finalized });
this.appUpdate({ finalized });
break;
}
@@ -257,7 +246,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateLocation([lat, lon, city]),
sortByColumn === Column.LOCATION
sortByColumn === LocationColumn
);
break;
@@ -277,8 +266,8 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateFinalized(height, hash),
sortByColumn === Column.FINALIZED ||
sortByColumn === Column.FINALIZED_HASH
sortByColumn === FinalizedBlockColumn ||
sortByColumn === FinalizedHashColumn
);
break;
@@ -290,7 +279,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateStats(nodeStats),
sortByColumn === Column.PEERS || sortByColumn === Column.TXS
sortByColumn === PeersColumn || sortByColumn === TxsColumn
);
break;
@@ -302,7 +291,7 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateHardware(nodeHardware),
sortByColumn === Column.UPLOAD || sortByColumn === Column.DOWNLOAD
sortByColumn === UploadColumn || sortByColumn === DownloadColumn
);
break;
@@ -314,14 +303,14 @@ export class Connection {
nodes.mutAndMaybeSort(
id,
(node) => node.updateIO(nodeIO),
sortByColumn === Column.STATE_CACHE
sortByColumn === StateCacheColumn
);
break;
}
case ACTIONS.TimeSync: {
this.state = this.update({
this.appUpdate({
timeDiff: (timestamp() - message.payload) as Types.Milliseconds,
});
@@ -338,7 +327,7 @@ export class Connection {
chains.set(label, { label, nodeCount });
}
this.state = this.update({ chains });
this.appUpdate({ chains });
break;
}
@@ -346,9 +335,9 @@ export class Connection {
case ACTIONS.RemovedChain: {
chains.delete(message.payload);
if (this.state.subscribed === message.payload) {
if (this.appState.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes, chains });
this.appUpdate({ subscribed: null, nodes, chains });
this.resetConsensus();
}
@@ -358,16 +347,16 @@ export class Connection {
case ACTIONS.SubscribedTo: {
nodes.clear();
this.state = this.update({ subscribed: message.payload, nodes });
this.appUpdate({ subscribed: message.payload, nodes });
break;
}
case ACTIONS.UnsubscribedFrom: {
if (this.state.subscribed === message.payload) {
if (this.appState.subscribed === message.payload) {
nodes.clear();
this.state = this.update({ subscribed: null, nodes });
this.appUpdate({ subscribed: null, nodes });
}
break;
@@ -416,12 +405,8 @@ export class Connection {
}
}
if (nodes.hasChangedSince(nodesStateRef) && !this.isUpdating) {
this.isUpdating = true;
window.requestAnimationFrame(() => {
this.update({ nodes });
this.isUpdating = false;
});
if (nodes.hasChangedSince(nodesStateRef)) {
this.appUpdate({ nodes });
}
this.autoSubscribe();
@@ -430,19 +415,19 @@ export class Connection {
private bindSocket() {
this.ping();
if (this.state) {
const { nodes } = this.state;
if (this.appState) {
const { nodes } = this.appState;
nodes.clear();
}
this.state = this.update({
this.appUpdate({
status: 'online',
});
if (this.state.subscribed) {
this.resubscribeTo = this.state.subscribed;
this.resubscribeSendFinality = this.state.sendFinality;
this.state = this.update({ subscribed: null, sendFinality: false });
if (this.appState.subscribed) {
this.resubscribeTo = this.appState.subscribed;
this.resubscribeSendFinality = this.appState.sendFinality;
this.appUpdate({ subscribed: null, sendFinality: false });
}
this.socket.addEventListener('message', this.handleFeedData);
@@ -479,8 +464,14 @@ export class Connection {
const latency = timestamp() - this.pingSent;
this.pingSent = null;
}
console.log('latency', latency);
private newVersion() {
this.appUpdate({ status: 'upgrade-requested' });
this.clean();
// Force reload from the server
setTimeout(() => window.location.reload(true), 3000);
}
private clean() {
@@ -493,18 +484,28 @@ export class Connection {
}
private handleFeedData = (event: MessageEvent) => {
const data =
typeof event.data === 'string'
? ((event.data as any) as FeedMessage.Data)
: ((Connection.utf8decoder.decode(
event.data
) as any) as FeedMessage.Data);
let data: FeedMessage.Data;
if (typeof event.data === 'string') {
data = (event.data as any) as FeedMessage.Data;
} else {
const u8aData = new Uint8Array(event.data);
// Future-proofing for when we switch to binary feed
if (u8aData[0] === 0x00) {
return this.newVersion();
}
const str = Connection.utf8decoder.decode(event.data);
data = (str as any) as FeedMessage.Data;
}
this.handleMessages(FeedMessage.deserialize(data));
};
private autoSubscribe() {
const { subscribed, chains } = this.state;
const { subscribed, chains } = this.appState;
const { resubscribeTo, resubscribeSendFinality } = this;
if (subscribed) {
@@ -540,7 +541,7 @@ export class Connection {
}
private handleDisconnect = async () => {
this.state = this.update({ status: 'offline' });
this.appUpdate({ status: 'offline' });
this.resetConsensus();
this.clean();
this.socket.close();