Calculate block latency details (#7030)

* Calculate block latency details

* Single useEffect

* Cleanups

* Latency tab

* Adjust chart size

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
Jaco 2022-02-24 11:14:44 +01:00 committed by GitHub
parent 327f281855
commit 0444b6da11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 273 additions and 6 deletions

View File

@ -0,0 +1,89 @@
// Copyright 2017-2022 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Detail } from './types';
import React, { useMemo } from 'react';
import styled from 'styled-components';
import { CardSummary, Chart, Spinner, SummaryBox } from '@polkadot/react-components';
import { formatNumber } from '@polkadot/util';
import { useTranslation } from '../translate';
import useLatency from './useLatency';
interface Props {
className?: string;
}
interface ChartInfo {
labels: string[];
values: number[][];
}
const COLORS = ['#8c2200'];
const LEGENDS: string[] = [];
const OPTIONS = {
animation: {
duration: 0
},
aspectRatio: 3,
maintainAspectRatio: false
};
function getPoints (details: Detail[]): ChartInfo {
const labels = new Array<string>(details.length);
const times = new Array<number>(details.length);
for (let i = 0; i < details.length; i++) {
labels[i] = formatNumber(details[i].blockNumber);
times[i] = details[i].delay / 1000;
}
return { labels, values: [times] };
}
function Latency ({ className }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const { details, timeAvg, timeMax, timeMin } = useLatency();
const { labels, values } = useMemo(
() => getPoints(details),
[details]
);
if (details.length <= 2) {
return (
<Spinner />
);
}
return (
<div className={className}>
<SummaryBox>
<CardSummary label={t<string>('min')}>{(timeMin / 1000).toFixed(3)}s</CardSummary>
<CardSummary label={t<string>('avg')}>{(timeAvg / 1000).toFixed(3)}s</CardSummary>
<CardSummary label={t<string>('max')}>{(timeMax / 1000).toFixed(3)}s</CardSummary>
</SummaryBox>
<div className='container'>
<h1>{t<string>('blocktimes (last {{num}} blocks)', { replace: { num: labels.length } })}</h1>
<Chart.Line
colors={COLORS}
labels={labels}
legends={LEGENDS}
options={OPTIONS}
values={values}
/>
</div>
</div>
);
}
export default styled(Latency)`
.container {
background: var(--bg-table);
border: 1px solid var(--border-table);
border-radius: 0.25rem;
padding: 1rem 1.5rem;
}
`;

View File

@ -0,0 +1,20 @@
// Copyright 2017-2022 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Hash } from '@polkadot/types/interfaces';
export interface Detail {
blockNumber: number;
countEvents: number;
countExtrinsics: number;
delay: number;
now: number;
parentHash: Hash;
}
export interface Result {
details: Detail[];
timeAvg: number;
timeMax: number;
timeMin: number;
}

View File

@ -0,0 +1,149 @@
// Copyright 2017-2022 @polkadot/app-explorer authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { ApiPromise } from '@polkadot/api';
import type { SignedBlockExtended } from '@polkadot/api-derive/types';
import type { GenericExtrinsic, u32 } from '@polkadot/types';
import type { Block } from '@polkadot/types/interfaces';
import type { Detail, Result } from './types';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useApi, useCall } from '@polkadot/react-hooks';
const INITIAL_ITEMS = 50;
const MAX_ITEMS = INITIAL_ITEMS;
function getSetter ({ extrinsics }: Block): GenericExtrinsic | undefined {
return extrinsics.find(({ method: { method, section } }) =>
method === 'set' &&
section === 'timestamp'
);
}
function extractNext (prev: Detail[], { block, events }: SignedBlockExtended): Detail[] {
const setter = getSetter(block);
if (!setter) {
return prev;
}
const now = (setter.args[0] as u32).toNumber();
const blockNumber = block.header.number.toNumber();
let delay = 0;
if (prev.length) {
if (blockNumber <= prev[prev.length - 1].blockNumber) {
return prev;
}
delay = now - prev[prev.length - 1].now;
}
return [
...prev,
{
blockNumber,
countEvents: events.length,
countExtrinsics: block.extrinsics.length,
delay,
now,
parentHash: block.header.parentHash
}
].slice(-MAX_ITEMS);
}
function extractPrev (prev: Detail[], { block, events }: SignedBlockExtended): Detail[] {
const setter = getSetter(block);
if (!setter) {
return prev;
}
const now = (setter.args[0] as u32).toNumber();
prev[0].delay = prev[0].now - now;
return [
{
blockNumber: block.header.number.toNumber(),
countEvents: events.length,
countExtrinsics: block.extrinsics.length,
delay: 0,
now,
parentHash: block.header.parentHash
},
...prev
];
}
async function getPrev (api: ApiPromise, { block: { header } }: SignedBlockExtended): Promise<SignedBlockExtended[]> {
const blockNumbers: number[] = [];
let blockNumber = header.number.toNumber() - 1;
for (let i = 1; blockNumber > 0 && i <= INITIAL_ITEMS; i++) {
blockNumbers.push(blockNumber);
blockNumber--;
}
if (!blockNumbers.length) {
return [];
}
const blocks = await Promise.all(
blockNumbers.map((n) => api.derive.chain.getBlockByNumber(n))
);
return blocks.filter((b): b is SignedBlockExtended => !!b);
}
export default function useLatency (): Result {
const { api } = useApi();
const [details, setDetails] = useState<Detail[]>([]);
const signedBlock = useCall<SignedBlockExtended>(api.derive.chain.subscribeNewBlocks, []);
const hasHistoric = useRef(false);
useEffect((): void => {
if (!signedBlock) {
return;
}
setDetails((prev) =>
extractNext(prev, signedBlock)
);
if (hasHistoric.current) {
return;
}
hasHistoric.current = true;
getPrev(api, signedBlock)
.then((blocks) => {
if (!blocks.length) {
return;
}
setDetails((prev) =>
blocks
.reduce((p, b) => extractPrev(p, b), prev)
.filter(({ delay }) => delay)
);
})
.catch(console.error);
}, [api, signedBlock]);
return useMemo((): Result => {
const filtered = details.filter(({ delay }) => delay);
const delays = filtered.map(({ delay }) => delay);
return filtered.length
? {
details: filtered,
timeAvg: delays.reduce((avg, d) => avg + (d / delays.length), 0),
timeMax: Math.max(...delays),
timeMin: Math.min(...delays)
}
: { details: [], timeAvg: 0, timeMax: 0, timeMin: 0 };
}, [details]);
}

View File

@ -12,6 +12,7 @@ import { BlockAuthorsContext, EventsContext } from '@polkadot/react-query';
import BlockInfo from './BlockInfo';
import Forks from './Forks';
import Latency from './Latency';
import Main from './Main';
import NodeInfo from './NodeInfo';
import { useTranslation } from './translate';
@ -39,6 +40,10 @@ function ExplorerApp ({ basePath, className }: Props): React.ReactElement<Props>
name: 'query',
text: t<string>('Block details')
},
{
name: 'latency',
text: t<string>('Latency')
},
{
name: 'forks',
text: t<string>('Forks')
@ -63,6 +68,7 @@ function ExplorerApp ({ basePath, className }: Props): React.ReactElement<Props>
/>
<Switch>
<Route path={`${basePath}/forks`}><Forks /></Route>
<Route path={`${basePath}/latency`}><Latency /></Route>
<Route path={`${basePath}/query/:value`}><BlockInfo /></Route>
<Route path={`${basePath}/query`}><BlockInfo /></Route>
<Route path={`${basePath}/node`}><NodeInfo /></Route>

View File

@ -8,7 +8,7 @@ import ChartJs from 'chart.js';
import React, { useMemo } from 'react';
import * as Chart from 'react-chartjs-2';
import { isBn } from '@polkadot/util';
import { isBn, objectSpread } from '@polkadot/util';
interface State {
chartData: ChartJs.ChartData;
@ -49,7 +49,7 @@ const chartOptions = {
}
};
function calculateOptions (colors: (string | undefined)[] = [], legends: string[], labels: string[], values: (number | BN)[][]): State {
function calculateOptions (colors: (string | undefined)[] = [], legends: string[], labels: string[], values: (number | BN)[][], options: ChartJs.ChartOptions = {}): State {
const chartData = values.reduce((chartData, values, index): Config => {
const color = colors[index] || alphaColor(COLORS[index]);
const data = values.map((value): number => isBn(value) ? value.toNumber() : value);
@ -68,14 +68,14 @@ function calculateOptions (colors: (string | undefined)[] = [], legends: string[
return {
chartData,
chartOptions
chartOptions: objectSpread({}, chartOptions, options)
};
}
function LineChart ({ className, colors, labels, legends, values }: LineProps): React.ReactElement<LineProps> | null {
function LineChart ({ className, colors, labels, legends, options, values }: LineProps): React.ReactElement<LineProps> | null {
const { chartData, chartOptions } = useMemo(
() => calculateOptions(colors, legends, labels, values),
[colors, labels, legends, values]
() => calculateOptions(colors, legends, labels, values, options),
[colors, labels, legends, options, values]
);
return (

View File

@ -3,6 +3,8 @@
import type { BN } from '@polkadot/util';
import ChartJs from 'chart.js';
export interface DoughnutValue {
colors: string[];
label: string;
@ -41,5 +43,6 @@ export interface LineProps {
className?: string;
labels: string[];
legends: string[];
options?: ChartJs.ChartOptions;
values: (number | BN)[][];
}