-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
199 additions
and
42 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,46 +1,9 @@ | ||
import { Progress } from '@penumbra-zone/ui'; | ||
import { motion } from 'framer-motion'; | ||
import { CheckIcon } from '@radix-ui/react-icons'; | ||
import { BlockSyncStatus } from '@penumbra-zone/ui'; | ||
import { useSyncProgress } from '../../../hooks/last-block-synced'; | ||
|
||
export const BlockSync = () => { | ||
const { lastBlockHeight, lastBlockSynced } = useSyncProgress(); | ||
if (!lastBlockHeight) return <></>; | ||
|
||
return ( | ||
<motion.div | ||
className='flex select-none flex-col items-center gap-1 leading-[30px]' | ||
initial={{ opacity: 0 }} | ||
animate={{ opacity: 1, transition: { duration: 0.5, ease: 'easeOut' } }} | ||
exit={{ opacity: 0 }} | ||
> | ||
{lastBlockHeight ? ( | ||
// Is syncing ⏳ | ||
lastBlockHeight - lastBlockSynced > 10 ? ( | ||
<> | ||
<div className='flex gap-2'> | ||
<p className='font-headline text-xl font-semibold text-sand'>Syncing blocks...</p> | ||
</div> | ||
<Progress variant='in-progress' value={(lastBlockSynced / lastBlockHeight) * 100} /> | ||
<p className='font-mono text-sand'> | ||
{lastBlockSynced}/{lastBlockHeight} | ||
</p> | ||
</> | ||
) : ( | ||
// Is synced ✅ | ||
<> | ||
<div className='flex gap-2'> | ||
<div className='flex items-center'> | ||
<p className='font-headline text-xl font-semibold text-teal'>Blocks synced</p> | ||
<CheckIcon className='size-6 text-teal' /> | ||
</div> | ||
</div> | ||
<Progress variant='done' value={(lastBlockSynced / lastBlockHeight) * 100} /> | ||
<p className='font-mono text-teal'> | ||
{lastBlockSynced}/{lastBlockHeight} | ||
</p> | ||
</> | ||
) | ||
) : null} | ||
</motion.div> | ||
); | ||
return <BlockSyncStatus lastBlockSynced={lastBlockSynced} lastBlockHeight={lastBlockHeight} />; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/** | ||
* Inspired from: https://github.com/rsocket/ewma/blob/master/index.js | ||
* However, the reason why we have our own code for this is because this library | ||
* was missing types and can only run in nodejs, not a browser environment. | ||
* | ||
* Ewma (Exponential Weighted Moving Average) is used to calculate a smoothed average of a series | ||
* of numbers, where more recent values have a greater weight than older values. This class is | ||
* particularly useful for smoothing out time-series data or calculating an average that needs to | ||
* adapt quickly to changes. The half-life parameter controls how quickly the weights decrease for | ||
* older values. | ||
*/ | ||
export class EWMA { | ||
private readonly decay: number; | ||
private ewma: number; | ||
private lastUpdate: number; | ||
|
||
/** | ||
* Initializes a new instance of the Ewma class. | ||
* @param halfLifeMs The half-life in milliseconds, indicating how quickly the influence of a value | ||
* decreases to half its original weight. This parameter directly impacts the speed of convergence | ||
* towards recent values, with lower values making the average more sensitive to recent changes. | ||
* @param initialValue The initial value of the EWMA, defaulted to 0 if not provided. | ||
*/ | ||
constructor(halfLifeMs = 10_000, initialValue = 0) { | ||
this.decay = halfLifeMs; | ||
this.ewma = initialValue; | ||
this.lastUpdate = Date.now(); | ||
} | ||
|
||
/** | ||
* Inserts a new value into the EWMA calculation, updating the average based on the elapsed time | ||
* since the last update and the new value's weight. | ||
* @param x The new numeric value to be added to the EWMA calculation. | ||
*/ | ||
insert(x: number): void { | ||
const now = Date.now(); | ||
const elapsed = now - this.lastUpdate; | ||
this.lastUpdate = now; | ||
|
||
const weight = Math.pow(2, -elapsed / this.decay); | ||
this.ewma = weight * this.ewma + (1 - weight) * x; | ||
} | ||
|
||
/** | ||
* Resets the EWMA to a specified new value, effectively restarting the calculation as if this | ||
* new value was the first and only value provided. | ||
* @param x The new value to reset the EWMA calculation to. | ||
*/ | ||
reset(x: number): void { | ||
this.lastUpdate = Date.now(); | ||
this.ewma = x; | ||
} | ||
|
||
/** | ||
* Returns the current value of the EWMA. | ||
* @returns The current calculated EWMA value. | ||
*/ | ||
value(): number { | ||
return this.ewma; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { motion } from 'framer-motion'; | ||
import { CheckIcon } from '@radix-ui/react-icons'; | ||
import { Progress } from '../progress'; | ||
import { useSyncProgress } from './sync-progress-hook'; | ||
import { cn } from '../../../lib/utils'; | ||
|
||
interface BlockSyncProps { | ||
lastBlockHeight: number; | ||
lastBlockSynced: number; | ||
} | ||
|
||
export const BlockSyncStatus = ({ lastBlockHeight, lastBlockSynced }: BlockSyncProps) => { | ||
const { formattedTimeRemaining, confident } = useSyncProgress(lastBlockSynced, lastBlockHeight); | ||
|
||
return ( | ||
<motion.div | ||
className='flex select-none flex-col items-center gap-1 leading-[30px]' | ||
initial={{ opacity: 0 }} | ||
animate={{ opacity: 1, transition: { duration: 0.5, ease: 'easeOut' } }} | ||
exit={{ opacity: 0 }} | ||
> | ||
{lastBlockHeight ? ( | ||
// Is syncing ⏳ | ||
lastBlockHeight - lastBlockSynced > 10 ? ( | ||
<> | ||
<div className='flex gap-2'> | ||
<p className='font-headline text-xl font-semibold text-sand'>Syncing blocks...</p> | ||
</div> | ||
<Progress variant='in-progress' value={(lastBlockSynced / lastBlockHeight) * 100} /> | ||
<p className='font-mono text-sand'> | ||
{lastBlockSynced}/{lastBlockHeight} | ||
</p> | ||
<p | ||
className={cn( | ||
'-mt-1 font-mono text-sm text-sand transition-all duration-300', | ||
confident ? 'opacity-100' : 'opacity-0', | ||
)} | ||
> | ||
{formattedTimeRemaining} | ||
</p> | ||
</> | ||
) : ( | ||
// Is synced ✅ | ||
<> | ||
<div className='flex gap-2'> | ||
<div className='flex items-center'> | ||
<p className='font-headline text-xl font-semibold text-teal'>Blocks synced</p> | ||
<CheckIcon className='size-6 text-teal' /> | ||
</div> | ||
</div> | ||
<Progress variant='done' value={(lastBlockSynced / lastBlockHeight) * 100} /> | ||
<p className='font-mono text-teal'> | ||
{lastBlockSynced}/{lastBlockHeight} | ||
</p> | ||
</> | ||
) | ||
) : null} | ||
</motion.div> | ||
); | ||
}; |
56 changes: 56 additions & 0 deletions
56
packages/ui/components/ui/block-sync-status/sync-progress-hook.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { useEffect, useRef, useState } from 'react'; | ||
import { EWMA } from './ewma'; | ||
import humanizeDuration from 'humanize-duration'; | ||
|
||
/** | ||
* Custom hook to calculate synchronization speed and estimate the time remaining | ||
* for a synchronization process to complete, using the Exponential Weighted Moving Average (EWMA) | ||
* to smooth the speed calculation. | ||
* | ||
* @returns An object containing: | ||
* - speed: Current speed of synchronization in blocks per second. | ||
* - timeRemaining: Estimated time remaining to complete the synchronization, in seconds. | ||
* - formattedTimeRemaining: Human-readable string representation (e.g., "13 min, 49 sec"). | ||
* - confident: A boolean flag indicating whether the speed calculation is considered reliable. | ||
*/ | ||
export const useSyncProgress = ( | ||
lastBlockSynced: number, | ||
lastBlockHeight: number, | ||
syncUpdatesThreshold = 10, // The number of synchronization updates required before the speed calculation is considered reliable | ||
) => { | ||
const ewmaSpeedRef = useRef(new EWMA()); | ||
|
||
const [speed, setSpeed] = useState<number>(0); | ||
const lastSyncedRef = useRef<number>(lastBlockSynced); | ||
const lastUpdateTimeRef = useRef<number>(Date.now()); | ||
const [confident, setConfident] = useState<boolean>(false); // Tracks confidence in the speed calculation | ||
const [syncUpdates, setSyncUpdates] = useState<number>(0); // Tracks the number of synchronization updates | ||
|
||
useEffect(() => { | ||
const now = Date.now(); | ||
const timeElapsedMs = now - lastUpdateTimeRef.current; | ||
const blocksSynced = lastBlockSynced - lastSyncedRef.current; | ||
|
||
if (timeElapsedMs > 0 && blocksSynced >= 0) { | ||
const instantSpeed = (blocksSynced / timeElapsedMs) * 1000; // Calculate speed in blocks per second | ||
ewmaSpeedRef.current.insert(instantSpeed); | ||
setSpeed(ewmaSpeedRef.current.value()); | ||
setSyncUpdates(prev => prev + 1); // Increment the number of sync updates | ||
} | ||
|
||
lastSyncedRef.current = lastBlockSynced; | ||
lastUpdateTimeRef.current = now; | ||
|
||
// Update confident flag based on the number of sync updates | ||
if (syncUpdates >= syncUpdatesThreshold && !confident) { | ||
setConfident(true); | ||
} | ||
}, [lastBlockSynced, syncUpdates, syncUpdatesThreshold, confident]); | ||
|
||
const blocksRemaining = lastBlockHeight - lastBlockSynced; | ||
const timeRemaining = speed > 0 ? blocksRemaining / speed : Infinity; | ||
const formattedTimeRemaining = | ||
timeRemaining === Infinity ? '' : humanizeDuration(timeRemaining * 1000, { round: true }); | ||
|
||
return { speed, timeRemaining, formattedTimeRemaining, confident }; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.