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 (
+
+ );
+ }
+ const numShards =
+ chainProcessingInfo!.status_response.ChainProcessingStatus.blocks_info[0].chunks_info!
+ .length;
+ const shardIndices = [...Array(numShards).keys()];
+ return (
+
+
+
+
+ Height |
+ Hash |
+ Received |
+ Status |
+ In Progress for |
+ In Orphan for |
+ Missing Chunks for |
+ {shardIndices.map((shardIndex) => {
+ return Shard {shardIndex} | ;
+ })}
+
+
+
+ {chainProcessingInfo!.status_response.ChainProcessingStatus.blocks_info.map(
+ (block) => {
+ return (
+
+ {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)} |
+ {block.chunks_info!.map((chunk) => {
+ if (chunk) {
+ return (
+
+ {`${
+ 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
+ )}`}
+ >
+ )}
+ |
+ );
+ } else {
+ return 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 (
+
+ );
+ }
+ 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 (
+
+ );
+ }
+ return (
+
+
+ {' '}
+ Floating chunks are chunks that we are not yet sure which blocks they belong to.{' '}
+
+
+
+
+ Height |
+ ShardId |
+ Hash |
+ Created by |
+ Status |
+
+
+
+ {chainProcessingInfo!.status_response.ChainProcessingStatus.floating_chunks_info.map(
+ (chunk) => {
+ return (
+
+ {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;
|