123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- /**
- * Copyright (c) Facebook, Inc. and its affiliates.
- *
- * This source code is licensed under the MIT license found in the
- * LICENSE file in the root directory of this source tree.
- *
- * @flow
- * @format
- */
- 'use strict';
- const performanceNow = require('fbjs/lib/performanceNow');
- const warning = require('fbjs/lib/warning');
- export type FillRateInfo = Info;
- class Info {
- any_blank_count: number = 0;
- any_blank_ms: number = 0;
- any_blank_speed_sum: number = 0;
- mostly_blank_count: number = 0;
- mostly_blank_ms: number = 0;
- pixels_blank: number = 0;
- pixels_sampled: number = 0;
- pixels_scrolled: number = 0;
- total_time_spent: number = 0;
- sample_count: number = 0;
- }
- type FrameMetrics = {
- inLayout?: boolean,
- length: number,
- offset: number,
- ...
- };
- const DEBUG = false;
- let _listeners: Array<(Info) => void> = [];
- let _minSampleCount = 10;
- let _sampleRate = DEBUG ? 1 : null;
- /**
- * A helper class for detecting when the maximem fill rate of `VirtualizedList` is exceeded.
- * By default the sampling rate is set to zero and this will do nothing. If you want to collect
- * samples (e.g. to log them), make sure to call `FillRateHelper.setSampleRate(0.0-1.0)`.
- *
- * Listeners and sample rate are global for all `VirtualizedList`s - typical usage will combine with
- * `SceneTracker.getActiveScene` to determine the context of the events.
- */
- class FillRateHelper {
- _anyBlankStartTime = (null: ?number);
- _enabled = false;
- _getFrameMetrics: (index: number) => ?FrameMetrics;
- _info = new Info();
- _mostlyBlankStartTime = (null: ?number);
- _samplesStartTime = (null: ?number);
- static addListener(
- callback: FillRateInfo => void,
- ): {remove: () => void, ...} {
- warning(
- _sampleRate !== null,
- 'Call `FillRateHelper.setSampleRate` before `addListener`.',
- );
- _listeners.push(callback);
- return {
- remove: () => {
- _listeners = _listeners.filter(listener => callback !== listener);
- },
- };
- }
- static setSampleRate(sampleRate: number) {
- _sampleRate = sampleRate;
- }
- static setMinSampleCount(minSampleCount: number) {
- _minSampleCount = minSampleCount;
- }
- constructor(getFrameMetrics: (index: number) => ?FrameMetrics) {
- this._getFrameMetrics = getFrameMetrics;
- this._enabled = (_sampleRate || 0) > Math.random();
- this._resetData();
- }
- activate() {
- if (this._enabled && this._samplesStartTime == null) {
- DEBUG && console.debug('FillRateHelper: activate');
- this._samplesStartTime = performanceNow();
- }
- }
- deactivateAndFlush() {
- if (!this._enabled) {
- return;
- }
- const start = this._samplesStartTime; // const for flow
- if (start == null) {
- DEBUG &&
- console.debug('FillRateHelper: bail on deactivate with no start time');
- return;
- }
- if (this._info.sample_count < _minSampleCount) {
- // Don't bother with under-sampled events.
- this._resetData();
- return;
- }
- const total_time_spent = performanceNow() - start;
- const info: any = {
- ...this._info,
- total_time_spent,
- };
- if (DEBUG) {
- const derived = {
- avg_blankness: this._info.pixels_blank / this._info.pixels_sampled,
- avg_speed: this._info.pixels_scrolled / (total_time_spent / 1000),
- avg_speed_when_any_blank:
- this._info.any_blank_speed_sum / this._info.any_blank_count,
- any_blank_per_min:
- this._info.any_blank_count / (total_time_spent / 1000 / 60),
- any_blank_time_frac: this._info.any_blank_ms / total_time_spent,
- mostly_blank_per_min:
- this._info.mostly_blank_count / (total_time_spent / 1000 / 60),
- mostly_blank_time_frac: this._info.mostly_blank_ms / total_time_spent,
- };
- for (const key in derived) {
- derived[key] = Math.round(1000 * derived[key]) / 1000;
- }
- console.debug('FillRateHelper deactivateAndFlush: ', {derived, info});
- }
- _listeners.forEach(listener => listener(info));
- this._resetData();
- }
- computeBlankness(
- props: {
- data: any,
- getItemCount: (data: any) => number,
- initialNumToRender: number,
- ...
- },
- state: {
- first: number,
- last: number,
- ...
- },
- scrollMetrics: {
- dOffset: number,
- offset: number,
- velocity: number,
- visibleLength: number,
- ...
- },
- ): number {
- if (
- !this._enabled ||
- props.getItemCount(props.data) === 0 ||
- this._samplesStartTime == null
- ) {
- return 0;
- }
- const {dOffset, offset, velocity, visibleLength} = scrollMetrics;
- // Denominator metrics that we track for all events - most of the time there is no blankness and
- // we want to capture that.
- this._info.sample_count++;
- this._info.pixels_sampled += Math.round(visibleLength);
- this._info.pixels_scrolled += Math.round(Math.abs(dOffset));
- const scrollSpeed = Math.round(Math.abs(velocity) * 1000); // px / sec
- // Whether blank now or not, record the elapsed time blank if we were blank last time.
- const now = performanceNow();
- if (this._anyBlankStartTime != null) {
- this._info.any_blank_ms += now - this._anyBlankStartTime;
- }
- this._anyBlankStartTime = null;
- if (this._mostlyBlankStartTime != null) {
- this._info.mostly_blank_ms += now - this._mostlyBlankStartTime;
- }
- this._mostlyBlankStartTime = null;
- let blankTop = 0;
- let first = state.first;
- let firstFrame = this._getFrameMetrics(first);
- while (first <= state.last && (!firstFrame || !firstFrame.inLayout)) {
- firstFrame = this._getFrameMetrics(first);
- first++;
- }
- // Only count blankTop if we aren't rendering the first item, otherwise we will count the header
- // as blank.
- if (firstFrame && first > 0) {
- blankTop = Math.min(
- visibleLength,
- Math.max(0, firstFrame.offset - offset),
- );
- }
- let blankBottom = 0;
- let last = state.last;
- let lastFrame = this._getFrameMetrics(last);
- while (last >= state.first && (!lastFrame || !lastFrame.inLayout)) {
- lastFrame = this._getFrameMetrics(last);
- last--;
- }
- // Only count blankBottom if we aren't rendering the last item, otherwise we will count the
- // footer as blank.
- if (lastFrame && last < props.getItemCount(props.data) - 1) {
- const bottomEdge = lastFrame.offset + lastFrame.length;
- blankBottom = Math.min(
- visibleLength,
- Math.max(0, offset + visibleLength - bottomEdge),
- );
- }
- const pixels_blank = Math.round(blankTop + blankBottom);
- const blankness = pixels_blank / visibleLength;
- if (blankness > 0) {
- this._anyBlankStartTime = now;
- this._info.any_blank_speed_sum += scrollSpeed;
- this._info.any_blank_count++;
- this._info.pixels_blank += pixels_blank;
- if (blankness > 0.5) {
- this._mostlyBlankStartTime = now;
- this._info.mostly_blank_count++;
- }
- } else if (scrollSpeed < 0.01 || Math.abs(dOffset) < 1) {
- this.deactivateAndFlush();
- }
- return blankness;
- }
- enabled(): boolean {
- return this._enabled;
- }
- _resetData() {
- this._anyBlankStartTime = null;
- this._info = new Info();
- this._mostlyBlankStartTime = null;
- this._samplesStartTime = null;
- }
- }
- module.exports = FillRateHelper;
|