Expose API stats tab (#7678)

This commit is contained in:
Jaco 2022-06-02 07:29:11 +02:00 committed by GitHub
parent 49f2c4c3bb
commit 471e62fc3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 77 additions and 59 deletions

View File

@ -21,10 +21,15 @@ interface ChartContents {
}
interface ChartInfo {
requests: ChartContents;
bytesChart: ChartContents;
errorsChart: ChartContents;
requestsChart: ChartContents;
}
const COLORS_REQUESTS = ['#008c8c', '#00448c', '#8c0044', '#acacac'];
// const COLORS_ERRORS = ['#8c0044', '#acacac'];
const COLORS_BYTES = ['#00448c', '#008c44', '#acacac'];
const COLORS_REQUESTS = ['#008c8c', '#00448c', '#8c4400', '#acacac'];
const OPTIONS = {
animation: {
duration: 0
@ -34,44 +39,74 @@ const OPTIONS = {
};
function getPoints (all: Stats[]): ChartInfo {
const requests: ChartContents = {
const bytesChart: ChartContents = {
labels: [],
values: [[], [], []]
};
const errorsChart: ChartContents = {
labels: [],
values: [[]]
};
const requestsChart: ChartContents = {
labels: [],
values: [[], [], [], []]
};
const reqBase = all.reduce((a, { stats: { active: { requests, subscriptions } } }) => a + requests + subscriptions, 0);
let { bytesRecv: prevRecv, bytesSent: prevSent, errors: prevErrors } = all[0].stats.total;
let recvTotal = 0;
for (let i = 0; i < all.length; i++) {
const { stats: { active: { requests: aReq, subscriptions: aSub } }, when } = all[i];
for (let i = 1; i < all.length; i++) {
const { stats: { active: { requests: aReq, subscriptions: aSub }, total: { bytesRecv, bytesSent, errors } }, when } = all[i];
const time = new Date(when).toLocaleTimeString();
requests.labels.push(time);
requests.values[0].push(aReq + aSub);
requests.values[1].push(aReq);
requests.values[2].push(aSub);
requests.values[3].push(reqBase / all.length);
bytesChart.labels.push(time);
bytesChart.values[0].push(bytesSent - prevSent);
bytesChart.values[1].push(bytesRecv - prevRecv);
errorsChart.labels.push(time);
errorsChart.values[0].push(errors - prevErrors);
requestsChart.labels.push(time);
requestsChart.values[0].push(aReq + aSub);
requestsChart.values[1].push(aReq);
requestsChart.values[2].push(aSub);
requestsChart.values[3].push(reqBase / all.length);
recvTotal += bytesRecv - prevRecv;
prevErrors = errors;
prevRecv = bytesRecv;
prevSent = bytesSent;
}
return { requests };
const recvAvg = recvTotal / (all.length - 1);
for (let i = 1; i < all.length; i++) {
bytesChart.values[2].push(recvAvg);
}
return { bytesChart, errorsChart, requestsChart };
}
function Api ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const stats = useContext(ApiStatsContext);
const { requestsLegend } = useMemo(
const { bytesLegend, requestsLegend } = useMemo(
() => ({
bytesLegend: [t<string>('sent'), t<string>('recv'), t<string>('average')],
errorsLegend: [t<string>('errors')],
requestsLegend: [t<string>('total'), t<string>('requests'), t<string>('subscriptions'), t<string>('average')]
}), [t]
);
const { requests } = useMemo(
const { bytesChart, requestsChart } = useMemo(
() => getPoints(stats),
[stats]
);
if (stats.length <= 2) {
if (stats.length <= 3) {
return <Spinner />;
}
@ -93,10 +128,20 @@ function Api ({ className }: Props): React.ReactElement<Props> {
<h1>{t<string>('requests & subscriptions')}</h1>
<Chart.Line
colors={COLORS_REQUESTS}
labels={requests.labels}
labels={requestsChart.labels}
legends={requestsLegend}
options={OPTIONS}
values={requests.values}
values={requestsChart.values}
/>
</div>
<div className='container'>
<h1>{t<string>('bytes')}</h1>
<Chart.Line
colors={COLORS_BYTES}
labels={bytesChart.labels}
legends={bytesLegend}
options={OPTIONS}
values={bytesChart.values}
/>
</div>
</div>

View File

@ -65,7 +65,6 @@ function createItemsRef (t: TFunction): TabItem[] {
text: t<string>('Node info')
},
{
isHidden: true,
name: 'api',
text: t<string>('API stats')
}

View File

@ -61,9 +61,8 @@ export const DEFAULT_AUX = ['Aux1', 'Aux2', 'Aux3', 'Aux4', 'Aux5', 'Aux6', 'Aux
const DISALLOW_EXTENSIONS: string[] = [];
let api: ApiPromise;
let apiSystem: ApiPromise;
export { api, apiSystem };
export { api };
function isKeyringLoaded () {
try {
@ -255,7 +254,7 @@ async function createApi (apiUrl: string, signer: ApiSigner, onError: (error: un
const isLight = apiUrl.startsWith('light://');
try {
const providers = [0, 1].map((): ProviderInterface =>
const providers = [0].map((): ProviderInterface =>
isLight
? new ScProvider(getWellKnownChain(apiUrl.replace('light://substrate-connect/', '')))
: new WsProvider(apiUrl)
@ -269,7 +268,6 @@ async function createApi (apiUrl: string, signer: ApiSigner, onError: (error: un
typesBundle,
typesChain
});
apiSystem = new ApiPromise({ provider: providers[1] });
// See https://github.com/polkadot-js/api/pull/4672#issuecomment-1078843960
if (isLight) {
@ -300,7 +298,7 @@ function Api ({ apiUrl, children, isElectron, store }: Props): React.ReactElemen
);
const apiRelay = useApiUrl(relayUrls);
const value = useMemo<ApiProps>(
() => objectSpread({}, state, { api, apiEndpoint, apiError, apiRelay, apiSystem, apiUrl, createLink: createLink(apiUrl, isElectron), extensions, getStats, isApiConnected, isApiInitialized, isElectron, isWaitingInjected: !extensions }),
() => objectSpread({}, state, { api, apiEndpoint, apiError, apiRelay, apiUrl, createLink: createLink(apiUrl, isElectron), extensions, getStats, isApiConnected, isApiInitialized, isElectron, isWaitingInjected: !extensions }),
[apiError, extensions, isApiConnected, isApiInitialized, isElectron, state, apiEndpoint, apiRelay, apiUrl]
);

View File

@ -35,7 +35,6 @@ export interface ApiProps extends ApiState {
apiEndpoint: LinkOption | null;
apiError: string | null;
apiRelay: ApiPromise | null;
apiSystem: ApiPromise;
apiUrl?: string;
createLink: (path: string, apiUrl?: string) => string;
extensions?: InjectedExtension[];

View File

@ -3,14 +3,7 @@
import type { ProviderStats } from '@polkadot/rpc-provider/types';
export interface StatsExtended extends ProviderStats {
max: {
requests: number;
subscriptions: number;
};
}
export interface Stats {
stats: StatsExtended;
stats: ProviderStats;
when: number;
}

View File

@ -7,11 +7,11 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { createNamedHook, useApi } from '@polkadot/react-hooks';
const MAX_NUM = 100;
const MAX_NUM = 60; // 5 minutes
const INTERVAL = 5_000;
function useApiStatsImpl (): Stats[] {
const { api, apiSystem, getStats } = useApi();
const { api, getStats } = useApi();
const [stats, setStats] = useState<Stats[]>([]);
const timerId = useRef<NodeJS.Timeout | null>(null);
@ -19,25 +19,18 @@ function useApiStatsImpl (): Stats[] {
(): void => {
timerId.current = null;
const [stats, when] = getStats(api, apiSystem);
const [stats, when] = getStats(api);
setStats((prev): Stats[] => {
if (prev.length === 0) {
return [{ stats: { ...stats, max: { requests: stats.active.requests, subscriptions: stats.active.subscriptions } }, when }];
}
const last = prev[prev.length - 1].stats;
const curr = {
stats: {
...stats,
max: {
requests: Math.max(stats.active.requests, last.max.requests),
subscriptions: Math.max(stats.active.subscriptions, last.max.subscriptions)
}
},
stats,
when
};
if (prev.length === 0) {
return [curr];
}
return prev.length === MAX_NUM
? prev.concat(curr).slice(-MAX_NUM)
: prev.concat(curr);
@ -45,7 +38,7 @@ function useApiStatsImpl (): Stats[] {
timerId.current = setTimeout(() => fireTimer(), INTERVAL);
},
[api, apiSystem, getStats, timerId]
[api, getStats, timerId]
);
useEffect((): () => void => {

View File

@ -3,24 +3,15 @@
import type { ApiPromise } from '@polkadot/api';
import { useEffect, useMemo, useState } from 'react';
import { useMemo } from 'react';
import { createNamedHook } from './createNamedHook';
import { useApi } from './useApi';
function useSystemApiImpl (): ApiPromise | undefined {
const { apiRelay, apiSystem } = useApi();
const [apiDefault, setApiDefault] = useState<ApiPromise | undefined>();
const { api, apiRelay } = useApi();
// This system API does not use the global ready, so we
// explicitly wait for it here
useEffect((): void => {
apiSystem && apiSystem.isReady
.then(setApiDefault)
.catch(console.error);
}, [apiSystem]);
return useMemo(() => apiRelay || apiDefault, [apiRelay, apiDefault]);
return useMemo(() => apiRelay || api, [apiRelay, api]);
}
export const useSystemApi = createNamedHook('useSystemApi', useSystemApiImpl);