Reduce impact of scanning blocklists (#121)

## Issue
- Each tile was checking against 4 blocklists (blacklisted, filtered, muted, commentron) on every render. Loading the front-page with Cheese alone caused 1400 calls.
- This is also part of the reason why pressing Back into the tile list takes forever.

## Fix
Since we still need to perform the checks at the app side for now, tried to memoize the operation through a selector.
This commit is contained in:
infinite-persistence 2021-10-25 22:56:31 +08:00 committed by GitHub
parent dad7264636
commit a90c516c71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 122 deletions

View file

@ -56,6 +56,7 @@ export { selectFilteredOutpoints, selectFilteredOutpointMap } from './redux/sele
// selectFetchingTrendingUris,
// } from './redux/selectors/homepage';
export { selectViewCount, makeSelectViewCountForUri, makeSelectSubCountForUri } from './redux/selectors/stats';
export { selectBanStateForUri } from './redux/selectors/ban';
export {
selectHasSyncedWallet,
selectSyncData,

View file

@ -0,0 +1,96 @@
// @flow
// TODO: This should be in 'redux/selectors/claim.js'. Temporarily putting it
// here to get past importing issues with 'lbryinc', which the real fix might
// involve moving it from 'extras' to 'ui' (big change).
import { createCachedSelector } from 're-reselect';
import { selectClaimForUri } from 'redux/selectors/claims';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { selectModerationBlockList } from 'redux/selectors/comments';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { isURIEqual } from 'util/lbryURI';
const selectClaimExistsForUri = (state, uri) => {
return Boolean(selectClaimForUri(state, uri));
};
const selectTxidForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.txid : claim ? claim.txid : undefined;
};
const selectNoutForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.nout : claim ? claim.nout : undefined;
};
const selectPermanentUrlForUri = (state, uri) => {
const claim = selectClaimForUri(state, uri);
const signingChannel = claim && claim.signing_channel;
return signingChannel ? signingChannel.permanent_url : claim ? claim.permanent_url : undefined;
};
export const selectBanStateForUri = createCachedSelector(
// Break apart 'selectClaimForUri' into 4 cheaper selectors that return
// primitives for values that we care about. The Claim object itself is easily
// invalidated due to constantly-changing fields like 'confirmation'.
selectClaimExistsForUri,
selectTxidForUri,
selectNoutForUri,
selectPermanentUrlForUri,
selectBlackListedOutpoints,
selectFilteredOutpoints,
selectMutedChannels,
selectModerationBlockList,
(
claimExists,
txid,
nout,
permanentUrl,
blackListedOutpoints,
filteredOutpoints,
mutedChannelUris,
personalBlocklist
) => {
const banState = {};
if (!claimExists) {
return banState;
}
// This will be replaced once blocking is done at the wallet server level.
if (blackListedOutpoints) {
if (blackListedOutpoints.some((outpoint) => outpoint.txid === txid && outpoint.nout === nout)) {
banState['blacklisted'] = true;
}
}
// We're checking to see if the stream outpoint or signing channel outpoint
// is in the filter list.
if (filteredOutpoints) {
if (filteredOutpoints.some((outpoint) => outpoint.txid === txid && outpoint.nout === nout)) {
banState['filtered'] = true;
}
}
// block stream claims
// block channel claims if we can't control for them in claim search
if (mutedChannelUris.length) {
if (mutedChannelUris.some((blockedUri) => isURIEqual(blockedUri, permanentUrl))) {
banState['muted'] = true;
}
}
// Commentron blocklist
if (personalBlocklist.length) {
if (personalBlocklist.some((blockedUri) => isURIEqual(blockedUri, permanentUrl))) {
banState['blocked'] = true;
}
}
return banState;
}
)((state, uri) => String(uri));

View file

@ -21,13 +21,11 @@ import {
import { doResolveUri } from 'redux/actions/claims';
import { doCollectionEdit } from 'redux/actions/collections';
import { doFileGet } from 'redux/actions/file';
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectBanStateForUri } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectModerationBlockList } from 'redux/selectors/comments';
import ClaimPreview from './view';
import formatMediaDuration from 'util/formatMediaDuration';
@ -48,12 +46,8 @@ const select = (state, props) => {
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state),
mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
banState: selectBanStateForUri(state, props.uri),
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),

View file

@ -5,7 +5,7 @@ import { NavLink, withRouter } from 'react-router-dom';
import { isEmpty } from 'util/object';
import { lazyImport } from 'util/lazyImport';
import classnames from 'classnames';
import { isURIEqual, isURIValid } from 'util/lbryURI';
import { isURIValid } from 'util/lbryURI';
import * as COLLECTIONS_CONSTS from 'constants/collections';
import { formatLbryUrlForWeb } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
@ -52,18 +52,9 @@ type Props = {
nsfw: boolean,
placeholder: string,
type: string,
banState: { blacklisted?: boolean, filtered?: boolean, muted?: boolean, blocked?: boolean },
hasVisitedUri: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
}>,
filteredOutpoints: Array<{
txid: string,
nout: number,
}>,
mutedUris: Array<string>,
blockedUris: Array<string>,
channelIsBlocked: boolean,
actions: boolean | Node | string | number,
properties: boolean | Node | string | number | ((Claim) => Node),
empty?: Node,
@ -113,7 +104,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
streamingUrl,
mediaDuration,
// user properties
channelIsBlocked,
hasVisitedUri,
// component
history,
@ -139,10 +129,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
properties,
onClick,
actions,
mutedUris,
blockedUris,
blackListedOutpoints,
filteredOutpoints,
banState,
includeSupportAction,
renderActions,
hideMenu = false,
@ -246,28 +233,13 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
((abandoned && !showUnresolvedClaim) || (!claimIsMine && obscureNsfw && nsfw));
// This will be replaced once blocking is done at the wallet server level
if (claim && !claimIsMine && !shouldHide && blackListedOutpoints) {
shouldHide = blackListedOutpoints.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
}
// We're checking to see if the stream outpoint
// or signing channel outpoint is in the filter list
if (claim && !claimIsMine && !shouldHide && filteredOutpoints) {
shouldHide = filteredOutpoints.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
if (!shouldHide && !claimIsMine && (banState.blacklisted || banState.filtered)) {
shouldHide = true;
}
// block stream claims
if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) {
shouldHide = mutedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
}
if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
shouldHide = blockedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
if (!shouldHide && !showUserBlocked && (banState.muted || banState.blocked)) {
shouldHide = true;
}
if (!shouldHide && customShouldHide && claim) {
@ -482,7 +454,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
</div>
)}
{isChannelUri && !channelIsBlocked && !claimIsMine && (
{isChannelUri && !banState.muted && !claimIsMine && (
<SubscribeButton
uri={repostedChannelUri || (uri.startsWith('lbry://') ? uri : `lbry://${uri}`)}
/>

View file

@ -11,8 +11,7 @@ import {
} from 'redux/selectors/claims';
import { doFileGet } from 'redux/actions/file';
import { doResolveUri } from 'redux/actions/claims';
import { selectMutedChannels } from 'redux/selectors/blocked';
import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { makeSelectViewCountForUri, selectBanStateForUri } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings';
import ClaimPreviewTile from './view';
@ -31,9 +30,7 @@ const select = (state, props) => {
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
title: props.uri && makeSelectTitleForUri(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectMutedChannels(state),
banState: selectBanStateForUri(state, props.uri),
showMature: selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(props.uri)(state),

View file

@ -12,7 +12,7 @@ import SubscribeButton from 'component/subscribeButton';
import useGetThumbnail from 'effects/use-get-thumbnail';
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import { parseURI, isURIEqual } from 'util/lbryURI';
import { parseURI } from 'util/lbryURI';
import PreviewOverlayProperties from 'component/previewOverlayProperties';
import FileDownloadLink from 'component/fileDownloadLink';
import FileWatchLaterLink from 'component/fileWatchLaterLink';
@ -33,15 +33,7 @@ type Props = {
thumbnail: string,
title: string,
placeholder: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
}>,
filteredOutpoints: Array<{
txid: string,
nout: number,
}>,
blockedChannelUris: Array<string>,
banState: { blacklisted?: boolean, filtered?: boolean, muted?: boolean, blocked?: boolean },
getFile: (string) => void,
streamingUrl: string,
isMature: boolean,
@ -67,11 +59,9 @@ function ClaimPreviewTile(props: Props) {
resolveUri,
claim,
placeholder,
blackListedOutpoints,
filteredOutpoints,
banState,
getFile,
streamingUrl,
blockedChannelUris,
isMature,
showMature,
showHiddenByUser,
@ -145,34 +135,9 @@ function ClaimPreviewTile(props: Props) {
// Unfortunately needed until this is resolved
// https://github.com/lbryio/lbry-sdk/issues/2785
shouldHide = true;
}
// This will be replaced once blocking is done at the wallet server level
if (claim && !shouldHide && blackListedOutpoints) {
shouldHide = blackListedOutpoints.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
}
// We're checking to see if the stream outpoint
// or signing channel outpoint is in the filter list
if (claim && !shouldHide && filteredOutpoints) {
shouldHide = filteredOutpoints.some(
(outpoint) =>
(signingChannel && outpoint.txid === signingChannel.txid && outpoint.nout === signingChannel.nout) ||
(outpoint.txid === claim.txid && outpoint.nout === claim.nout)
);
}
// block stream claims
if (claim && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
}
// block channel claims if we can't control for them in claim search
// e.g. fetchRecommendedSubscriptions
if (claim && isChannel && !shouldHide && !showHiddenByUser && blockedChannelUris.length && signingChannel) {
shouldHide = blockedChannelUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
} else {
shouldHide =
banState.blacklisted || banState.filtered || (!showHiddenByUser && (banState.muted || banState.blocked));
}
if (shouldHide || (isLivestream && !showNoSourceClaims)) {
@ -290,34 +255,4 @@ function ClaimPreviewTile(props: Props) {
);
}
export default React.memo<Props>(withRouter(ClaimPreviewTile), areEqual);
const BLOCKLIST_KEYS = ['blackListedOutpoints', 'filteredOutpoints', 'blockedChannelUris'];
const HANDLED_KEYS = [...BLOCKLIST_KEYS, 'date'];
function areEqual(prev: Props, next: Props) {
for (let i = 0; i < BLOCKLIST_KEYS.length; ++i) {
const key = BLOCKLIST_KEYS[i];
const a = prev[key];
const b = next[key];
if (((!a || !b) && a !== b) || (a && b && a.length !== b.length)) {
// The arrays are huge, so just compare the length instead of each entry.
return false;
}
}
if (Number(prev.date) !== Number(next.date)) {
return false;
}
const propKeys = Object.keys(next);
for (let i = 0; i < propKeys.length; ++i) {
const pk = propKeys[i];
if (!HANDLED_KEYS.includes(pk) && prev[pk] !== next[pk]) {
return false;
}
}
return true;
}
export default withRouter(ClaimPreviewTile);