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, // selectFetchingTrendingUris,
// } from './redux/selectors/homepage'; // } from './redux/selectors/homepage';
export { selectViewCount, makeSelectViewCountForUri, makeSelectSubCountForUri } from './redux/selectors/stats'; export { selectViewCount, makeSelectViewCountForUri, makeSelectSubCountForUri } from './redux/selectors/stats';
export { selectBanStateForUri } from './redux/selectors/ban';
export { export {
selectHasSyncedWallet, selectHasSyncedWallet,
selectSyncData, 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 { doResolveUri } from 'redux/actions/claims';
import { doCollectionEdit } from 'redux/actions/collections'; import { doCollectionEdit } from 'redux/actions/collections';
import { doFileGet } from 'redux/actions/file'; import { doFileGet } from 'redux/actions/file';
import { selectMutedChannels, makeSelectChannelIsMuted } from 'redux/selectors/blocked'; import { selectBanStateForUri } from 'lbryinc';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content'; import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { selectModerationBlockList } from 'redux/selectors/comments';
import ClaimPreview from './view'; import ClaimPreview from './view';
import formatMediaDuration from 'util/formatMediaDuration'; import formatMediaDuration from 'util/formatMediaDuration';
@ -48,12 +46,8 @@ const select = (state, props) => {
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state), isResolvingRepost: props.uri && makeSelectIsUriResolving(props.repostUrl)(state),
nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state), nsfw: props.uri && makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), banState: selectBanStateForUri(state, props.uri),
filteredOutpoints: selectFilteredOutpoints(state),
mutedUris: selectMutedChannels(state),
blockedUris: selectModerationBlockList(state),
hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state), hasVisitedUri: props.uri && makeSelectHasVisitedUri(props.uri)(state),
channelIsBlocked: props.uri && makeSelectChannelIsMuted(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state), streamingUrl: props.uri && makeSelectStreamingUrlForUri(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(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 { isEmpty } from 'util/object';
import { lazyImport } from 'util/lazyImport'; import { lazyImport } from 'util/lazyImport';
import classnames from 'classnames'; import classnames from 'classnames';
import { isURIEqual, isURIValid } from 'util/lbryURI'; import { isURIValid } from 'util/lbryURI';
import * as COLLECTIONS_CONSTS from 'constants/collections'; import * as COLLECTIONS_CONSTS from 'constants/collections';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
@ -52,18 +52,9 @@ type Props = {
nsfw: boolean, nsfw: boolean,
placeholder: string, placeholder: string,
type: string, type: string,
banState: { blacklisted?: boolean, filtered?: boolean, muted?: boolean, blocked?: boolean },
hasVisitedUri: boolean, hasVisitedUri: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
}>,
filteredOutpoints: Array<{
txid: string,
nout: number,
}>,
mutedUris: Array<string>,
blockedUris: Array<string>, blockedUris: Array<string>,
channelIsBlocked: boolean,
actions: boolean | Node | string | number, actions: boolean | Node | string | number,
properties: boolean | Node | string | number | ((Claim) => Node), properties: boolean | Node | string | number | ((Claim) => Node),
empty?: Node, empty?: Node,
@ -113,7 +104,6 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
streamingUrl, streamingUrl,
mediaDuration, mediaDuration,
// user properties // user properties
channelIsBlocked,
hasVisitedUri, hasVisitedUri,
// component // component
history, history,
@ -139,10 +129,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
properties, properties,
onClick, onClick,
actions, actions,
mutedUris, banState,
blockedUris,
blackListedOutpoints,
filteredOutpoints,
includeSupportAction, includeSupportAction,
renderActions, renderActions,
hideMenu = false, hideMenu = false,
@ -246,28 +233,13 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
((abandoned && !showUnresolvedClaim) || (!claimIsMine && obscureNsfw && nsfw)); ((abandoned && !showUnresolvedClaim) || (!claimIsMine && obscureNsfw && nsfw));
// This will be replaced once blocking is done at the wallet server level // This will be replaced once blocking is done at the wallet server level
if (claim && !claimIsMine && !shouldHide && blackListedOutpoints) { if (!shouldHide && !claimIsMine && (banState.blacklisted || banState.filtered)) {
shouldHide = blackListedOutpoints.some( shouldHide = true;
(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)
);
} }
// block stream claims // block stream claims
if (claim && !shouldHide && !showUserBlocked && mutedUris.length && signingChannel) { if (!shouldHide && !showUserBlocked && (banState.muted || banState.blocked)) {
shouldHide = mutedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url)); shouldHide = true;
}
if (claim && !shouldHide && !showUserBlocked && blockedUris.length && signingChannel) {
shouldHide = blockedUris.some((blockedUri) => isURIEqual(blockedUri, signingChannel.permanent_url));
} }
if (!shouldHide && customShouldHide && claim) { if (!shouldHide && customShouldHide && claim) {
@ -482,7 +454,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
</div> </div>
)} )}
{isChannelUri && !channelIsBlocked && !claimIsMine && ( {isChannelUri && !banState.muted && !claimIsMine && (
<SubscribeButton <SubscribeButton
uri={repostedChannelUri || (uri.startsWith('lbry://') ? uri : `lbry://${uri}`)} uri={repostedChannelUri || (uri.startsWith('lbry://') ? uri : `lbry://${uri}`)}
/> />

View file

@ -11,8 +11,7 @@ import {
} from 'redux/selectors/claims'; } from 'redux/selectors/claims';
import { doFileGet } from 'redux/actions/file'; import { doFileGet } from 'redux/actions/file';
import { doResolveUri } from 'redux/actions/claims'; import { doResolveUri } from 'redux/actions/claims';
import { selectMutedChannels } from 'redux/selectors/blocked'; import { makeSelectViewCountForUri, selectBanStateForUri } from 'lbryinc';
import { makeSelectViewCountForUri, selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream'; import { makeSelectIsActiveLivestream } from 'redux/selectors/livestream';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
import ClaimPreviewTile from './view'; import ClaimPreviewTile from './view';
@ -31,9 +30,7 @@ const select = (state, props) => {
isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state), isResolvingUri: props.uri && makeSelectIsUriResolving(props.uri)(state),
thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state), thumbnail: props.uri && makeSelectThumbnailForUri(props.uri)(state),
title: props.uri && makeSelectTitleForUri(props.uri)(state), title: props.uri && makeSelectTitleForUri(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state), banState: selectBanStateForUri(state, props.uri),
filteredOutpoints: selectFilteredOutpoints(state),
blockedChannelUris: selectMutedChannels(state),
showMature: selectShowMatureContent(state), showMature: selectShowMatureContent(state),
isMature: makeSelectClaimIsNsfw(props.uri)(state), isMature: makeSelectClaimIsNsfw(props.uri)(state),
isLivestream: makeSelectClaimIsStreamPlaceholder(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 useGetThumbnail from 'effects/use-get-thumbnail';
import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url'; import { formatLbryUrlForWeb, generateListSearchUrlParams } from 'util/url';
import { formatClaimPreviewTitle } from 'util/formatAriaLabel'; import { formatClaimPreviewTitle } from 'util/formatAriaLabel';
import { parseURI, isURIEqual } from 'util/lbryURI'; import { parseURI } from 'util/lbryURI';
import PreviewOverlayProperties from 'component/previewOverlayProperties'; import PreviewOverlayProperties from 'component/previewOverlayProperties';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import FileWatchLaterLink from 'component/fileWatchLaterLink'; import FileWatchLaterLink from 'component/fileWatchLaterLink';
@ -33,15 +33,7 @@ type Props = {
thumbnail: string, thumbnail: string,
title: string, title: string,
placeholder: boolean, placeholder: boolean,
blackListedOutpoints: Array<{ banState: { blacklisted?: boolean, filtered?: boolean, muted?: boolean, blocked?: boolean },
txid: string,
nout: number,
}>,
filteredOutpoints: Array<{
txid: string,
nout: number,
}>,
blockedChannelUris: Array<string>,
getFile: (string) => void, getFile: (string) => void,
streamingUrl: string, streamingUrl: string,
isMature: boolean, isMature: boolean,
@ -67,11 +59,9 @@ function ClaimPreviewTile(props: Props) {
resolveUri, resolveUri,
claim, claim,
placeholder, placeholder,
blackListedOutpoints, banState,
filteredOutpoints,
getFile, getFile,
streamingUrl, streamingUrl,
blockedChannelUris,
isMature, isMature,
showMature, showMature,
showHiddenByUser, showHiddenByUser,
@ -145,34 +135,9 @@ function ClaimPreviewTile(props: Props) {
// Unfortunately needed until this is resolved // Unfortunately needed until this is resolved
// https://github.com/lbryio/lbry-sdk/issues/2785 // https://github.com/lbryio/lbry-sdk/issues/2785
shouldHide = true; shouldHide = true;
} } else {
shouldHide =
// This will be replaced once blocking is done at the wallet server level banState.blacklisted || banState.filtered || (!showHiddenByUser && (banState.muted || banState.blocked));
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));
} }
if (shouldHide || (isLivestream && !showNoSourceClaims)) { if (shouldHide || (isLivestream && !showNoSourceClaims)) {
@ -290,34 +255,4 @@ function ClaimPreviewTile(props: Props) {
); );
} }
export default React.memo<Props>(withRouter(ClaimPreviewTile), areEqual); export default withRouter(ClaimPreviewTile);
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;
}