Skip to content

Commit

Permalink
Add block sync time estimate
Browse files Browse the repository at this point in the history
  • Loading branch information
grod220 committed Feb 23, 2024
1 parent 52d1393 commit cb4fab4
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 42 deletions.
43 changes: 3 additions & 40 deletions apps/extension/src/routes/popup/home/block-sync.tsx
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} />;
};
61 changes: 61 additions & 0 deletions packages/ui/components/ui/block-sync-status/ewma.ts
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;
}
}
60 changes: 60 additions & 0 deletions packages/ui/components/ui/block-sync-status/index.tsx
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 packages/ui/components/ui/block-sync-status/sync-progress-hook.ts
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 };
};
1 change: 1 addition & 0 deletions packages/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './components/ui/back-icon';
export * from './components/ui/block-sync-status';
export * from './components/ui/button';
export * from './components/ui/card';
export * from './components/ui/copy-to-clipboard';
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"clsx": "^2.1.0",
"djb2a": "^2.0.0",
"framer-motion": "^11.0.5",
"humanize-duration": "^3.31.0",
"lucide-react": "^0.331.0",
"react-dom": "^18.2.0",
"react-json-view": "^1.21.3",
Expand All @@ -47,6 +48,7 @@
"@storybook/react": "^7.6.16",
"@storybook/react-vite": "^7.6.17",
"@testing-library/react": "^14.2.1",
"@types/humanize-duration": "^3.27.4",
"@types/node": "^20.11.19",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
Expand Down
18 changes: 16 additions & 2 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cb4fab4

Please sign in to comment.