-
-
Notifications
You must be signed in to change notification settings - Fork 277
feat(AssetsController): Measure intial fetch duration #7871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,8 @@ import type { | |
| DataType, | ||
| DataRequest, | ||
| DataResponse, | ||
| FetchContext, | ||
| FetchNextFunction, | ||
| NextFunction, | ||
| Middleware, | ||
| SubscriptionResponse, | ||
|
|
@@ -202,6 +204,23 @@ export type AssetsControllerMessenger = Messenger< | |
| // CONTROLLER OPTIONS | ||
| // ============================================================================ | ||
|
|
||
| /** | ||
| * Payload for the first init/fetch MetaMetrics event. | ||
| * Passed to the optional trackMetaMetricsEvent callback when the initial | ||
| * asset fetch completes after unlock or app open. | ||
| */ | ||
| export type AssetsControllerFirstInitFetchMetaMetricsPayload = { | ||
| /** Duration of the first init fetch in milliseconds (wall-clock). */ | ||
| durationMs: number; | ||
| /** Chain IDs requested in the fetch (e.g. ['eip155:1', 'eip155:137']). */ | ||
| chainIds: string[]; | ||
| /** | ||
| * Exclusive latency in ms per data source (time spent in that source only). | ||
| * Sum of values approximates durationMs. Order: same as middleware chain. | ||
| */ | ||
| durationByDataSource: Record<string, number>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and i guess this is duration per data source
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes |
||
| }; | ||
|
|
||
| export type AssetsControllerOptions = { | ||
| messenger: AssetsControllerMessenger; | ||
| state?: Partial<AssetsControllerState>; | ||
|
|
@@ -235,6 +254,13 @@ export type AssetsControllerOptions = { | |
| queryApiClient: ApiPlatformClient; | ||
| /** Optional configuration for RpcDataSource. */ | ||
| rpcDataSourceConfig?: RpcDataSourceConfig; | ||
| /** | ||
| * Optional callback invoked when the first init/fetch completes (e.g. after unlock). | ||
| * Use this to track first init fetch duration in MetaMetrics. | ||
| */ | ||
| trackMetaMetricsEvent?: ( | ||
| payload: AssetsControllerFirstInitFetchMetaMetricsPayload, | ||
| ) => void; | ||
| /** Optional configuration for AccountsApiDataSource. */ | ||
| accountsApiDataSourceConfig?: AccountsApiDataSourceConfig; | ||
| /** Optional configuration for PriceDataSource. */ | ||
|
|
@@ -390,6 +416,14 @@ export class AssetsController extends BaseController< | |
| /** Default update interval hint passed to data sources */ | ||
| readonly #defaultUpdateInterval: number; | ||
|
|
||
| /** Optional callback for first init/fetch MetaMetrics (duration). */ | ||
| readonly #trackMetaMetricsEvent?: ( | ||
| payload: AssetsControllerFirstInitFetchMetaMetricsPayload, | ||
| ) => void; | ||
|
|
||
| /** Whether we have already reported first init fetch for this session (reset on #stop). */ | ||
| #firstInitFetchReported = false; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what this means ? is this reset after each open/close of the app or when the background stop and restart ?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes |
||
|
|
||
| readonly #controllerMutex = new Mutex(); | ||
|
|
||
| /** | ||
|
|
@@ -461,6 +495,7 @@ export class AssetsController extends BaseController< | |
| subscribeToBasicFunctionalityChange, | ||
| queryApiClient, | ||
| rpcDataSourceConfig, | ||
| trackMetaMetricsEvent, | ||
| accountsApiDataSourceConfig, | ||
| priceDataSourceConfig, | ||
| }: AssetsControllerOptions) { | ||
|
|
@@ -477,7 +512,7 @@ export class AssetsController extends BaseController< | |
| this.#isEnabled = isEnabled(); | ||
| this.#isBasicFunctionality = isBasicFunctionality ?? ((): boolean => true); | ||
| this.#defaultUpdateInterval = defaultUpdateInterval; | ||
|
|
||
| this.#trackMetaMetricsEvent = trackMetaMetricsEvent; | ||
| const rpcConfig = rpcDataSourceConfig ?? {}; | ||
|
|
||
| const onActiveChainsUpdated = ( | ||
|
|
@@ -701,18 +736,44 @@ export class AssetsController extends BaseController< | |
|
|
||
| /** | ||
| * Execute middlewares with request/response context. | ||
| * Returns response and exclusive duration per source (sum ≈ wall time). | ||
| * | ||
| * @param middlewares - Middlewares to execute in order. | ||
| * @param sources - Data sources or middlewares with getName() and assetsMiddleware (executed in order). | ||
| * @param request - The data request. | ||
| * @param initialResponse - Optional initial response (for enriching existing data). | ||
| * @returns The final DataResponse after all middlewares have processed. | ||
| * @returns Response and durationByDataSource (exclusive ms per source name). | ||
| */ | ||
| async #executeMiddlewares( | ||
| middlewares: Middleware[], | ||
| sources: { getName(): string; assetsMiddleware: Middleware }[], | ||
| request: DataRequest, | ||
| initialResponse: DataResponse = {}, | ||
| ): Promise<DataResponse> { | ||
| const chain = middlewares.reduceRight<NextFunction>( | ||
| ): Promise<{ | ||
| response: DataResponse; | ||
| durationByDataSource: Record<string, number>; | ||
| }> { | ||
| const names = sources.map((source) => source.getName()); | ||
| const middlewares = sources.map((source) => source.assetsMiddleware); | ||
| const inclusive: number[] = []; | ||
| const wrapped = middlewares.map( | ||
| (middleware, i) => | ||
| (async ( | ||
| ctx: FetchContext, | ||
| next: FetchNextFunction, | ||
| ): Promise<{ | ||
| request: DataRequest; | ||
| response: DataResponse; | ||
| getAssetsState: () => AssetsControllerStateInternal; | ||
| }> => { | ||
| const start = Date.now(); | ||
| try { | ||
| return await middleware(ctx, next); | ||
| } finally { | ||
| inclusive[i] = Date.now() - start; | ||
| } | ||
| }) as Middleware, | ||
| ); | ||
|
|
||
| const chain = wrapped.reduceRight<NextFunction>( | ||
| (next, middleware) => | ||
| async ( | ||
| ctx, | ||
|
|
@@ -736,7 +797,17 @@ export class AssetsController extends BaseController< | |
| response: initialResponse, | ||
| getAssetsState: () => this.state as AssetsControllerStateInternal, | ||
| }); | ||
| return result.response; | ||
|
|
||
| const durationByDataSource: Record<string, number> = {}; | ||
| for (let i = 0; i < inclusive.length; i++) { | ||
| const nextInc = i + 1 < inclusive.length ? (inclusive[i + 1] ?? 0) : 0; | ||
| const exclusive = Math.max(0, (inclusive[i] ?? 0) - nextInc); | ||
| const name = names[i]; | ||
| if (name !== undefined) { | ||
| durationByDataSource[name] = exclusive; | ||
| } | ||
| } | ||
| return { response: result.response, durationByDataSource }; | ||
| } | ||
|
|
||
| // ============================================================================ | ||
|
|
@@ -764,27 +835,37 @@ export class AssetsController extends BaseController< | |
| } | ||
|
|
||
| if (options?.forceUpdate) { | ||
| const startTime = Date.now(); | ||
| const request = this.#buildDataRequest(accounts, chainIds, { | ||
| assetTypes, | ||
| dataTypes, | ||
| customAssets: customAssets.length > 0 ? customAssets : undefined, | ||
| forceUpdate: true, | ||
| }); | ||
| const middlewares = this.#isBasicFunctionality() | ||
| const sources = this.#isBasicFunctionality() | ||
| ? [ | ||
| this.#accountsApiDataSource.assetsMiddleware, | ||
| this.#snapDataSource.assetsMiddleware, | ||
| this.#rpcDataSource.assetsMiddleware, | ||
| this.#detectionMiddleware.assetsMiddleware, | ||
| this.#tokenDataSource.assetsMiddleware, | ||
| this.#priceDataSource.assetsMiddleware, | ||
| this.#accountsApiDataSource, | ||
| this.#snapDataSource, | ||
| this.#rpcDataSource, | ||
| this.#detectionMiddleware, | ||
| this.#tokenDataSource, | ||
| this.#priceDataSource, | ||
| ] | ||
| : [ | ||
| this.#rpcDataSource.assetsMiddleware, | ||
| this.#detectionMiddleware.assetsMiddleware, | ||
| ]; | ||
| const response = await this.#executeMiddlewares(middlewares, request); | ||
| : [this.#rpcDataSource, this.#detectionMiddleware]; | ||
| const { response, durationByDataSource } = await this.#executeMiddlewares( | ||
| sources, | ||
| request, | ||
| ); | ||
| await this.#updateState(response); | ||
| if (this.#trackMetaMetricsEvent && !this.#firstInitFetchReported) { | ||
| this.#firstInitFetchReported = true; | ||
| const durationMs = Date.now() - startTime; | ||
| this.#trackMetaMetricsEvent({ | ||
| durationMs, | ||
| chainIds, | ||
| durationByDataSource, | ||
| }); | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Stale fetch can misreport session metricMedium Severity
Additional Locations (1) |
||
| } | ||
| } | ||
|
|
||
| return this.#getAssetsFromState(accounts, chainIds, assetTypes); | ||
|
|
@@ -1335,14 +1416,12 @@ export class AssetsController extends BaseController< | |
| }); | ||
|
|
||
| this.#subscribeAssets(); | ||
| if (this.#selectedAccounts.length > 0) { | ||
| this.getAssets(this.#selectedAccounts, { | ||
| chainIds: [...this.#enabledChains], | ||
| forceUpdate: true, | ||
| }).catch((error) => { | ||
| log('Failed to fetch assets', error); | ||
| }); | ||
| } | ||
| this.getAssets(this.#selectedAccounts, { | ||
| chainIds: [...this.#enabledChains], | ||
| forceUpdate: true, | ||
| }).catch((error) => { | ||
Kriys94 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| log('Failed to fetch assets', error); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -1355,6 +1434,8 @@ export class AssetsController extends BaseController< | |
| hasPriceSubscription: this.#activeSubscriptions.has('ds:PriceDataSource'), | ||
| }); | ||
|
|
||
| this.#firstInitFetchReported = false; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Session metric resets on preference toggleMedium Severity
Additional Locations (1)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not a problem |
||
|
|
||
| // Stop price subscription first (uses direct messenger call) | ||
| this.unsubscribeAssetsPrice(); | ||
|
|
||
|
|
@@ -1726,12 +1807,8 @@ export class AssetsController extends BaseController< | |
|
|
||
| // Run through enrichment middlewares (Event Stack: Detection → Token → Price) | ||
| // Include 'metadata' in dataTypes so TokenDataSource runs to enrich detected assets | ||
| const enrichedResponse = await this.#executeMiddlewares( | ||
| [ | ||
| this.#detectionMiddleware.assetsMiddleware, | ||
| this.#tokenDataSource.assetsMiddleware, | ||
| this.#priceDataSource.assetsMiddleware, | ||
| ], | ||
| const { response: enrichedResponse } = await this.#executeMiddlewares( | ||
| [this.#detectionMiddleware, this.#tokenDataSource, this.#priceDataSource], | ||
| request ?? { | ||
| accountsWithSupportedChains: [], | ||
| chainIds: [], | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this a duration for all fetch we do ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes