flamegraphQueryParamSync.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useEffect} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {Query} from 'history';
  4. import * as qs from 'query-string';
  5. import {DeepPartial} from 'sentry/types/utils';
  6. import {useFlamegraphState} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphState';
  7. import {Rect} from 'sentry/utils/profiling/gl/utils';
  8. import {useLocalStorageState} from 'sentry/utils/useLocalStorageState';
  9. import {useLocation} from 'sentry/utils/useLocation';
  10. import {DEFAULT_FLAMEGRAPH_STATE, FlamegraphState} from './flamegraphContext';
  11. // Intersect the types so we can properly guard
  12. type PossibleQuery =
  13. | Query
  14. | (Pick<FlamegraphState['preferences'], 'colorCoding' | 'sorting' | 'view'> &
  15. Pick<FlamegraphState['search'], 'query'>);
  16. function isColorCoding(
  17. value: PossibleQuery['colorCoding'] | FlamegraphState['preferences']['colorCoding']
  18. ): value is FlamegraphState['preferences']['colorCoding'] {
  19. if (typeof value !== 'string') {
  20. return false;
  21. }
  22. return (
  23. value === 'by symbol name' ||
  24. value === 'by system frame' ||
  25. value === 'by application frame' ||
  26. value === 'by library' ||
  27. value === 'by recursion' ||
  28. value === 'by frequency'
  29. );
  30. }
  31. function isLayout(
  32. value: PossibleQuery['colorCoding'] | FlamegraphState['preferences']['colorCoding']
  33. ): value is FlamegraphState['preferences']['layout'] {
  34. if (typeof value !== 'string') {
  35. return false;
  36. }
  37. return value === 'table right' || value === 'table bottom' || value === 'table left';
  38. }
  39. function isSorting(
  40. value: PossibleQuery['sorting'] | FlamegraphState['preferences']['sorting']
  41. ): value is FlamegraphState['preferences']['sorting'] {
  42. if (typeof value !== 'string') {
  43. return false;
  44. }
  45. return value === 'left heavy' || value === 'call order';
  46. }
  47. function isView(
  48. value: PossibleQuery['view'] | FlamegraphState['preferences']['view']
  49. ): value is FlamegraphState['preferences']['view'] {
  50. if (typeof value !== 'string') {
  51. return false;
  52. }
  53. return value === 'top down' || value === 'bottom up';
  54. }
  55. export function decodeFlamegraphStateFromQueryParams(
  56. query: qs.ParsedQuery
  57. ): DeepPartial<FlamegraphState> {
  58. const decoded: DeepPartial<FlamegraphState> = {};
  59. // Similarly to how we encode frame name and values, we want to
  60. // omit the field entirely if it is not present in the query string or
  61. // if it is an empty string.
  62. if (typeof query.frameName === 'string') {
  63. decoded.profiles = {
  64. ...(decoded.profiles ?? {}),
  65. highlightFrames: {
  66. ...(decoded.profiles?.highlightFrames ?? {}),
  67. name: query.frameName ? query.frameName : undefined,
  68. },
  69. };
  70. }
  71. if (typeof query.framePackage === 'string') {
  72. decoded.profiles = {
  73. ...(decoded.profiles ?? {}),
  74. highlightFrames: {
  75. ...(decoded.profiles?.highlightFrames ?? {}),
  76. package: query.framePackage ? query.framePackage : undefined,
  77. },
  78. };
  79. }
  80. if (typeof query.tid === 'string' && !isNaN(parseInt(query.tid, 10))) {
  81. decoded.profiles = {
  82. ...(decoded.profiles ?? {}),
  83. threadId: parseInt(query.tid, 10),
  84. };
  85. }
  86. const fov = Rect.decode(query.fov);
  87. if (fov) {
  88. decoded.position = {view: fov};
  89. }
  90. decoded.preferences = {};
  91. decoded.search = {};
  92. if (isLayout(query.layout)) {
  93. decoded.preferences.layout = query.layout;
  94. }
  95. if (isColorCoding(query.colorCoding)) {
  96. decoded.preferences.colorCoding = query.colorCoding;
  97. }
  98. if (isSorting(query.sorting)) {
  99. decoded.preferences.sorting = query.sorting;
  100. }
  101. if (isView(query.view)) {
  102. decoded.preferences.view = query.view;
  103. }
  104. if (typeof query.query === 'string') {
  105. decoded.search.query = query.query;
  106. }
  107. return decoded;
  108. }
  109. export function encodeFlamegraphStateToQueryParams(state: FlamegraphState) {
  110. const highlightFrameToEncode: Record<string, string> = {};
  111. // For some frames we do not have a package (or name) if that happens we want to omit
  112. // the field entirely from the query string. This is to avoid default values being used
  113. // as qs.parse will initialize empty values to "" which can differ from the respective
  114. // frame values which are undefined.
  115. if (state.profiles.highlightFrames?.name) {
  116. highlightFrameToEncode.frameName = state.profiles.highlightFrames.name;
  117. }
  118. if (state.profiles.highlightFrames?.package) {
  119. highlightFrameToEncode.framePackage = state.profiles.highlightFrames.package;
  120. }
  121. const highlightFrame = state.profiles.highlightFrames
  122. ? {
  123. frameName: state.profiles.highlightFrames?.name,
  124. framePackage: state.profiles.highlightFrames?.package,
  125. }
  126. : {};
  127. return {
  128. colorCoding: state.preferences.colorCoding,
  129. sorting: state.preferences.sorting,
  130. view: state.preferences.view,
  131. query: state.search.query,
  132. ...highlightFrame,
  133. ...(state.position.view.isEmpty()
  134. ? {fov: undefined}
  135. : {fov: Rect.encode(state.position.view)}),
  136. ...(typeof state.profiles.threadId === 'number'
  137. ? {tid: state.profiles.threadId}
  138. : {}),
  139. };
  140. }
  141. function maybeOmitHighlightedFrame(query: Query, state: FlamegraphState) {
  142. if (!state.profiles.highlightFrames && query.frameName && query.framePackage) {
  143. const {frameName: _, framePackage: __, ...rest} = query;
  144. return rest;
  145. }
  146. return query;
  147. }
  148. export function FlamegraphStateQueryParamSync() {
  149. const location = useLocation();
  150. const [state] = useFlamegraphState();
  151. useEffect(() => {
  152. browserHistory.replace({
  153. ...location,
  154. query: {
  155. ...maybeOmitHighlightedFrame(location.query, state),
  156. ...encodeFlamegraphStateToQueryParams(state),
  157. },
  158. });
  159. // We only want to sync the query params when the state changes
  160. // eslint-disable-next-line react-hooks/exhaustive-deps
  161. }, [state]);
  162. return null;
  163. }
  164. export const FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY = 'flamegraph-preferences';
  165. export function FlamegraphStateLocalStorageSync() {
  166. const [state] = useFlamegraphState();
  167. const [_, setState] = useLocalStorageState<DeepPartial<FlamegraphState>>(
  168. FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY,
  169. {
  170. preferences: {
  171. layout: DEFAULT_FLAMEGRAPH_STATE.preferences.layout,
  172. timelines: DEFAULT_FLAMEGRAPH_STATE.preferences.timelines,
  173. view: DEFAULT_FLAMEGRAPH_STATE.preferences.view,
  174. },
  175. }
  176. );
  177. useEffect(() => {
  178. setState({
  179. preferences: {
  180. layout: state.preferences.layout,
  181. timelines: state.preferences.timelines,
  182. view: state.preferences.view,
  183. },
  184. });
  185. // We only want to sync the local storage when the state changes
  186. // eslint-disable-next-line react-hooks/exhaustive-deps
  187. }, [state.preferences.layout, state.preferences.timelines, state.preferences.view]);
  188. return null;
  189. }