Skip to content

Commit

Permalink
Fix slow loading on Staking page (#705)
Browse files Browse the repository at this point in the history
* Render all validator infos at once instead of streaming

* Flush to state periodically

* Tweak how we load delegations

* Pass down more props to avoid unnecessary rerenders

* Move Delegations to its own component to avoid unnecessary rerenders

* Fix tests

* Use more granular selectors instead of prop drilling

* Use useShallow

* Revert name change

* Tweak syntax

* Simplify use of state in Delegations; remove unused props

* Remove unneeded fragment

* Tweak logic; add comments

* Add performance learnings to docs

* Create a useStoreShallow hook

* Tweak interval

* Allow fetching metadata by any property of AssetID (#719)

* Use throttle()

* Fix test
  • Loading branch information
jessepinho authored Mar 12, 2024
1 parent f06c2a8 commit c3903a5
Show file tree
Hide file tree
Showing 17 changed files with 344 additions and 120 deletions.
2 changes: 2 additions & 0 deletions apps/minifront/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"bignumber.js": "^9.1.2",
"date-fns": "^3.3.1",
"immer": "^10.0.4",
"lodash": "^4.17.21",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet": "^6.1.0",
Expand All @@ -36,6 +37,7 @@
"@penumbra-zone/polyfills": "workspace:*",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@types/lodash": "^4.17.0",
"@types/react": "^18.2.64",
"@types/react-dom": "^18.2.21",
"@types/react-helmet": "^6.1.11",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ValidatorInfoComponent } from './validator-info-component';
import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value';
import { StakingActions } from './staking-actions';
import { getValidatorInfoFromValueView } from '@penumbra-zone/getters';
import { memo } from 'react';

/**
* Renders a `ValueView` that contains a delegation token, along with the
Expand All @@ -12,43 +13,46 @@ import { getValidatorInfoFromValueView } from '@penumbra-zone/getters';
* https://github.com/penumbra-zone/penumbra/issues/3882, we may be able to
* remove `votingPowerAsIntegerPercentage`.
*/
export const DelegationValueView = ({
valueView,
votingPowerAsIntegerPercentage,
unstakedTokens,
}: {
/**
* A `ValueView` representing the address's balance of the given delegation
* token.
*/
valueView: ValueView;
votingPowerAsIntegerPercentage?: number;
/**
* A `ValueView` representing the address's balance of staking (UM) tokens.
* Used to show the user how many tokens they have available to delegate.
*/
unstakedTokens?: ValueView;
}) => {
const validatorInfo = getValidatorInfoFromValueView(valueView);
export const DelegationValueView = memo(
({
valueView,
votingPowerAsIntegerPercentage,
unstakedTokens,
}: {
/**
* A `ValueView` representing the address's balance of the given delegation
* token.
*/
valueView: ValueView;
votingPowerAsIntegerPercentage?: number;
/**
* A `ValueView` representing the address's balance of staking (UM) tokens.
* Used to show the user how many tokens they have available to delegate.
*/
unstakedTokens?: ValueView;
}) => {
const validatorInfo = getValidatorInfoFromValueView(valueView);

return (
<div className='flex flex-col gap-4 lg:flex-row lg:items-center lg:gap-8'>
<div className='min-w-0 shrink grow'>
<ValidatorInfoComponent
return (
<div className='flex flex-col gap-4 lg:flex-row lg:items-center lg:gap-8'>
<div className='min-w-0 shrink grow'>
<ValidatorInfoComponent
validatorInfo={validatorInfo}
votingPowerAsIntegerPercentage={votingPowerAsIntegerPercentage}
/>
</div>

<div className='shrink lg:max-w-[200px]'>
<ValueViewComponent view={valueView} />
</div>

<StakingActions
validatorInfo={validatorInfo}
votingPowerAsIntegerPercentage={votingPowerAsIntegerPercentage}
delegationTokens={valueView}
unstakedTokens={unstakedTokens}
/>
</div>

<div className='shrink lg:max-w-[200px]'>
<ValueViewComponent view={valueView} />
</div>

<StakingActions
validatorInfo={validatorInfo}
delegationTokens={valueView}
unstakedTokens={unstakedTokens}
/>
</div>
);
};
);
},
);
DelegationValueView.displayName = 'DelegationValueView';
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Button } from '@penumbra-zone/ui';
import { useStore } from '../../../../../state';
import { stakingSelector } from '../../../../../state/staking';
import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb';
import { getAmount, getValidator } from '@penumbra-zone/getters';
import { joinLoHiAmount } from '@penumbra-zone/types';
import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { FormDialog } from './form-dialog';
import { useMemo } from 'react';
import { AllSlices } from '../../../../../state';
import { useStoreShallow } from '../../../../../utils/use-store-shallow';

const stakingActionsSelector = (state: AllSlices) => ({
action: state.staking.action,
amount: state.staking.amount,
delegate: state.staking.delegate,
undelegate: state.staking.undelegate,
onClickActionButton: state.staking.onClickActionButton,
onClose: state.staking.onClose,
setAmount: state.staking.setAmount,
validatorInfo: state.staking.validatorInfo,
});

/**
* Renders Delegate/Undelegate buttons for a validator, as well as a form inside
Expand All @@ -30,17 +41,7 @@ export const StakingActions = ({
*/
unstakedTokens?: ValueView;
}) => {
const {
action,
amount,
onClickActionButton,
delegate,
undelegate,
onClose,
setAmount,
validatorInfo: selectedValidatorInfo,
} = useStore(stakingSelector);

const state = useStoreShallow(stakingActionsSelector);
const validator = getValidator(validatorInfo);

const canDelegate = useMemo(
Expand All @@ -53,8 +54,8 @@ export const StakingActions = ({
);

const handleSubmit = () => {
if (action === 'delegate') void delegate();
else void undelegate();
if (state.action === 'delegate') void state.delegate();
else void state.undelegate();
};

return (
Expand All @@ -64,30 +65,30 @@ export const StakingActions = ({
<Button
className='px-4'
disabled={!canDelegate}
onClick={() => onClickActionButton('delegate', validatorInfo)}
onClick={() => state.onClickActionButton('delegate', validatorInfo)}
>
Delegate
</Button>
<Button
variant='secondary'
className='px-4'
disabled={!canUndelegate}
onClick={() => onClickActionButton('undelegate', validatorInfo)}
onClick={() => state.onClickActionButton('undelegate', validatorInfo)}
>
Undelegate
</Button>
</div>
</div>

<FormDialog
action={action}
open={!!action && validator.equals(getValidator(selectedValidatorInfo))}
action={state.action}
open={!!state.action && validator.equals(getValidator(state.validatorInfo))}
validator={validator}
amount={amount}
amount={state.amount}
delegationTokens={delegationTokens}
unstakedTokens={unstakedTokens}
onChangeAmount={setAmount}
onClose={onClose}
onChangeAmount={state.setAmount}
onClose={state.onClose}
onSubmit={handleSubmit}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@penumbra-zone/ui';
import { useStore } from '../../../../state';

/**
* Renders a single `ValidatorInfo`: its name, bech32-encoded identity key,
Expand All @@ -21,6 +22,9 @@ export const ValidatorInfoComponent = ({
validatorInfo: ValidatorInfo;
votingPowerAsIntegerPercentage?: number;
}) => {
// The tooltip component is a bit heavy to render, so we'll wait to render it
// until all loading completes.
const showTooltips = useStore(state => !state.staking.loading);
const validator = getValidator(validatorInfo);
const identityKey = getIdentityKeyFromValidatorInfo(validatorInfo);

Expand All @@ -43,24 +47,28 @@ export const ValidatorInfoComponent = ({

{votingPowerAsIntegerPercentage !== undefined && (
<span>
<Tooltip>
<TooltipTrigger>
<span className='underline decoration-dotted underline-offset-4'>VP:</span>
</TooltipTrigger>
<TooltipContent>Voting power</TooltipContent>
</Tooltip>{' '}
{votingPowerAsIntegerPercentage}%
{showTooltips && (
<Tooltip>
<TooltipTrigger>
<span className='underline decoration-dotted underline-offset-4'>VP:</span>
</TooltipTrigger>
<TooltipContent>Voting power</TooltipContent>
</Tooltip>
)}
{!showTooltips && <span>VP:</span>} {votingPowerAsIntegerPercentage}%
</span>
)}

<span>
<Tooltip>
<TooltipTrigger>
<span className='underline decoration-dotted underline-offset-4'>Com:</span>
</TooltipTrigger>
<TooltipContent>Commission</TooltipContent>
</Tooltip>{' '}
{calculateCommissionAsPercentage(validatorInfo)}%
{showTooltips && (
<Tooltip>
<TooltipTrigger>
<span className='underline decoration-dotted underline-offset-4'>Com:</span>
</TooltipTrigger>
<TooltipContent>Commission</TooltipContent>
</Tooltip>
)}
{!showTooltips && <span>Com:</span>} {calculateCommissionAsPercentage(validatorInfo)}%
</span>
</div>
</div>
Expand Down
38 changes: 38 additions & 0 deletions apps/minifront/src/components/staking/account/delegations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getIdentityKeyFromValueView } from '@penumbra-zone/getters';
import { VotingPowerAsIntegerPercentage, bech32IdentityKey } from '@penumbra-zone/types';
import { AllSlices } from '../../../state';
import { DelegationValueView } from './delegation-value-view';
import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { useStoreShallow } from '../../../utils/use-store-shallow';

const getVotingPowerAsIntegerPercentage = (
votingPowerByValidatorInfo: Record<string, VotingPowerAsIntegerPercentage>,
delegation: ValueView,
) => votingPowerByValidatorInfo[bech32IdentityKey(getIdentityKeyFromValueView(delegation))];

const delegationsSelector = (state: AllSlices) => ({
delegations: state.staking.delegationsByAccount.get(state.staking.account) ?? [],
unstakedTokens: state.staking.unstakedTokensByAccount.get(state.staking.account),
votingPowerByValidatorInfo: state.staking.votingPowerByValidatorInfo,
});

export const Delegations = () => {
const { delegations, unstakedTokens, votingPowerByValidatorInfo } =
useStoreShallow(delegationsSelector);

return (
<div className='mt-8 flex flex-col gap-8'>
{delegations.map(delegation => (
<DelegationValueView
key={bech32IdentityKey(getIdentityKeyFromValueView(delegation))}
valueView={delegation}
unstakedTokens={unstakedTokens}
votingPowerAsIntegerPercentage={getVotingPowerAsIntegerPercentage(
votingPowerByValidatorInfo,
delegation,
)}
/>
))}
</div>
);
};
47 changes: 9 additions & 38 deletions apps/minifront/src/components/staking/account/index.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,20 @@
import { VotingPowerAsIntegerPercentage, bech32IdentityKey } from '@penumbra-zone/types';
import { getIdentityKeyFromValueView } from '@penumbra-zone/getters';
import { useStore } from '../../../state';
import { stakingSelector } from '../../../state/staking';
import { DelegationValueView } from './delegation-value-view';
import { Card, CardContent, CardHeader, CardTitle } from '@penumbra-zone/ui';
import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { Header } from './header';

const getVotingPowerAsIntegerPercentage = (
votingPowerByValidatorInfo: Record<string, VotingPowerAsIntegerPercentage>,
delegation: ValueView,
) => votingPowerByValidatorInfo[bech32IdentityKey(getIdentityKeyFromValueView(delegation))];
import { Delegations } from './delegations';

export const Account = () => {
const { account, delegationsByAccount, unstakedTokensByAccount, votingPowerByValidatorInfo } =
useStore(stakingSelector);
const unstakedTokens = unstakedTokensByAccount.get(account);
const delegations = delegationsByAccount.get(account) ?? [];

return (
<div className='flex flex-col gap-4'>
<Header />

{!!delegations.length && (
<Card>
<CardHeader>
<CardTitle>Delegation tokens</CardTitle>
</CardHeader>
<CardContent>
<div className='mt-8 flex flex-col gap-8'>
{delegations.map(delegation => (
<DelegationValueView
key={bech32IdentityKey(getIdentityKeyFromValueView(delegation))}
valueView={delegation}
unstakedTokens={unstakedTokens}
votingPowerAsIntegerPercentage={getVotingPowerAsIntegerPercentage(
votingPowerByValidatorInfo,
delegation,
)}
/>
))}
</div>
</CardContent>
</Card>
)}
<Card>
<CardHeader>
<CardTitle>Delegation tokens</CardTitle>
</CardHeader>
<CardContent>
<Delegations />
</CardContent>
</Card>
</div>
);
};
4 changes: 3 additions & 1 deletion apps/minifront/src/state/staking/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
AddressView,
IdentityKey,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb';
import { accountsSelector } from '.';
import { THROTTLE_MS, accountsSelector } from '.';

const validator1IdentityKey = new IdentityKey({ ik: new Uint8Array([1, 2, 3]) });
const validator1Bech32IdentityKey = bech32IdentityKey(validator1IdentityKey);
Expand Down Expand Up @@ -178,6 +178,7 @@ describe('Staking Slice', () => {

beforeEach(() => {
useStore = create<AllSlices>()(initializeStore()) as UseBoundStore<StoreApi<AllSlices>>;
vi.useFakeTimers();
});

it('has correct initial state', () => {
Expand Down Expand Up @@ -208,6 +209,7 @@ describe('Staking Slice', () => {
const { getState } = useStore;

await getState().staking.loadDelegationsForCurrentAccount();
vi.advanceTimersByTime(THROTTLE_MS);

const delegations = getState().staking.delegationsByAccount.get(0)!;

Expand Down
Loading

0 comments on commit c3903a5

Please sign in to comment.