genericQueryBatcher.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {createContext, Fragment, Ref, useEffect, useRef} from 'react';
  2. import {Client} from 'sentry/api';
  3. import {Organization} from 'sentry/types';
  4. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  5. import useApi from 'sentry/utils/useApi';
  6. import useOrganization from 'sentry/utils/useOrganization';
  7. import {createDefinedContext} from './utils';
  8. type QueryObject = {
  9. query: {
  10. [k: string]: any;
  11. };
  12. }; // TODO(k-fish): Fix to ensure exact types for all requests. Simplified type for now, need to pull this in from events file.
  13. type BatchQueryDefinition = {
  14. api: Client;
  15. batchProperty: string;
  16. path: string;
  17. reject: (reason?: string) => void;
  18. requestQueryObject: QueryObject;
  19. // Intermediate promise functions
  20. resolve: (value: any) => void;
  21. };
  22. type QueryBatch = {
  23. addQuery: (q: BatchQueryDefinition, id: symbol) => void;
  24. };
  25. const [GenericQueryBatcherProvider, _useGenericQueryBatcher] =
  26. createDefinedContext<QueryBatch>({
  27. name: 'GenericQueryBatcherContext',
  28. });
  29. function mergeKey(query: BatchQueryDefinition) {
  30. return `${query.batchProperty}.${query.path}`;
  31. }
  32. type MergeMap = Record<string, BatchQueryDefinition[]>;
  33. // Builds a map that will contain an array of query definitions by mergeable key (using batch property and path)
  34. function queriesToMap(collectedQueries: Record<symbol, BatchQueryDefinition>) {
  35. const keys = Reflect.ownKeys(collectedQueries);
  36. if (!keys.length) {
  37. return false;
  38. }
  39. const mergeMap: MergeMap = {};
  40. keys.forEach(key => {
  41. const query = collectedQueries[key];
  42. mergeMap[mergeKey(query)] = mergeMap[mergeKey(query)] || [];
  43. mergeMap[mergeKey(query)].push(query);
  44. delete collectedQueries[key];
  45. });
  46. return mergeMap;
  47. }
  48. function requestFunction(api: Client, path: string, queryObject: QueryObject) {
  49. return api.requestPromise(path, queryObject);
  50. }
  51. function _handleUnmergeableQuery(queryDefinition: BatchQueryDefinition) {
  52. const result = requestFunction(
  53. queryDefinition.api,
  54. queryDefinition.path,
  55. queryDefinition.requestQueryObject
  56. );
  57. queryDefinition.resolve(result);
  58. }
  59. function _handleUnmergeableQueries(mergeMap: MergeMap) {
  60. let queriesSent = 0;
  61. Object.keys(mergeMap).forEach(k => {
  62. // Using async forEach to ensure calls start in parallel.
  63. const mergeList = mergeMap[k];
  64. if (mergeList.length === 1) {
  65. const [queryDefinition] = mergeList;
  66. queriesSent++;
  67. _handleUnmergeableQuery(queryDefinition);
  68. }
  69. });
  70. return queriesSent;
  71. }
  72. function _handleMergeableQueries(mergeMap: MergeMap) {
  73. let queriesSent = 0;
  74. Object.keys(mergeMap).forEach(async k => {
  75. const mergeList = mergeMap[k];
  76. if (mergeList.length <= 1) {
  77. return;
  78. }
  79. const [exampleDefinition] = mergeList;
  80. const batchProperty = exampleDefinition.batchProperty;
  81. const query = {...exampleDefinition.requestQueryObject.query};
  82. const requestQueryObject = {...exampleDefinition.requestQueryObject, query};
  83. const batchValues: string[] = [];
  84. mergeList.forEach(q => {
  85. const batchFieldValue = q.requestQueryObject.query[batchProperty];
  86. if (Array.isArray(batchFieldValue)) {
  87. if (batchFieldValue.length > 1) {
  88. // Omit multiple requests with multi fields (eg. yAxis) for now and run them as single queries
  89. queriesSent++;
  90. _handleUnmergeableQuery(q);
  91. return;
  92. }
  93. // Unwrap array value if it is a single value
  94. batchValues.push(batchFieldValue[0]);
  95. } else {
  96. batchValues.push(batchFieldValue);
  97. }
  98. });
  99. requestQueryObject.query[batchProperty] = batchValues;
  100. queriesSent++;
  101. const requestPromise = requestFunction(
  102. exampleDefinition.api,
  103. exampleDefinition.path,
  104. requestQueryObject
  105. );
  106. try {
  107. const result = await requestPromise;
  108. // Unmerge back into individual results
  109. mergeList.forEach(queryDefinition => {
  110. const propertyName = Array.isArray(
  111. queryDefinition.requestQueryObject.query[queryDefinition.batchProperty]
  112. )
  113. ? queryDefinition.requestQueryObject.query[queryDefinition.batchProperty][0]
  114. : queryDefinition.requestQueryObject.query[queryDefinition.batchProperty];
  115. const singleResult = result[propertyName];
  116. queryDefinition.resolve(singleResult);
  117. });
  118. } catch (e) {
  119. // On error fail all requests relying on this merged query (for now)
  120. mergeList.forEach(q => q.reject(e));
  121. }
  122. });
  123. return queriesSent;
  124. }
  125. function handleBatching(
  126. organization: Organization,
  127. queries: Record<symbol, BatchQueryDefinition>
  128. ) {
  129. const mergeMap = queriesToMap(queries);
  130. if (!mergeMap) {
  131. return;
  132. }
  133. let queriesSent = 0;
  134. queriesSent += _handleUnmergeableQueries(mergeMap);
  135. queriesSent += _handleMergeableQueries(mergeMap);
  136. const queriesCollected = Object.values(mergeMap).reduce(
  137. (acc, mergeList) => acc + mergeList.length,
  138. 0
  139. );
  140. const queriesSaved = queriesCollected - queriesSent;
  141. trackAdvancedAnalyticsEvent('performance_views.landingv3.batch_queries', {
  142. organization,
  143. num_collected: queriesCollected,
  144. num_saved: queriesSaved,
  145. num_sent: queriesSent,
  146. });
  147. }
  148. export const GenericQueryBatcher = ({children}: {children: React.ReactNode}) => {
  149. const queries = useRef<Record<symbol, BatchQueryDefinition>>({});
  150. const timeoutRef = useRef<number | undefined>(undefined);
  151. const organization = useOrganization();
  152. const addQuery = (q: BatchQueryDefinition, id: symbol) => {
  153. queries.current[id] = q;
  154. window.clearTimeout(timeoutRef.current);
  155. // Put batch function in the next macro task to aggregate all requests in this frame.
  156. timeoutRef.current = window.setTimeout(() => {
  157. handleBatching(organization, queries.current);
  158. timeoutRef.current = undefined;
  159. }, 0);
  160. };
  161. // Cleanup timeout after component unmounts.
  162. useEffect(
  163. () => () => {
  164. timeoutRef.current && window.clearTimeout(timeoutRef.current);
  165. },
  166. []
  167. );
  168. return (
  169. <GenericQueryBatcherProvider
  170. value={{
  171. addQuery,
  172. }}
  173. >
  174. {children}
  175. </GenericQueryBatcherProvider>
  176. );
  177. };
  178. type NodeContext = {
  179. batchProperty: string;
  180. id: Ref<Symbol>;
  181. };
  182. const BatchNodeContext = createContext<NodeContext | undefined>(undefined);
  183. export type QueryBatching = {
  184. batchRequest: (_: Client, path: string, query: QueryObject) => Promise<any>;
  185. };
  186. // Wraps api request components to collect at most one request per frame / render pass using symbol as a unique id.
  187. // Transforms these requests into an intermediate promise and adds a query definition that the batch function will use.
  188. export function QueryBatchNode(props: {
  189. batchProperty: string;
  190. children(_: any): React.ReactNode;
  191. }) {
  192. const api = useApi();
  193. const {batchProperty, children} = props;
  194. const id = useRef(Symbol());
  195. let batchContext: QueryBatch;
  196. try {
  197. batchContext = _useGenericQueryBatcher();
  198. } catch (_) {
  199. return <Fragment>{children({})}</Fragment>;
  200. }
  201. function batchRequest(
  202. _: Client,
  203. path: string,
  204. requestQueryObject: QueryObject
  205. ): Promise<any> {
  206. const queryPromise = new Promise((resolve, reject) => {
  207. const queryDefinition: BatchQueryDefinition = {
  208. resolve,
  209. reject,
  210. batchProperty,
  211. path,
  212. requestQueryObject,
  213. api,
  214. };
  215. batchContext?.addQuery(queryDefinition, id.current);
  216. });
  217. return queryPromise;
  218. }
  219. const queryBatching: QueryBatching = {
  220. batchRequest,
  221. };
  222. return (
  223. <BatchNodeContext.Provider
  224. value={{
  225. id,
  226. batchProperty,
  227. }}
  228. >
  229. {children({queryBatching})}
  230. </BatchNodeContext.Provider>
  231. );
  232. }