From b3df3176241a91e83c9061b441d3a11516981601 Mon Sep 17 00:00:00 2001 From: SoonNear <126540305+soonnear@users.noreply.github.com> Date: Thu, 13 Apr 2023 23:04:34 -0400 Subject: [PATCH] Migrate Chain and Chunks Info Page #8350 (#8903) * Migrate Chain and Chunks Info Page #8350

Problem

Need migrate chain and chunks info page to Typescript + React framework.

What has changed

- Fixed bug on old page for not displaying floating chunks - Migrated the page with exactly the same format - used multiple navigation bars

Testing

- Ran linter - Attached before and after screenshots of UI on github issue and pull request - Manually viewed pages on UI ``` cd nearcore make debug nearup run localnet --binary-path ~/Github/near/nearcore/target/debug/ open http://localhost:3030/debug/pages/chain_n_chunk_info cd nearcore/tools/debug-ui npm install npm start open http://localhost:3000/localhost:3030/chain_and_chunk_info open http://localhost:3000/localhost:3030/chain_and_chunk_info/chain_info_summary open http://localhost:3000/localhost:3030/chain_and_chunk_info/floating_chunks open http://localhost:3000/localhost:3030/chain_and_chunk_info/blocks ``` * Addressed review 1 comments - Fixed enum format and printing of enum - Used and instead of conditional operator with a useless else statement - Added return type to some functions - Initialize numShards as constant - Used templated literals - String concat or string interpolation --- chain/jsonrpc/res/chain_n_chunk_info.html | 1 + tools/debug-ui/README.md | 2 +- tools/debug-ui/src/App.tsx | 7 +- tools/debug-ui/src/BlocksView.scss | 34 ++++ tools/debug-ui/src/BlocksView.tsx | 164 ++++++++++++++++++ tools/debug-ui/src/ChainAndChunkInfoView.scss | 33 ++++ tools/debug-ui/src/ChainAndChunkInfoView.tsx | 37 ++++ tools/debug-ui/src/ChainInfoSummaryView.scss | 5 + tools/debug-ui/src/ChainInfoSummaryView.tsx | 91 ++++++++++ tools/debug-ui/src/FloatingChunksView.scss | 34 ++++ tools/debug-ui/src/FloatingChunksView.tsx | 64 +++++++ tools/debug-ui/src/api.tsx | 65 +++++++ tools/debug-ui/src/utils.tsx | 4 +- 13 files changed, 536 insertions(+), 5 deletions(-) create mode 100644 tools/debug-ui/src/BlocksView.scss create mode 100644 tools/debug-ui/src/BlocksView.tsx create mode 100644 tools/debug-ui/src/ChainAndChunkInfoView.scss create mode 100644 tools/debug-ui/src/ChainAndChunkInfoView.tsx create mode 100644 tools/debug-ui/src/ChainInfoSummaryView.scss create mode 100644 tools/debug-ui/src/ChainInfoSummaryView.tsx create mode 100644 tools/debug-ui/src/FloatingChunksView.scss create mode 100644 tools/debug-ui/src/FloatingChunksView.tsx diff --git a/chain/jsonrpc/res/chain_n_chunk_info.html b/chain/jsonrpc/res/chain_n_chunk_info.html index 9b76e0da44b..2c422a22184 100644 --- a/chain/jsonrpc/res/chain_n_chunk_info.html +++ b/chain/jsonrpc/res/chain_n_chunk_info.html @@ -144,6 +144,7 @@ row.append($('').append(chunk.chunk_hash)); row.append($('').append(chunk.created_by)); row.append($('').append(chunk.status)); + $('.js-floating-chunks-tbody').append(row); }) generateBlocksTableHeader(num_shards); } diff --git a/tools/debug-ui/README.md b/tools/debug-ui/README.md index a23c7e72ae9..7e338573698 100644 --- a/tools/debug-ui/README.md +++ b/tools/debug-ui/README.md @@ -62,7 +62,7 @@ more precisely, using TestLoop from core/async/src/test_loop.rs), the test can b ``` cargo test -p near-chunks test_multi -- --show-output > ~/log.txt ``` -2. Go to the UI at `/logviz`, such as http://localhost:3030/logviz +2. Go to the UI at `/logviz`, such as http://localhost:3000/logviz 3. Drag the log.txt file into the UI. Screenshots: diff --git a/tools/debug-ui/src/App.tsx b/tools/debug-ui/src/App.tsx index 0e20db55399..a0b85dcdfef 100644 --- a/tools/debug-ui/src/App.tsx +++ b/tools/debug-ui/src/App.tsx @@ -1,6 +1,7 @@ import './App.scss'; import { NavLink } from 'react-router-dom'; import { Navigate, Route, Routes, useParams } from 'react-router'; +import { ChainAndChunkInfoView } from './ChainAndChunkInfoView'; import { ClusterView } from './ClusterView'; import { EpochInfoView } from './EpochInfoView'; import { HeaderBar } from './HeaderBar'; @@ -15,7 +16,6 @@ function useNodeAddr(): string { export const App = () => { const addr = useNodeAddr(); - return (
@@ -47,7 +47,10 @@ export const App = () => { } /> } /> } /> - TODO
} /> + } + /> TODO} /> TODO} /> } /> diff --git a/tools/debug-ui/src/BlocksView.scss b/tools/debug-ui/src/BlocksView.scss new file mode 100644 index 00000000000..d509577f4c0 --- /dev/null +++ b/tools/debug-ui/src/BlocksView.scss @@ -0,0 +1,34 @@ +.chain-and-chunk-blocks-view { + .error { + color: red; + } +} +.chain-and-chunk-blocks-table { + table { + width: 100%; + border-collapse: collapse; + } + + table, + th, + td { + border: 1px solid black; + } + + td { + text-align: left; + vertical-align: top; + padding: 8px; + } + + th { + text-align: center; + vertical-align: center; + padding: 8px; + background-color: lightgrey; + } + + tr.active { + background-color: #eff8bf; + } +} diff --git a/tools/debug-ui/src/BlocksView.tsx b/tools/debug-ui/src/BlocksView.tsx new file mode 100644 index 00000000000..f6da3292c93 --- /dev/null +++ b/tools/debug-ui/src/BlocksView.tsx @@ -0,0 +1,164 @@ +import './BlocksView.scss'; +import { useQuery } from 'react-query'; +import { fetchChainProcessingStatus, BlockProcessingStatus, ChunkProcessingStatus } from './api'; + +type BlocksViewProps = { + addr: string; +}; + +function prettyTime(datetime: string): string { + const time = new Date(Date.parse(datetime)); + return String(time.getUTCHours()).concat( + ':', + String(time.getUTCMinutes()).padStart(2, '0'), + ':', + String(time.getUTCSeconds()).padStart(2, '0'), + '.', + String(time.getUTCMilliseconds()).padStart(3, '0') + ); +} + +function printTimeInMs(time: number | null): string { + if (time == null) { + return 'N/A'; + } else { + return time + 'ms'; + } +} + +function printDuration(start: string, end: string): string { + const duration = Date.parse(end) - Date.parse(start); + if (duration > 0) { + return `+${duration}ms`; + } else { + return `${duration}ms`; + } +} + +function getChunkStatusSymbol(chunkStatus: ChunkProcessingStatus): string { + switch (chunkStatus) { + case 'Completed': + return '✔'; + case 'Requested': + return '⬇'; + case 'NeedToRequest': + return '.'; + default: + return ''; + } +} + +function printBlockStatus(blockStatus: BlockProcessingStatus): string { + if (typeof blockStatus === 'string') { + return blockStatus; + } + if ('Error' in blockStatus) { + return `Error: ${blockStatus.Error}`; + } + return `Dropped: ${blockStatus.Dropped}`; +} + +export const BlocksView = ({ addr }: BlocksViewProps) => { + const { + data: chainProcessingInfo, + error: chainProcessingInfoError, + isLoading: chainProcessingInfoLoading, + } = useQuery(['chainProcessingInfo', addr], () => fetchChainProcessingStatus(addr)); + if (chainProcessingInfoLoading) { + return
Loading...
; + } else if (chainProcessingInfoError) { + return ( +
+
{(chainProcessingInfoError as Error).stack}
+
+ ); + } else if (!chainProcessingInfo) { + return ( +
+
No Data
+
+ ); + } + const numShards = + chainProcessingInfo!.status_response.ChainProcessingStatus.blocks_info[0].chunks_info! + .length; + const shardIndices = [...Array(numShards).keys()]; + return ( +
+ + + + + + + + + + + {shardIndices.map((shardIndex) => { + return ; + })} + + + + {chainProcessingInfo!.status_response.ChainProcessingStatus.blocks_info.map( + (block) => { + return ( + + + + + + + + + {block.chunks_info!.map((chunk) => { + if (chunk) { + return ( + + ); + } else { + return ; + } + })} + + ); + } + )} + +
HeightHashReceivedStatusIn Progress forIn Orphan forMissing Chunks for Shard {shardIndex}
{block.height}{block.hash}{prettyTime(block.received_timestamp)}{printBlockStatus(block.block_status)}{printTimeInMs(block.in_progress_ms)}{printTimeInMs(block.orphaned_ms)}{printTimeInMs(block.missing_chunks_ms)} + {`${ + chunk.status + } ${getChunkStatusSymbol( + chunk.status + )}`} + {chunk.completed_timestamp && ( + <> +
Completed @ BR + {`${printDuration( + block.received_timestamp, + chunk.completed_timestamp + )}`} + + )} + {chunk.requested_timestamp && ( + <> +
Requested @ BR + {`${printDuration( + block.received_timestamp, + chunk.requested_timestamp + )}`} + + )} + {chunk.request_duration && ( + <> +
Duration + {`${printTimeInMs( + chunk.request_duration + )}`} + + )} +
No Chunk
+
+ ); +}; diff --git a/tools/debug-ui/src/ChainAndChunkInfoView.scss b/tools/debug-ui/src/ChainAndChunkInfoView.scss new file mode 100644 index 00000000000..5e30a435338 --- /dev/null +++ b/tools/debug-ui/src/ChainAndChunkInfoView.scss @@ -0,0 +1,33 @@ +.chain-and-chunk-info-view { + .error { + color: red; + } + + table { + width: 100%; + border-collapse: collapse; + } + + table, + th, + td { + border: 1px solid black; + } + + td { + text-align: left; + vertical-align: top; + padding: 8px; + } + + th { + text-align: center; + vertical-align: center; + padding: 8px; + background-color: lightgrey; + } + + tr.active { + background-color: #eff8bf; + } +} diff --git a/tools/debug-ui/src/ChainAndChunkInfoView.tsx b/tools/debug-ui/src/ChainAndChunkInfoView.tsx new file mode 100644 index 00000000000..59086be4ba4 --- /dev/null +++ b/tools/debug-ui/src/ChainAndChunkInfoView.tsx @@ -0,0 +1,37 @@ +import './ChainAndChunkInfoView.scss'; +import { NavLink, Navigate, Route, Routes } from 'react-router-dom'; +import { BlocksView } from './BlocksView'; +import { ChainInfoSummaryView } from './ChainInfoSummaryView'; +import { FloatingChunksView } from './FloatingChunksView'; + +type ChainAndChunkInfoViewProps = { + addr: string; +}; + +export const ChainAndChunkInfoView = ({ addr }: ChainAndChunkInfoViewProps) => { + return ( +
+
+ + Chain Info Summary + + + Floating Chunks + + + Blocks + +
+ + } /> + } /> + } /> + } /> + +
+ ); +}; + +function navLinkClassName({ isActive }: { isActive: boolean }) { + return isActive ? 'nav-link active' : 'nav-link'; +} diff --git a/tools/debug-ui/src/ChainInfoSummaryView.scss b/tools/debug-ui/src/ChainInfoSummaryView.scss new file mode 100644 index 00000000000..ad9baade398 --- /dev/null +++ b/tools/debug-ui/src/ChainInfoSummaryView.scss @@ -0,0 +1,5 @@ +.chain-info-summary-view { + .error { + color: red; + } +} diff --git a/tools/debug-ui/src/ChainInfoSummaryView.tsx b/tools/debug-ui/src/ChainInfoSummaryView.tsx new file mode 100644 index 00000000000..be002ce521a --- /dev/null +++ b/tools/debug-ui/src/ChainInfoSummaryView.tsx @@ -0,0 +1,91 @@ +import './ChainInfoSummaryView.scss'; +import { useMemo } from 'react'; +import { useQuery } from 'react-query'; +import { fetchChainProcessingStatus, fetchFullStatus } from './api'; + +type ChainInfoSummaryViewProps = { + addr: string; +}; + +export const ChainInfoSummaryView = ({ addr }: ChainInfoSummaryViewProps) => { + const { + data: fullStatus, + error: fullStatusError, + isLoading: fullStatusLoading, + } = useQuery(['fullStatus', addr], () => fetchFullStatus(addr)); + const { + data: chainProcessingInfo, + error: chainProcessingInfoError, + isLoading: chainProcessingInfoLoading, + } = useQuery(['chainProcessingInfo', addr], () => fetchChainProcessingStatus(addr)); + const { + chainInfoHead, + chainInfoHeaderHead, + numBlocksOrphanPool, + numBlocksMissingChunksPool, + numBlocksProcessing, + } = useMemo(() => { + let chainInfoHead = ''; + let chainInfoHeaderHead = ''; + let numBlocksOrphanPool = -1; + let numBlocksMissingChunksPool = -1; + let numBlocksProcessing = -1; + if (fullStatus && chainProcessingInfo) { + const head = fullStatus.detailed_debug_status!.current_head_status; + chainInfoHead += head.hash; + chainInfoHead += '@'; + chainInfoHead += head.height; + const headerHead = fullStatus.detailed_debug_status!.current_header_head_status; + chainInfoHeaderHead += headerHead.hash; + chainInfoHeaderHead += '@'; + chainInfoHeaderHead += headerHead.height; + const chainInfo = chainProcessingInfo.status_response.ChainProcessingStatus; + numBlocksOrphanPool = chainInfo.num_orphans; + numBlocksMissingChunksPool = chainInfo.num_blocks_missing_chunks; + numBlocksProcessing = chainInfo.num_blocks_in_processing; + } + return { + chainInfoHead, + chainInfoHeaderHead, + numBlocksOrphanPool, + numBlocksMissingChunksPool, + numBlocksProcessing, + }; + }, [fullStatus, chainProcessingInfo]); + if (fullStatusLoading || chainProcessingInfoLoading) { + return
Loading...
; + } else if (fullStatusError || chainProcessingInfoError) { + return ( +
+
+ {((fullStatusError || chainProcessingInfoError) as Error).stack} +
+
+ ); + } else if (!fullStatus || !chainProcessingInfo) { + return ( +
+
No Data
+
+ ); + } + return ( +
+

+ Current head: {`${chainInfoHead}`} +

+

+ Current header head: {`${chainInfoHeaderHead}`} +

+

+ Number of blocks in orphan pool: {`${numBlocksOrphanPool}`} +

+

+ Number of blocks in missing chunks pool: {`${numBlocksMissingChunksPool}`} +

+

+ Number of blocks in processing: {`${numBlocksProcessing}`} +

+
+ ); +}; diff --git a/tools/debug-ui/src/FloatingChunksView.scss b/tools/debug-ui/src/FloatingChunksView.scss new file mode 100644 index 00000000000..4d47c14999e --- /dev/null +++ b/tools/debug-ui/src/FloatingChunksView.scss @@ -0,0 +1,34 @@ +.floating-chunks-view { + .error { + color: red; + } +} +.floating-chunks-table { + table { + width: 100%; + border-collapse: collapse; + } + + table, + th, + td { + border: 1px solid black; + } + + td { + text-align: left; + vertical-align: top; + padding: 8px; + } + + th { + text-align: center; + vertical-align: center; + padding: 8px; + background-color: lightgrey; + } + + tr.active { + background-color: #eff8bf; + } +} diff --git a/tools/debug-ui/src/FloatingChunksView.tsx b/tools/debug-ui/src/FloatingChunksView.tsx new file mode 100644 index 00000000000..de1b163294d --- /dev/null +++ b/tools/debug-ui/src/FloatingChunksView.tsx @@ -0,0 +1,64 @@ +import './FloatingChunksView.scss'; +import { useQuery } from 'react-query'; +import { fetchChainProcessingStatus } from './api'; + +type FloatingChunksViewProps = { + addr: string; +}; + +export const FloatingChunksView = ({ addr }: FloatingChunksViewProps) => { + const { + data: chainProcessingInfo, + error: chainProcessingInfoError, + isLoading: chainProcessingInfoLoading, + } = useQuery(['chainProcessingInfo', addr], () => fetchChainProcessingStatus(addr)); + if (chainProcessingInfoLoading) { + return
Loading...
; + } else if (chainProcessingInfoError) { + return ( +
+
{(chainProcessingInfoError as Error).stack}
+
+ ); + } else if (!chainProcessingInfo) { + return ( +
+
No Data
+
+ ); + } + return ( +
+

+ {' '} + Floating chunks are chunks that we are not yet sure which blocks they belong to.{' '} +

+ + + + + + + + + + + + {chainProcessingInfo!.status_response.ChainProcessingStatus.floating_chunks_info.map( + (chunk) => { + return ( + + + + + + + + ); + } + )} + +
HeightShardIdHashCreated byStatus
{chunk.height_created}{chunk.shard_id}{chunk.chunk_hash}{chunk.created_by}{chunk.status}
+
+ ); +}; diff --git a/tools/debug-ui/src/api.tsx b/tools/debug-ui/src/api.tsx index 7ce87c9a131..f13ba8a75ab 100644 --- a/tools/debug-ui/src/api.tsx +++ b/tools/debug-ui/src/api.tsx @@ -293,6 +293,64 @@ export interface RecentOutboundConnectionsResponse { }; } +export type DroppedReason = 'HeightProcessed' | 'TooManyProcessingBlocks'; + +export type BlockProcessingStatus = + | 'Orphan' + | 'WaitingForChunks' + | 'InProcessing' + | 'Accepted' + | { Error: string } + | { Dropped: DroppedReason } + | 'Unknown'; + +export interface BlockProcessingInfo { + height: number; + hash: string; + received_timestamp: string; + in_progress_ms: number; + orphaned_ms: number | null; + missing_chunks_ms: number | null; + block_status: BlockProcessingStatus; + chunks_info: ChunkProcessingInfo[] | null; +} + +export interface PartCollectionInfo { + part_owner: string; + received_time: string | null; + forwarded_received_time: string | null; + chunk_received_time: string | null; +} + +export type ChunkProcessingStatus = 'NeedToRequest' | 'Requested' | 'Completed'; + +export interface ChunkProcessingInfo { + height_created: number; + shard_id: number; + chunk_hash: string; + prev_block_hash: string; + created_by: string | null; + status: ChunkProcessingStatus; + requested_timestamp: string | null; + completed_timestamp: string | null; + request_duration: number | null; + chunk_parts_collection: PartCollectionInfo[]; +} + +export interface ChainProcessingInfo { + num_blocks_in_processing: number; + num_orphans: number; + num_blocks_missing_chunks: number; + blocks_info: BlockProcessingInfo[]; + floating_chunks_info: ChunkProcessingInfo[]; +} + +export interface ChainProcessingStatusResponse { + status_response: { + ChainProcessingStatus: ChainProcessingInfo; + }; +} + export async function fetchBasicStatus(addr: string): Promise { const response = await fetch(`http://${addr}/status`); return await response.json(); @@ -338,3 +396,10 @@ export async function fetchRecentOutboundConnections( const response = await fetch(`http://${addr}/debug/api/recent_outbound_connections`); return await response.json(); } + +export async function fetchChainProcessingStatus( + addr: string +): Promise { + const response = await fetch(`http://${addr}/debug/api/chain_processing_status`); + return await response.json(); +} diff --git a/tools/debug-ui/src/utils.tsx b/tools/debug-ui/src/utils.tsx index babe0192fcb..958597d5e3e 100644 --- a/tools/debug-ui/src/utils.tsx +++ b/tools/debug-ui/src/utils.tsx @@ -51,11 +51,11 @@ export function addDebugPortLink(peer_network_addr: string): ReactElement { // https://github.com/near/nearcore/blob/700ec29270f72f2e78a17029b4799a8228926c07/chain/network/src/network_protocol/peer.rs#L13-L19 const DEFAULT_RPC_PORT = 3030; const DEFAULT_NETWORK_PORT = 24567; - const peer_network_addr_array = peer_network_addr.split(":"); + const peer_network_addr_array = peer_network_addr.split(':'); const peer_network_port = parseInt(peer_network_addr_array.pop() || '24567'); const peer_network_ip = peer_network_addr_array.pop() || peer_network_addr; let peer_num = 0; - if (peer_network_ip.includes("127.0.0.1")) { + if (peer_network_ip.includes('127.0.0.1')) { peer_num = peer_network_port - DEFAULT_NETWORK_PORT; } const peer_rpc_port = DEFAULT_RPC_PORT + peer_num;