FillRateHelper.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const performanceNow = require('fbjs/lib/performanceNow');
  12. const warning = require('fbjs/lib/warning');
  13. export type FillRateInfo = Info;
  14. class Info {
  15. any_blank_count: number = 0;
  16. any_blank_ms: number = 0;
  17. any_blank_speed_sum: number = 0;
  18. mostly_blank_count: number = 0;
  19. mostly_blank_ms: number = 0;
  20. pixels_blank: number = 0;
  21. pixels_sampled: number = 0;
  22. pixels_scrolled: number = 0;
  23. total_time_spent: number = 0;
  24. sample_count: number = 0;
  25. }
  26. type FrameMetrics = {
  27. inLayout?: boolean,
  28. length: number,
  29. offset: number,
  30. ...
  31. };
  32. const DEBUG = false;
  33. let _listeners: Array<(Info) => void> = [];
  34. let _minSampleCount = 10;
  35. let _sampleRate = DEBUG ? 1 : null;
  36. /**
  37. * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
  38. * By default the sampling rate is set to zero and this will do nothing. If you want to collect
  39. * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
  40. *
  41. * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
  42. * `SceneTracker.getActiveScene` to determine the context of the events.
  43. */
  44. class FillRateHelper {
  45. _anyBlankStartTime = (null: ?number);
  46. _enabled = false;
  47. _getFrameMetrics: (index: number) => ?FrameMetrics;
  48. _info = new Info();
  49. _mostlyBlankStartTime = (null: ?number);
  50. _samplesStartTime = (null: ?number);
  51. static addListener(
  52. callback: FillRateInfo => void,
  53. ): {remove: () => void, ...} {
  54. warning(
  55. _sampleRate !== null,
  56. 'Call `FillRateHelper.setSampleRate` before `addListener`.',
  57. );
  58. _listeners.push(callback);
  59. return {
  60. remove: () => {
  61. _listeners = _listeners.filter(listener => callback !== listener);
  62. },
  63. };
  64. }
  65. static setSampleRate(sampleRate: number) {
  66. _sampleRate = sampleRate;
  67. }
  68. static setMinSampleCount(minSampleCount: number) {
  69. _minSampleCount = minSampleCount;
  70. }
  71. constructor(getFrameMetrics: (index: number) => ?FrameMetrics) {
  72. this._getFrameMetrics = getFrameMetrics;
  73. this._enabled = (_sampleRate || 0) > Math.random();
  74. this._resetData();
  75. }
  76. activate() {
  77. if (this._enabled && this._samplesStartTime == null) {
  78. DEBUG && console.debug('FillRateHelper: activate');
  79. this._samplesStartTime = performanceNow();
  80. }
  81. }
  82. deactivateAndFlush() {
  83. if (!this._enabled) {
  84. return;
  85. }
  86. const start = this._samplesStartTime; // const for flow
  87. if (start == null) {
  88. DEBUG &&
  89. console.debug('FillRateHelper: bail on deactivate with no start time');
  90. return;
  91. }
  92. if (this._info.sample_count < _minSampleCount) {
  93. // Don't bother with under-sampled events.
  94. this._resetData();
  95. return;
  96. }
  97. const total_time_spent = performanceNow() - start;
  98. const info: any = {
  99. ...this._info,
  100. total_time_spent,
  101. };
  102. if (DEBUG) {
  103. const derived = {
  104. avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
  105. avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
  106. avg_speed_when_any_blank:
  107. this._info.any_blank_speed_sum / this._info.any_blank_count,
  108. any_blank_per_min:
  109. this._info.any_blank_count / (total_time_spent / 1000 / 60),
  110. any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
  111. mostly_blank_per_min:
  112. this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
  113. mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
  114. };
  115. for (const key in derived) {
  116. derived[key] = Math.round(1000 * derived[key]) / 1000;
  117. }
  118. console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
  119. }
  120. _listeners.forEach(listener => listener(info));
  121. this._resetData();
  122. }
  123. computeBlankness(
  124. props: {
  125. data: any,
  126. getItemCount: (data: any) => number,
  127. initialNumToRender: number,
  128. ...
  129. },
  130. state: {
  131. first: number,
  132. last: number,
  133. ...
  134. },
  135. scrollMetrics: {
  136. dOffset: number,
  137. offset: number,
  138. velocity: number,
  139. visibleLength: number,
  140. ...
  141. },
  142. ): number {
  143. if (
  144. !this._enabled ||
  145. props.getItemCount(props.data) === 0 ||
  146. this._samplesStartTime == null
  147. ) {
  148. return 0;
  149. }
  150. const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
  151. // Denominator metrics that we track for all events - most of the time there is no blankness and
  152. // we want to capture that.
  153. this._info.sample_count++;
  154. this._info.pixels_sampled += Math.round(visibleLength);
  155. this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
  156. const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
  157. // Whether blank now or not, record the elapsed time blank if we were blank last time.
  158. const now = performanceNow();
  159. if (this._anyBlankStartTime != null) {
  160. this._info.any_blank_ms += now - this._anyBlankStartTime;
  161. }
  162. this._anyBlankStartTime = null;
  163. if (this._mostlyBlankStartTime != null) {
  164. this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
  165. }
  166. this._mostlyBlankStartTime = null;
  167. let blankTop = 0;
  168. let first = state.first;
  169. let firstFrame = this._getFrameMetrics(first);
  170. while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) {
  171. firstFrame = this._getFrameMetrics(first);
  172. first++;
  173. }
  174. // Only count blankTop if we aren't rendering the first item, otherwise we will count the header
  175. // as blank.
  176. if (firstFrame && first > 0) {
  177. blankTop = Math.min(
  178. visibleLength,
  179. Math.max(0, firstFrame.offset - offset),
  180. );
  181. }
  182. let blankBottom = 0;
  183. let last = state.last;
  184. let lastFrame = this._getFrameMetrics(last);
  185. while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) {
  186. lastFrame = this._getFrameMetrics(last);
  187. last--;
  188. }
  189. // Only count blankBottom if we aren't rendering the last item, otherwise we will count the
  190. // footer as blank.
  191. if (lastFrame && last < props.getItemCount(props.data) - 1) {
  192. const bottomEdge = lastFrame.offset + lastFrame.length;
  193. blankBottom = Math.min(
  194. visibleLength,
  195. Math.max(0, offset + visibleLength - bottomEdge),
  196. );
  197. }
  198. const pixels_blank = Math.round(blankTop + blankBottom);
  199. const blankness = pixels_blank / visibleLength;
  200. if (blankness > 0) {
  201. this._anyBlankStartTime = now;
  202. this._info.any_blank_speed_sum += scrollSpeed;
  203. this._info.any_blank_count++;
  204. this._info.pixels_blank += pixels_blank;
  205. if (blankness > 0.5) {
  206. this._mostlyBlankStartTime = now;
  207. this._info.mostly_blank_count++;
  208. }
  209. } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
  210. this.deactivateAndFlush();
  211. }
  212. return blankness;
  213. }
  214. enabled(): boolean {
  215. return this._enabled;
  216. }
  217. _resetData() {
  218. this._anyBlankStartTime = null;
  219. this._info = new Info();
  220. this._mostlyBlankStartTime = null;
  221. this._samplesStartTime = null;
  222. }
  223. }
  224. module.exports = FillRateHelper;