genericQueryBatcher.tsx 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import {createContext, Fragment, Ref, useEffect, useRef} from 'react';
  2. import identity from 'lodash/identity';
  3. import {Client} from 'sentry/api';
  4. import {Organization} from 'sentry/types';
  5. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  6. import useApi from 'sentry/utils/useApi';
  7. import useOrganization from 'sentry/utils/useOrganization';
  8. import {createDefinedContext} from './utils';
  9. type QueryObject = {
  10. includeAllArgs: boolean | undefined;
  11. query: {
  12. [k: string]: any;
  13. };
  14. }; // TODO(k-fish): Fix to ensure exact types for all requests. Simplified type for now, need to pull this in from events file.
  15. export type Transform = (data: any, queryDefinition: BatchQueryDefinition) => any;
  16. type BatchQueryDefinition = {
  17. api: Client;
  18. batchProperty: string;
  19. path: string;
  20. reject: (reason?: string) => void;
  21. requestQueryObject: QueryObject;
  22. // Intermediate promise functions
  23. resolve: (value: any) => void;
  24. transform?: Transform;
  25. };
  26. type QueryBatch = {
  27. addQuery: (q: BatchQueryDefinition, id: symbol) => void;
  28. };
  29. const [GenericQueryBatcherProvider, _useGenericQueryBatcher] =
  30. createDefinedContext<QueryBatch>({
  31. name: 'GenericQueryBatcherContext',
  32. });
  33. function mergeKey(query: BatchQueryDefinition) {
  34. return `${query.batchProperty}.${query.path}`;
  35. }
  36. type MergeMap = Record<string, BatchQueryDefinition[]>;
  37. // Builds a map that will contain an array of query definitions by mergeable key (using batch property and path)
  38. function queriesToMap(collectedQueries: Record<symbol, BatchQueryDefinition>) {
  39. const keys = Reflect.ownKeys(collectedQueries);
  40. if (!keys.length) {
  41. return false;
  42. }
  43. const mergeMap: MergeMap = {};
  44. keys.forEach(key => {
  45. const query = collectedQueries[key];
  46. mergeMap[mergeKey(query)] = mergeMap[mergeKey(query)] || [];
  47. mergeMap[mergeKey(query)].push(query);
  48. delete collectedQueries[key];
  49. });
  50. return mergeMap;
  51. }
  52. function requestFunction(api: Client, path: string, queryObject: QueryObject) {
  53. return api.requestPromise(path, queryObject);
  54. }
  55. function _handleUnmergeableQuery(queryDefinition: BatchQueryDefinition) {
  56. const result = requestFunction(
  57. queryDefinition.api,
  58. queryDefinition.path,
  59. queryDefinition.requestQueryObject
  60. );
  61. queryDefinition.resolve(result);
  62. }
  63. function _handleUnmergeableQueries(mergeMap: MergeMap) {
  64. let queriesSent = 0;
  65. Object.keys(mergeMap).forEach(k => {
  66. // Using async forEach to ensure calls start in parallel.
  67. const mergeList = mergeMap[k];
  68. if (mergeList.length === 1) {
  69. const [queryDefinition] = mergeList;
  70. queriesSent++;
  71. _handleUnmergeableQuery(queryDefinition);
  72. }
  73. });
  74. return queriesSent;
  75. }
  76. function _handleMergeableQueries(mergeMap: MergeMap) {
  77. let queriesSent = 0;
  78. Object.keys(mergeMap).forEach(async k => {
  79. const mergeList = mergeMap[k];
  80. if (mergeList.length <= 1) {
  81. return;
  82. }
  83. const [exampleDefinition] = mergeList;
  84. const batchProperty = exampleDefinition.batchProperty;
  85. const query = {...exampleDefinition.requestQueryObject.query};
  86. const requestQueryObject = {...exampleDefinition.requestQueryObject, query};
  87. const batchValues: string[] = [];
  88. mergeList.forEach(q => {
  89. const batchFieldValue = q.requestQueryObject.query[batchProperty];
  90. if (Array.isArray(batchFieldValue)) {
  91. if (batchFieldValue.length > 1) {
  92. // Omit multiple requests with multi fields (eg. yAxis) for now and run them as single queries
  93. queriesSent++;
  94. _handleUnmergeableQuery(q);
  95. return;
  96. }
  97. // Unwrap array value if it is a single value
  98. batchValues.push(batchFieldValue[0]);
  99. } else {
  100. batchValues.push(batchFieldValue);
  101. }
  102. });
  103. requestQueryObject.query[batchProperty] = batchValues;
  104. queriesSent++;
  105. const requestPromise = requestFunction(
  106. exampleDefinition.api,
  107. exampleDefinition.path,
  108. requestQueryObject
  109. );
  110. try {
  111. const result = await requestPromise;
  112. // Unmerge back into individual results
  113. mergeList.forEach(queryDefinition => {
  114. queryDefinition.resolve(
  115. (queryDefinition.transform || identity)(result, queryDefinition)
  116. );
  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. transform?: Transform;
  192. }) {
  193. const api = useApi();
  194. const {batchProperty, children, transform} = props;
  195. const id = useRef(Symbol());
  196. let batchContext: QueryBatch;
  197. try {
  198. batchContext = _useGenericQueryBatcher();
  199. } catch (_) {
  200. return <Fragment>{children({})}</Fragment>;
  201. }
  202. function batchRequest(
  203. _: Client,
  204. path: string,
  205. requestQueryObject: QueryObject
  206. ): Promise<any> {
  207. const queryPromise = new Promise((resolve, reject) => {
  208. const queryDefinition: BatchQueryDefinition = {
  209. resolve,
  210. reject,
  211. transform,
  212. batchProperty,
  213. path,
  214. requestQueryObject,
  215. api,
  216. };
  217. batchContext?.addQuery(queryDefinition, id.current);
  218. });
  219. return queryPromise;
  220. }
  221. const queryBatching: QueryBatching = {
  222. batchRequest,
  223. };
  224. return (
  225. <BatchNodeContext.Provider
  226. value={{
  227. id,
  228. batchProperty,
  229. }}
  230. >
  231. {children({queryBatching})}
  232. </BatchNodeContext.Provider>
  233. );
  234. }