fix(trading): performance issues with many orders (#3944)

Co-authored-by: Bartłomiej Głownia <bglownia@gmail.com>
This commit is contained in:
Matthew Russell 2023-05-31 08:38:49 -07:00 committed by GitHub
parent e8ff1b0b60
commit f2c70e6da4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 96 additions and 111 deletions

View File

@ -59,7 +59,7 @@ const cacheConfig: InMemoryCacheConfig = {
keyFields: false,
},
TradableInstrument: {
keyFields: ['instrument'],
keyFields: false,
},
Product: {
keyFields: ['settlementAsset', ['id']],
@ -95,5 +95,10 @@ const cacheConfig: InMemoryCacheConfig = {
Fees: {
keyFields: false,
},
// Don't cache order update as this subscription result gets merged into the main order cache
// We don't need to write these to the cache at all
OrderUpdate: {
keyFields: false,
},
},
};

View File

@ -36,7 +36,7 @@ import { AppLoader, DynamicLoader } from '../components/app-loader';
import { Navbar } from '../components/navbar';
import { ENV } from '../lib/config';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { activeOrdersProvider, allOrdersProvider } from '@vegaprotocol/orders';
import { activeOrdersProvider } from '@vegaprotocol/orders';
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
import {
ProtocolUpgradeCountdownMode,
@ -151,11 +151,6 @@ const PartyData = () => {
variables,
skip,
});
useDataProvider({
dataProvider: allOrdersProvider,
variables,
skip,
});
return null;
};

View File

@ -235,6 +235,7 @@ function makeDataProviderInternal<
// subscription is started before initial query, all deltas that will arrive before initial query response are put on queue
const updateQueue: Delta[] = [];
let initialized = false;
let resetTimer: ReturnType<typeof setTimeout>;
let variables: Variables;
let data: Data | null = null;
@ -480,12 +481,10 @@ function makeDataProviderInternal<
};
const initialize = async () => {
if (subscription) {
if (resetTimer) {
clearTimeout(resetTimer);
}
if (initialized) {
return;
}
initialized = true;
loading = true;
error = undefined;
notifyAll();
@ -499,15 +498,12 @@ function makeDataProviderInternal<
};
const reset = () => {
if (!subscription) {
return;
}
subscriptionUnsubscribe();
initialized = false;
data = null;
error = undefined;
loading = false;
loaded = false;
notifyAll();
};
// remove callback from list, and unsubscribe if there is no more callbacks registered
@ -524,13 +520,14 @@ function makeDataProviderInternal<
return (callback, c, v) => {
callbacks.push(callback);
if (callbacks.length === 1) {
if (!initialized) {
client = c;
if (v) {
variables = v;
}
initialize();
} else {
clearTimeout(resetTimer);
notify(callback);
}
return {

View File

@ -170,6 +170,7 @@ export const DealTicket = ({
marginAccountBalance || generalAccountBalance ? balance : undefined,
},
skip: !normalizedOrder,
fetchPolicy: 'no-cache',
});
const assetSymbol =

View File

@ -34,6 +34,7 @@ export const useEstimateFees = (
type: order.type,
},
skip: !pubKey || !order?.size || !order?.price,
fetchPolicy: 'no-cache',
});
return data?.estimateFees;
};

View File

@ -1,4 +1,3 @@
import produce from 'immer';
import orderBy from 'lodash/orderBy';
import uniqBy from 'lodash/uniqBy';
import {
@ -6,9 +5,9 @@ import {
makeDerivedDataProvider,
defaultAppend as append,
} from '@vegaprotocol/data-provider';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import type { Market } from '@vegaprotocol/markets';
import { marketsProvider } from '@vegaprotocol/markets';
import type { PageInfo, Edge } from '@vegaprotocol/data-provider';
import { OrderStatus } from '@vegaprotocol/types';
import type {
OrderFieldsFragment,
@ -80,6 +79,28 @@ const orderMatchFilters = (
return true;
};
const mapOrderUpdateToOrder = (
orderUpdate: OrderUpdateFieldsFragment
): OrderFieldsFragment => {
const { marketId, liquidityProvisionId, ...order } = orderUpdate;
// If there is a liquidity provision id add the object to the resulting order
const liquidityProvision: OrderFieldsFragment['liquidityProvision'] | null =
liquidityProvisionId
? {
__typename: 'LiquidityProvision',
}
: null;
return {
...order,
liquidityProvision: liquidityProvision,
market: {
__typename: 'Market',
id: marketId,
},
__typename: 'Order',
};
};
const getData = (
responseData: OrdersQuery | null
): Edge<OrderFieldsFragment>[] =>
@ -93,27 +114,9 @@ const getDelta = (
if (!subscriptionData.orders) {
return [];
}
subscriptionData.orders.forEach((order) => {
client.cache.modify({
id: client.cache.identify({
__typename: 'Order',
id: order.id,
}),
fields: {
price: () => order.price,
size: () => order.size,
remaining: () => order.remaining,
updatedAt: () => order.updatedAt,
status: () => order.status,
},
});
});
return subscriptionData.orders;
};
const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
responseData.party?.ordersConnection?.pageInfo || null;
export const update = (
data: ReturnType<typeof getData> | null,
delta: ReturnType<typeof getDelta>,
@ -132,49 +135,37 @@ export const update = (
),
'createdAt'
);
return produce(data, (draft) => {
// Add or update incoming orders
incoming.forEach((node) => {
const index = draft.findIndex((edge) => edge.node.id === node.id);
const newer =
draft.length === 0 || node.createdAt >= draft[0].node.createdAt;
const doesFilterPass = !variables || orderMatchFilters(node, variables);
if (index !== -1) {
if (doesFilterPass) {
Object.assign(draft[index].node, node);
} else {
draft.splice(index, 1);
}
} else if (newer && doesFilterPass) {
const { marketId, liquidityProvisionId, ...order } = node;
// If there is a liquidity provision id add the object to the resulting order
const liquidityProvision:
| OrderFieldsFragment['liquidityProvision']
| null = liquidityProvisionId
? {
__typename: 'LiquidityProvision',
}
: null;
draft.unshift({
node: {
...order,
liquidityProvision: liquidityProvision,
market: {
__typename: 'Market',
id: marketId,
},
__typename: 'Order',
},
cursor: '',
});
const updatedData = [...data];
incoming.forEach((orderUpdate) => {
const index = data.findIndex((edge) => edge.node.id === orderUpdate.id);
const newer =
data.length === 0 || orderUpdate.createdAt >= data[0].node.createdAt;
const doesFilterPass =
!variables || orderMatchFilters(orderUpdate, variables);
if (index !== -1) {
if (doesFilterPass) {
updatedData[index] = {
...updatedData[index],
node: mapOrderUpdateToOrder(orderUpdate),
};
} else {
updatedData.splice(index, 1);
}
});
} else if (newer && doesFilterPass) {
updatedData.unshift({
node: mapOrderUpdateToOrder(orderUpdate),
cursor: '',
});
}
});
return updatedData;
};
const ordersProvider = makeDataProvider<
const getPageInfo = (responseData: OrdersQuery): PageInfo | null =>
responseData.party?.ordersConnection?.pageInfo || null;
export const ordersProvider = makeDataProvider<
OrdersQuery,
ReturnType<typeof getData>,
OrdersUpdateSubscription,
@ -189,39 +180,13 @@ const ordersProvider = makeDataProvider<
pagination: {
getPageInfo,
append,
first: 1000,
first: 5000,
},
resetDelay: 1000,
additionalContext: { isEnlargedTimeout: true },
fetchPolicy: 'no-cache',
});
const allOrderMaxCount = 50000;
export const allOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,
{ partyId: string; marketId?: string }
>(
[
(callback, client, variables) =>
ordersProvider(callback, client, { partyId: variables.partyId }),
],
(partsData, variables, prevData, parts, subscriptions) => {
const orders = partsData[0] as ReturnType<typeof getData>;
// load next pages until allOrderMaxCount reached
if (
!parts[0].isUpdate &&
subscriptions &&
subscriptions[0].load &&
orders?.length < allOrderMaxCount
) {
subscriptions[0].load();
}
return variables.marketId
? orders.filter((edge) => variables.marketId === edge.node.market.id)
: orders;
}
);
export const activeOrdersProvider = makeDerivedDataProvider<
ReturnType<typeof getData>,
never,

View File

@ -76,12 +76,21 @@ export const OrderListManager = ({
const [editOrder, setEditOrder] = useState<Order | null>(null);
const create = useVegaTransactionStore((state) => state.create);
const hasAmendableOrder = useHasAmendableOrder(marketId);
const variables =
filter === Filter.Open
? { partyId, filter: { liveOnly: true } }
: { partyId };
const { data, error, loading, reload } = useDataProvider({
dataProvider: ordersWithMarketProvider,
variables:
filter === Filter.Open
? { partyId, filter: { liveOnly: true } }
: { partyId },
variables,
update: ({ data }) => {
if (data && gridRef.current?.api) {
gridRef.current.api.setRowData(data);
return true;
}
return true;
},
});
const {
@ -127,8 +136,13 @@ export const OrderListManager = ({
[bottomPlaceholderOnFilterChanged]
);
const onRowDataChanged = useCallback(() => {
const rowCount = gridRef.current?.api?.getModel().getRowCount();
setHasData((rowCount ?? 0) > 0);
}, []);
useEffect(() => {
setHasData((gridRef.current?.api?.getModel().getRowCount() ?? 0) > 0);
setHasData(Boolean(data?.length));
}, [data]);
const cancelAll = useCallback(() => {
@ -150,8 +164,8 @@ export const OrderListManager = ({
onMarketClick={onMarketClick}
onOrderTypeClick={onOrderTypeClick}
onFilterChanged={onFilterChanged}
onRowDataChanged={onRowDataChanged}
isReadOnly={isReadOnly}
blockLoadDebounceMillis={100}
storeKey={storeKey}
suppressLoadingOverlay
suppressNoRowsOverlay

View File

@ -49,8 +49,11 @@ export const OrderListTable = memo<
{ onCancel, onEdit, onMarketClick, onOrderTypeClick, filter, ...props },
ref
) => {
const showAllActions =
filter === undefined || filter === Filter.Open ? true : false;
const showAllActions = props.isReadOnly
? false
: filter === undefined || filter === Filter.Open
? true
: false;
return (
<AgGrid

View File

@ -25,6 +25,7 @@ export const useVegaTransactionUpdater = () => {
useOrderTxUpdateSubscription({
variables,
skip,
fetchPolicy: 'no-cache',
onData: ({ data: result }) =>
result.data?.orders?.forEach((order) => {
updateOrder(order);
@ -34,6 +35,7 @@ export const useVegaTransactionUpdater = () => {
useWithdrawalBusEventSubscription({
variables,
skip,
fetchPolicy: 'no-cache',
onData: ({ data: result }) =>
result.data?.busEvents?.forEach((event) => {
if (event.event.__typename === 'Withdrawal') {
@ -48,6 +50,7 @@ export const useVegaTransactionUpdater = () => {
useTransactionEventSubscription({
variables,
skip,
fetchPolicy: 'no-cache',
onData: ({ data: result }) =>
result.data?.busEvents?.forEach((event) => {
if (event.event.__typename === 'TransactionResult') {

View File

@ -174,6 +174,7 @@ const EditOrderDetails = ({
}) => {
const { data: orderById } = useOrderByIdQuery({
variables: { orderId: data.orderId },
fetchPolicy: 'no-cache',
});
const { data: markets } = useMarketList();