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:
parent
327f281855
commit
0444b6da11
89
packages/page-explorer/src/Latency/index.tsx
Normal file
89
packages/page-explorer/src/Latency/index.tsx
Normal 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;
|
||||
}
|
||||
`;
|
||||
20
packages/page-explorer/src/Latency/types.ts
Normal file
20
packages/page-explorer/src/Latency/types.ts
Normal 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;
|
||||
}
|
||||
149
packages/page-explorer/src/Latency/useLatency.ts
Normal file
149
packages/page-explorer/src/Latency/useLatency.ts
Normal 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]);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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)[][];
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user