ソースを参照

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.
Connor Prussin 3 週間 前
コミット
4de4efc635

+ 1 - 2
apps/insights/src/components/Root/index.tsx

@@ -10,7 +10,6 @@ import {
   GOOGLE_ANALYTICS_ID,
 } from "../../config/server";
 import { getPublishersWithRankings } from "../../get-publishers-with-rankings";
-import { LivePriceDataProvider } from "../../hooks/use-live-price-data";
 import { Cluster } from "../../services/pyth";
 import { getFeeds } from "../../services/pyth/get-feeds";
 import { PriceFeedIcon } from "../PriceFeedIcon";
@@ -32,7 +31,7 @@ export const Root = ({ children }: Props) => (
     amplitudeApiKey={AMPLITUDE_API_KEY}
     googleAnalyticsId={GOOGLE_ANALYTICS_ID}
     enableAccessibilityReporting={ENABLE_ACCESSIBILITY_REPORTING}
-    providers={[NuqsAdapter, LivePriceDataProvider]}
+    providers={[NuqsAdapter]}
     tabs={TABS}
     extraCta={<SearchButton />}
   >

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

@@ -3,53 +3,34 @@
 import type { PriceData } from "@pythnetwork/client";
 import { useLogger } from "@pythnetwork/component-library/useLogger";
 import { PublicKey } from "@solana/web3.js";
-import type { ComponentProps } from "react";
-import {
-  use,
-  createContext,
-  useEffect,
-  useCallback,
-  useState,
-  useMemo,
-  useRef,
-} from "react";
+import { useEffect, useState, useMemo } from "react";
 
-import {
-  Cluster,
-  subscribe,
-  getAssetPricesFromAccounts,
-} from "../services/pyth";
-
-const LivePriceDataContext = createContext<
-  ReturnType<typeof usePriceData> | undefined
->(undefined);
-
-type LivePriceDataProviderProps = Omit<
-  ComponentProps<typeof LivePriceDataContext>,
-  "value"
->;
-
-export const LivePriceDataProvider = (props: LivePriceDataProviderProps) => {
-  const priceData = usePriceData();
-
-  return <LivePriceDataContext value={priceData} {...props} />;
-};
+import { Cluster, subscribe, unsubscribe } from "../services/pyth";
 
 export const useLivePriceData = (cluster: Cluster, feedKey: string) => {
-  const { addSubscription, removeSubscription } =
-    useLivePriceDataContext()[cluster];
-
+  const logger = useLogger();
   const [data, setData] = useState<{
     current: PriceData | undefined;
     prev: PriceData | undefined;
   }>({ current: undefined, prev: undefined });
 
   useEffect(() => {
-    addSubscription(feedKey, setData);
+    const subscriptionId = subscribe(
+      cluster,
+      new PublicKey(feedKey),
+      ({ data }) => {
+        setData((prev) => ({ current: data, prev: prev.current }));
+      },
+    );
     return () => {
-      removeSubscription(feedKey, setData);
+      unsubscribe(cluster, subscriptionId).catch((error: unknown) => {
+        logger.error(
+          `Failed to remove subscription for price feed ${feedKey}`,
+          error,
+        );
+      });
     };
-  }, [addSubscription, removeSubscription, feedKey]);
+  }, [cluster, feedKey, logger]);
 
   return data;
 };
@@ -75,130 +56,3 @@ export const useLivePriceComponent = (
     exponent: current?.exponent,
   };
 };
-
-const usePriceData = () => {
-  const pythnetPriceData = usePriceDataForCluster(Cluster.Pythnet);
-  const pythtestPriceData = usePriceDataForCluster(Cluster.PythtestConformance);
-
-  return {
-    [Cluster.Pythnet]: pythnetPriceData,
-    [Cluster.PythtestConformance]: pythtestPriceData,
-  };
-};
-
-type Subscription = (value: {
-  current: PriceData;
-  prev: PriceData | undefined;
-}) => void;
-
-const usePriceDataForCluster = (cluster: Cluster) => {
-  const [feedKeys, setFeedKeys] = useState<string[]>([]);
-  const feedSubscriptions = useRef<Map<string, Set<Subscription>>>(new Map());
-  const priceData = useRef<Map<string, PriceData>>(new Map());
-  const prevPriceData = useRef<Map<string, PriceData>>(new Map());
-  const logger = useLogger();
-
-  useEffect(() => {
-    // First, we initialize prices with the last available price.  This way, if
-    // there's any symbol that isn't currently publishing prices (e.g. the
-    // markets are closed), we will still display the last published price for
-    // that symbol.
-    const uninitializedFeedKeys = feedKeys.filter(
-      (key) => !priceData.current.has(key),
-    );
-    if (uninitializedFeedKeys.length > 0) {
-      getAssetPricesFromAccounts(
-        cluster,
-        uninitializedFeedKeys.map((key) => new PublicKey(key)),
-      )
-        .then((initialPrices) => {
-          for (const [i, price] of initialPrices.entries()) {
-            const key = uninitializedFeedKeys[i];
-            if (key && !priceData.current.has(key)) {
-              priceData.current.set(key, price);
-            }
-          }
-        })
-        .catch((error: unknown) => {
-          logger.error("Failed to fetch initial prices", error);
-        });
-    }
-
-    // Then, we create a subscription to update prices live.
-    const connection = subscribe(
-      cluster,
-      feedKeys.map((key) => new PublicKey(key)),
-      ({ price_account }, data) => {
-        if (price_account) {
-          const prevData = priceData.current.get(price_account);
-          if (prevData) {
-            prevPriceData.current.set(price_account, prevData);
-          }
-          priceData.current.set(price_account, data);
-          for (const subscription of feedSubscriptions.current.get(
-            price_account,
-          ) ?? []) {
-            subscription({ current: data, prev: prevData });
-          }
-        }
-      },
-    );
-
-    connection.start().catch((error: unknown) => {
-      logger.error("Failed to subscribe to prices", error);
-    });
-    return () => {
-      connection.stop().catch((error: unknown) => {
-        logger.error("Failed to unsubscribe from price updates", error);
-      });
-    };
-  }, [feedKeys, logger, cluster]);
-
-  const addSubscription = useCallback(
-    (key: string, subscription: Subscription) => {
-      const current = feedSubscriptions.current.get(key);
-      if (current === undefined) {
-        feedSubscriptions.current.set(key, new Set([subscription]));
-        setFeedKeys((prev) => [...new Set([...prev, key])]);
-      } else {
-        current.add(subscription);
-      }
-    },
-    [feedSubscriptions],
-  );
-
-  const removeSubscription = useCallback(
-    (key: string, subscription: Subscription) => {
-      const current = feedSubscriptions.current.get(key);
-      if (current) {
-        if (current.size === 0) {
-          feedSubscriptions.current.delete(key);
-          setFeedKeys((prev) => prev.filter((elem) => elem !== key));
-        } else {
-          current.delete(subscription);
-        }
-      }
-    },
-    [feedSubscriptions],
-  );
-
-  return {
-    addSubscription,
-    removeSubscription,
-  };
-};
-
-const useLivePriceDataContext = () => {
-  const prices = use(LivePriceDataContext);
-  if (prices === undefined) {
-    throw new LivePriceDataProviderNotInitializedError();
-  }
-  return prices;
-};
-
-class LivePriceDataProviderNotInitializedError extends Error {
-  constructor() {
-    super("This component must be a child of <LivePriceDataProvider>");
-    this.name = "LivePriceDataProviderNotInitializedError";
-  }
-}

+ 20 - 13
apps/insights/src/services/pyth/index.ts

@@ -1,9 +1,10 @@
+import type { PriceData } from "@pythnetwork/client";
 import {
   PythHttpClient,
-  PythConnection,
   getPythProgramKeyForCluster,
+  parsePriceData,
 } from "@pythnetwork/client";
-import type { PythPriceCallback } from "@pythnetwork/client/lib/PythConnection";
+import type { AccountInfo } from "@solana/web3.js";
 import { Connection, PublicKey } from "@solana/web3.js";
 
 import { PYTHNET_RPC, PYTHTEST_CONFORMANCE_RPC } from "../../config/isomorphic";
@@ -67,15 +68,21 @@ export const getAssetPricesFromAccounts = (
 
 export const subscribe = (
   cluster: Cluster,
-  feeds: PublicKey[],
-  cb: PythPriceCallback,
-) => {
-  const pythConn = new PythConnection(
-    connections[cluster],
-    getPythProgramKeyForCluster(ClusterToName[cluster]),
-    "confirmed",
-    feeds,
+  feed: PublicKey,
+  cb: (values: { accountInfo: AccountInfo<Buffer>; data: PriceData }) => void,
+) =>
+  connections[cluster].onAccountChange(
+    feed,
+    (accountInfo, context) => {
+      cb({
+        accountInfo,
+        data: parsePriceData(accountInfo.data, context.slot),
+      });
+    },
+    {
+      commitment: "confirmed",
+    },
   );
-  pythConn.onPriceChange(cb);
-  return pythConn;
-};
+
+export const unsubscribe = (cluster: Cluster, subscriptionId: number) =>
+  connections[cluster].removeAccountChangeListener(subscriptionId);