Perf: Add diagnostic sub-spans to ManualOpenSearchRouter#83076
Perf: Add diagnostic sub-spans to ManualOpenSearchRouter#83076
Conversation
Add 4 sub-spans and 2 attributes to the ManualOpenSearchRouter Sentry span to identify which phases of the search router opening are bottlenecks. Sub-spans: - SearchRouter.ModalCloseWait: Modal.close() callback latency - SearchRouter.OptionsInit: Cold-path createOptionList() cost - SearchRouter.ComputeOptions: JS computation in SearchAutocompleteList - SearchRouter.ListRender: FlashList rendering + native layout Attributes: - cold_start: whether options needed initialization - trigger: 'button' or 'keyboard' Co-authored-by: Cursor <cursoragent@cursor.com>
|
@aimane-chnaif Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
| import React, {useRef} from 'react'; | ||
| import type {StyleProp, View, ViewStyle} from 'react-native'; | ||
| import React, { useRef } from 'react'; | ||
| import type { StyleProp, View, ViewStyle } from 'react-native'; |
There was a problem hiding this comment.
Looks like your local prettier is broken
|
|
||
| if (isInitialRender) { | ||
| endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS); | ||
| startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER, { |
There was a problem hiding this comment.
❌ PERF-12 (docs)
The SPAN_SEARCH_ROUTER_LIST_RENDER span is started during render but endSpan is only called inside the onLayout callback. If the component unmounts before layout completes (e.g., the user closes the search router quickly), this span will never be ended, causing a resource leak.
Consider adding a cleanup mechanism via useEffect to ensure the span is ended on unmount:
useEffect(() => {
return () => {
endSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_LIST_RENDER);
};
}, []);Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
Codecov Report❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8be55219db
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| parentSpan.setAttribute('cold_start', !areOptionsInitialized); | ||
| coldStartAttributeSet.current = true; |
There was a problem hiding this comment.
Set cold_start after options state has synchronized
This sets cold_start during the first render and then permanently locks it via coldStartAttributeSet, but useOptionsList() initializes areOptionsInitialized to false and only syncs it from context in an effect (OptionListContextProvider.tsx uses useState(false) then updates in useEffect). On warm opens, this render still sees false, so the ManualOpenSearchRouter span is incorrectly tagged cold_start=true and never corrected, which breaks warm vs cold segmentation.
Useful? React with 👍 / 👎.
| startSpan(CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS, { | ||
| name: CONST.TELEMETRY.SPAN_SEARCH_ROUTER_COMPUTE_OPTIONS, | ||
| op: 'function', | ||
| }); | ||
| computeSpanStarted.current = true; |
There was a problem hiding this comment.
Guard ComputeOptions span behind open-router parent span
This starts SearchRouter.ComputeOptions unconditionally whenever SearchAutocompleteList mounts, but that component is also rendered from SearchPageHeaderInput (non-router flows), so these spans are produced without a ManualOpenSearchRouter parent. Since startSpan() in activeSpans.ts cancels any existing span with the same ID before creating a new one, unrelated mounts can overwrite/cancel real router instrumentation and pollute the telemetry breakdown.
Useful? React with 👍 / 👎.
Explanation of Change
The
ManualOpenSearchRouterSentry span currently has no child spans — Sentry shows 100% self-time with no breakdown, making it impossible to identify which phase is the bottleneck. This PR adds 4 diagnostic sub-spans that decompose the parent span into sequential, non-overlapping phases:SearchRouter.ModalCloseWait— Time waiting forModal.close()callback. Identifies if the modal system adds latency before we even start mounting the search UI.SearchRouter.OptionsInit(cold path only) — Time spent increateOptionList(). Only fires when the parent span is active (guarded bygetSpan()check to avoid noise from otheruseOptionsListconsumers).SearchRouter.ComputeOptions— Total JS computation time inSearchAutocompleteList(searchOptions, autocompleteSuggestions, sections assembly). Only measured on first render.SearchRouter.ListRender— FlashList rendering + native layout time (from post-computation toonLayout). Only measured on first render.The gap between sub-spans (parent minus all children) gives us the React mount + Onyx hydration overhead without requiring a dedicated span.
Additionally, 2 attributes are added to the parent span for Sentry segmentation:
cold_start:true/false— whether options needed initialization when SearchRouter mountedtrigger:'button'/'keyboard'— how the search was opened (button path was previously missing this)Overhead is negligible (~8
Date.now()calls per open). Sub-spans only fire on the initial render, guarded by refs.Fixed Issues
$ #79353
Tests
Offline tests
QA Steps
ManualOpenSearchRouterspans — verify child spans (SearchRouter.ModalCloseWait,SearchRouter.ComputeOptions,SearchRouter.ListRender) appearcold_startandtriggerattributes are present on the parent spanPR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
N/A — No visual changes, telemetry-only instrumentation
Android: mWeb Chrome
N/A — No visual changes, telemetry-only instrumentation
iOS: Native
N/A — No visual changes, telemetry-only instrumentation
iOS: mWeb Safari
N/A — No visual changes, telemetry-only instrumentation
MacOS: Chrome / Safari
N/A — No visual changes, telemetry-only instrumentation
Made with Cursor