Skip to content

Commit 4de4efc

Browse files
committed
fix(insights): use web3.js Connection instead of PythConnection
Upon investigation, it seems the `PythConnection` from `@pythnetwork/client` is extremely inefficient for a few reasons: 1. `PythConnection` requires loading _all_ account info when creating the connection class, so that it can build a mapping from price account to program account. However IH never used this mapping in practice, so all of that was wasted work. This caused major issues for page load performance as loading all account info from pythnet was a ton of parsing which locked up the browser for multiple seconds. 2. `PythConnection` did not expose a mechanism to remove unused subscriptions In doing this I also removed all the live prices contexts since none of that is needed any more, as well as the call to initialize prices with the last available price as that too looks unnecessary and redundant. This change should _drastically_ improve performance in IH, especially during page load.
1 parent c1f723c commit 4de4efc

File tree

3 files changed

+38
-178
lines changed

3 files changed

+38
-178
lines changed

apps/insights/src/components/Root/index.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
GOOGLE_ANALYTICS_ID,
1111
} from "../../config/server";
1212
import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
13-
import { LivePriceDataProvider } from "../../hooks/use-live-price-data";
1413
import { Cluster } from "../../services/pyth";
1514
import { getFeeds } from "../../services/pyth/get-feeds";
1615
import { PriceFeedIcon } from "../PriceFeedIcon";
@@ -32,7 +31,7 @@ export const Root = ({ children }: Props) => (
3231
amplitudeApiKey={AMPLITUDE_API_KEY}
3332
googleAnalyticsId={GOOGLE_ANALYTICS_ID}
3433
enableAccessibilityReporting={ENABLE_ACCESSIBILITY_REPORTING}
35-
providers={[NuqsAdapter, LivePriceDataProvider]}
34+
providers={[NuqsAdapter]}
3635
tabs={TABS}
3736
extraCta={<SearchButton />}
3837
>

apps/insights/src/hooks/use-live-price-data.tsx

Lines changed: 17 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,34 @@
33
import type { PriceData } from "@pythnetwork/client";
44
import { useLogger } from "@pythnetwork/component-library/useLogger";
55
import { PublicKey } from "@solana/web3.js";
6-
import type { ComponentProps } from "react";
7-
import {
8-
use,
9-
createContext,
10-
useEffect,
11-
useCallback,
12-
useState,
13-
useMemo,
14-
useRef,
15-
} from "react";
6+
import { useEffect, useState, useMemo } from "react";
167

17-
import {
18-
Cluster,
19-
subscribe,
20-
getAssetPricesFromAccounts,
21-
} from "../services/pyth";
22-
23-
const LivePriceDataContext = createContext<
24-
ReturnType<typeof usePriceData> | undefined
25-
>(undefined);
26-
27-
type LivePriceDataProviderProps = Omit<
28-
ComponentProps<typeof LivePriceDataContext>,
29-
"value"
30-
>;
31-
32-
export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => {
33-
const priceData = usePriceData();
34-
35-
return <LivePriceDataContext value={priceData} {...props} />;
36-
};
8+
import { Cluster, subscribe, unsubscribe } from "../services/pyth";
379

3810
export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
39-
const { addSubscription, removeSubscription } =
40-
useLivePriceDataContext()[cluster];
41-
11+
const logger = useLogger();
4212
const [data, setData] = useState<{
4313
current: PriceData | undefined;
4414
prev: PriceData | undefined;
4515
}>({ current: undefined, prev: undefined });
4616

4717
useEffect(() => {
48-
addSubscription(feedKey, setData);
18+
const subscriptionId = subscribe(
19+
cluster,
20+
new PublicKey(feedKey),
21+
({ data }) => {
22+
setData((prev) => ({ current: data, prev: prev.current }));
23+
},
24+
);
4925
return () => {
50-
removeSubscription(feedKey, setData);
26+
unsubscribe(cluster, subscriptionId).catch((error: unknown) => {
27+
logger.error(
28+
`Failed to remove subscription for price feed ${feedKey}`,
29+
error,
30+
);
31+
});
5132
};
52-
}, [addSubscription, removeSubscription, feedKey]);
33+
}, [cluster, feedKey, logger]);
5334

5435
return data;
5536
};
@@ -75,130 +56,3 @@ export const useLivePriceComponent = (
7556
exponent: current?.exponent,
7657
};
7758
};
78-
79-
const usePriceData = () => {
80-
const pythnetPriceData = usePriceDataForCluster(Cluster.Pythnet);
81-
const pythtestPriceData = usePriceDataForCluster(Cluster.PythtestConformance);
82-
83-
return {
84-
[Cluster.Pythnet]: pythnetPriceData,
85-
[Cluster.PythtestConformance]: pythtestPriceData,
86-
};
87-
};
88-
89-
type Subscription = (value: {
90-
current: PriceData;
91-
prev: PriceData | undefined;
92-
}) => void;
93-
94-
const usePriceDataForCluster = (cluster: Cluster) => {
95-
const [feedKeys, setFeedKeys] = useState<string[]>([]);
96-
const feedSubscriptions = useRef<Map<string, Set<Subscription>>>(new Map());
97-
const priceData = useRef<Map<string, PriceData>>(new Map());
98-
const prevPriceData = useRef<Map<string, PriceData>>(new Map());
99-
const logger = useLogger();
100-
101-
useEffect(() => {
102-
// First, we initialize prices with the last available price. This way, if
103-
// there's any symbol that isn't currently publishing prices (e.g. the
104-
// markets are closed), we will still display the last published price for
105-
// that symbol.
106-
const uninitializedFeedKeys = feedKeys.filter(
107-
(key) => !priceData.current.has(key),
108-
);
109-
if (uninitializedFeedKeys.length > 0) {
110-
getAssetPricesFromAccounts(
111-
cluster,
112-
uninitializedFeedKeys.map((key) => new PublicKey(key)),
113-
)
114-
.then((initialPrices) => {
115-
for (const [i, price] of initialPrices.entries()) {
116-
const key = uninitializedFeedKeys[i];
117-
if (key && !priceData.current.has(key)) {
118-
priceData.current.set(key, price);
119-
}
120-
}
121-
})
122-
.catch((error: unknown) => {
123-
logger.error("Failed to fetch initial prices", error);
124-
});
125-
}
126-
127-
// Then, we create a subscription to update prices live.
128-
const connection = subscribe(
129-
cluster,
130-
feedKeys.map((key) => new PublicKey(key)),
131-
({ price_account }, data) => {
132-
if (price_account) {
133-
const prevData = priceData.current.get(price_account);
134-
if (prevData) {
135-
prevPriceData.current.set(price_account, prevData);
136-
}
137-
priceData.current.set(price_account, data);
138-
for (const subscription of feedSubscriptions.current.get(
139-
price_account,
140-
) ?? []) {
141-
subscription({ current: data, prev: prevData });
142-
}
143-
}
144-
},
145-
);
146-
147-
connection.start().catch((error: unknown) => {
148-
logger.error("Failed to subscribe to prices", error);
149-
});
150-
return () => {
151-
connection.stop().catch((error: unknown) => {
152-
logger.error("Failed to unsubscribe from price updates", error);
153-
});
154-
};
155-
}, [feedKeys, logger, cluster]);
156-
157-
const addSubscription = useCallback(
158-
(key: string, subscription: Subscription) => {
159-
const current = feedSubscriptions.current.get(key);
160-
if (current === undefined) {
161-
feedSubscriptions.current.set(key, new Set([subscription]));
162-
setFeedKeys((prev) => [...new Set([...prev, key])]);
163-
} else {
164-
current.add(subscription);
165-
}
166-
},
167-
[feedSubscriptions],
168-
);
169-
170-
const removeSubscription = useCallback(
171-
(key: string, subscription: Subscription) => {
172-
const current = feedSubscriptions.current.get(key);
173-
if (current) {
174-
if (current.size === 0) {
175-
feedSubscriptions.current.delete(key);
176-
setFeedKeys((prev) => prev.filter((elem) => elem !== key));
177-
} else {
178-
current.delete(subscription);
179-
}
180-
}
181-
},
182-
[feedSubscriptions],
183-
);
184-
185-
return {
186-
addSubscription,
187-
removeSubscription,
188-
};
189-
};
190-
191-
const useLivePriceDataContext = () => {
192-
const prices = use(LivePriceDataContext);
193-
if (prices === undefined) {
194-
throw new LivePriceDataProviderNotInitializedError();
195-
}
196-
return prices;
197-
};
198-
199-
class LivePriceDataProviderNotInitializedError extends Error {
200-
constructor() {
201-
super("This component must be a child of <LivePriceDataProvider>");
202-
this.name = "LivePriceDataProviderNotInitializedError";
203-
}
204-
}

apps/insights/src/services/pyth/index.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1+
import type { PriceData } from "@pythnetwork/client";
12
import {
23
PythHttpClient,
3-
PythConnection,
44
getPythProgramKeyForCluster,
5+
parsePriceData,
56
} from "@pythnetwork/client";
6-
import type { PythPriceCallback } from "@pythnetwork/client/lib/PythConnection";
7+
import type { AccountInfo } from "@solana/web3.js";
78
import { Connection, PublicKey } from "@solana/web3.js";
89

910
import { PYTHNET_RPC, PYTHTEST_CONFORMANCE_RPC } from "../../config/isomorphic";
@@ -67,15 +68,21 @@ export const getAssetPricesFromAccounts = (
6768

6869
export const subscribe = (
6970
cluster: Cluster,
70-
feeds: PublicKey[],
71-
cb: PythPriceCallback,
72-
) => {
73-
const pythConn = new PythConnection(
74-
connections[cluster],
75-
getPythProgramKeyForCluster(ClusterToName[cluster]),
76-
"confirmed",
77-
feeds,
71+
feed: PublicKey,
72+
cb: (values: { accountInfo: AccountInfo<Buffer>; data: PriceData }) => void,
73+
) =>
74+
connections[cluster].onAccountChange(
75+
feed,
76+
(accountInfo, context) => {
77+
cb({
78+
accountInfo,
79+
data: parsePriceData(accountInfo.data, context.slot),
80+
});
81+
},
82+
{
83+
commitment: "confirmed",
84+
},
7885
);
79-
pythConn.onPriceChange(cb);
80-
return pythConn;
81-
};
86+
87+
export const unsubscribe = (cluster: Cluster, subscriptionId: number) =>
88+
connections[cluster].removeAccountChangeListener(subscriptionId);

0 commit comments

Comments
 (0)