Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Impression measurement config #1

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0afc48d
Adding impression measurement : adding minimum view percentage for ch…
prachi-myntra Feb 1, 2023
96f8a59
adding minimum item view time logic for impression measurement
prachi-myntra Feb 2, 2023
79a439d
code refactoring
prachi-myntra Feb 2, 2023
f94f8d4
adding prepare command
prachi-myntra Feb 3, 2023
fdd3de7
adding minimum view time check
prachi-myntra Feb 3, 2023
2066b1f
fixing build issue
prachi-myntra Feb 3, 2023
9c2be52
default time value change
Feb 21, 2023
21c0318
default view time modified
Feb 22, 2023
db82652
adding commentwq
prachi-myntra Mar 2, 2023
b733a24
removing trailing space
prachi-myntra Mar 3, 2023
0ff8e70
addressing review comments
prachi-myntra Mar 6, 2023
0e748f5
added lodash type for jenkins fail fix
nitish24p Apr 14, 2023
0d26334
added forced resolution of version for package lock for yarn installe…
nitish24p May 10, 2023
88b7feb
resetting last visible and resported indexes
iamashish121 Jul 14, 2023
67db7b8
Merge pull request #3 from myntra/resetViewabilityTracker
sridhar-arumugam Jul 17, 2023
204576d
Revert "Reset viewability tracker"
sridhar-arumugam Jul 17, 2023
3c11259
Merge pull request #4 from myntra/revert-3-resetViewabilityTracker
sridhar-arumugam Jul 17, 2023
02cf7a0
resooved lint issues
iamashish121 Jul 17, 2023
f765491
sync
iamashish121 Jul 17, 2023
4f5c3aa
Merge pull request #5 from myntra/resetViewabilityTracker
sridhar-arumugam Jul 17, 2023
35e1bfa
fixed version for @types/resize-observer-browser removed ^
vivektiwarimyntra Nov 21, 2023
8aba7e0
Merge pull request #6 from myntra/devDep-version-fix
sridhar-arumugam Nov 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"clean": "sh scripts/clean.sh",
"release": "sh scripts/publish-latest.sh",
"release-beta": "sh scripts/publish-beta.sh",
"release-local": "sh scripts/publish-local.sh"
"release-local": "sh scripts/publish-local.sh",
"prepare": "npm run build"
},
"keywords": [
"react-native",
Expand Down
10 changes: 9 additions & 1 deletion src/core/RecyclerListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export interface OnRecreateParams {
lastOffset?: number;
}

export interface ViewabilityConfig {
minimumItemViewPercentage: number;
minimumViewTime: number;
}

export interface RecyclerListViewProps {
layoutProvider: BaseLayoutProvider;
dataProvider: BaseDataProvider;
Expand Down Expand Up @@ -110,6 +115,7 @@ export interface RecyclerListViewProps {
//For all props that need to be proxied to inner/external scrollview. Put them in an object and they'll be spread
//and passed down. For better typescript support.
scrollViewProps?: object;
viewabilityConfig?: ViewabilityConfig;
applyWindowCorrection?: (offsetX: number, offsetY: number, windowCorrection: WindowCorrection) => void;
onItemLayout?: (index: number) => void;
windowCorrectionConfig?: { value?: WindowCorrection, applyToInitialOffset?: boolean, applyToItemScroll?: boolean };
Expand Down Expand Up @@ -141,6 +147,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
onEndReachedThreshold: 0,
onEndReachedThresholdRelative: 0,
renderAheadOffset: IS_WEB ? 1000 : 250,
viewabilityConfig: undefined,
};

public static propTypes = {};
Expand Down Expand Up @@ -181,7 +188,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
this._pendingScrollToOffset = offset;
}, (index) => {
return this.props.dataProvider.getStableId(index);
}, !props.disableRecycling);
}, !props.disableRecycling, props.viewabilityConfig);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

viewabilityConfig is just initialized in the constructor any changes to viewabilityConfig after the first render will not be respected.
This use case would come for pagination later with multiple impression configs at the widget level.


if (this.props.windowCorrectionConfig) {
let windowCorrection;
Expand Down Expand Up @@ -262,6 +269,7 @@ export default class RecyclerListView<P extends RecyclerListViewProps, S extends
}
}
}
this._virtualRenderer.timerCleanup();
}

public scrollToIndex(index: number, animate?: boolean): void {
Expand Down
78 changes: 70 additions & 8 deletions src/core/ViewabilityTracker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import BinarySearch from "../utils/BinarySearch";
import { Constants } from "./constants/Constants";
import { Dimension } from "./dependencies/LayoutProvider";
import { Layout } from "./layoutmanager/LayoutManager";
import { ViewabilityConfig } from "./RecyclerListView";
/***
* Given an offset this utility can compute visible items. Also tracks previously visible items to compute items which get hidden or visible
* Virtual renderer uses callbacks from this utility to main recycle pool and the render stack.
Expand Down Expand Up @@ -34,12 +36,15 @@ export default class ViewabilityTracker {
private _isHorizontal: boolean;
private _windowBound: number;
private _visibleIndexes: number[];
private _lastReportedVisibleIndexes: number[];
private _engagedIndexes: number[];
private _layouts: Layout[] = [];
private _actualOffset: number;
private _defaultCorrection: WindowCorrection;
private _viewabilityConfig: ViewabilityConfig | undefined;
private timers: Set<number> = new Set();

constructor(renderAheadOffset: number, initialOffset: number) {
constructor(renderAheadOffset: number, initialOffset: number, viewabilityConfig: ViewabilityConfig | undefined) {
this._currentOffset = Math.max(0, initialOffset);
this._maxOffset = 0;
this._actualOffset = 0;
Expand All @@ -52,12 +57,14 @@ export default class ViewabilityTracker {

this._visibleIndexes = []; //needs to be sorted
this._engagedIndexes = []; //needs to be sorted
this._lastReportedVisibleIndexes = [];

this.onVisibleRowsChanged = null;
this.onEngagedRowsChanged = null;

this._relevantDim = { start: 0, end: 0 };
this._defaultCorrection = { startCorrection: 0, endCorrection: 0, windowShift: 0 };
this._viewabilityConfig = viewabilityConfig;
}

public init(windowCorrection: WindowCorrection): void {
Expand Down Expand Up @@ -149,6 +156,11 @@ export default class ViewabilityTracker {
this._actualOffset = actualOffset;
}

public timerCleanup(): void {
this.timers.forEach(clearTimeout);
this.timers.clear();
}

private _findFirstVisibleIndexOptimally(): number {
let firstVisibleIndex = 0;

Expand Down Expand Up @@ -185,7 +197,8 @@ export default class ViewabilityTracker {
for (let i = 0; i < count; i++) {
itemRect = this._layouts[i];
this._setRelevantBounds(itemRect, relevantDim);
if (this._itemIntersectsVisibleWindow(relevantDim.start, relevantDim.end)) {
const minimumItemViewPercentage = this._viewabilityConfig && this._viewabilityConfig.minimumItemViewPercentage || undefined;
if (this._itemIntersectsVisibleWindow(relevantDim.start, relevantDim.end, minimumItemViewPercentage)) {
return i;
}
}
Expand Down Expand Up @@ -242,7 +255,8 @@ export default class ViewabilityTracker {
const itemRect = this._layouts[index];
let isFound = false;
this._setRelevantBounds(itemRect, relevantDim);
if (this._itemIntersectsVisibleWindow(relevantDim.start, relevantDim.end)) {
const mininumViewPercentage = this._viewabilityConfig && this._viewabilityConfig.minimumItemViewPercentage || undefined;
if (this._itemIntersectsVisibleWindow(relevantDim.start, relevantDim.end, mininumViewPercentage)) {
if (insertOnTop) {
newVisibleIndexes.splice(0, 0, index);
newEngagedIndexes.splice(0, 0, index);
Expand Down Expand Up @@ -297,8 +311,31 @@ export default class ViewabilityTracker {
return this._itemIntersectsWindow(this._engagedWindow, startBound, endBound);
}

private _itemIntersectsVisibleWindow(startBound: number, endBound: number): boolean {
return this._itemIntersectsWindow(this._visibleWindow, startBound, endBound);
private _isItemInVisibleBounds(window: Range, itemStartBound: number, itemEndBound: number, mininumViewPercentage: number | undefined): boolean {
let visibleItemContent = 0;
const itemSize = itemEndBound - itemStartBound;

if (window.start >= itemStartBound && window.end >= itemEndBound) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you add some comments explaining the if-else with some examples

visibleItemContent = itemEndBound - window.start;
} else if (window.start <= itemStartBound && window.end <= itemEndBound) {
visibleItemContent = window.end - itemStartBound;
} else if (window.start <= itemStartBound && window.end >= itemEndBound) {
visibleItemContent = itemEndBound - itemStartBound;
} else if (window.start >= itemStartBound && window.end <= itemEndBound) {
return true;
} else {
return false;
}

const isVisible = mininumViewPercentage
? visibleItemContent / itemSize * 100 >= mininumViewPercentage
: visibleItemContent > 0;
return isVisible;
}

private _itemIntersectsVisibleWindow(startBound: number, endBound: number, mininumViewPercentage?: number): boolean {
return this._isItemInVisibleBounds(this._visibleWindow, startBound, endBound, mininumViewPercentage) ||
this._isZeroHeightEdgeElement(this._visibleWindow, startBound, endBound);
}

private _updateTrackingWindows(offset: number, correction: WindowCorrection): void {
Expand All @@ -317,18 +354,43 @@ export default class ViewabilityTracker {

//TODO:Talha optimize this
private _diffUpdateOriginalIndexesAndRaiseEvents(newVisibleItems: number[], newEngagedItems: number[]): void {
this._diffArraysAndCallFunc(newVisibleItems, this._visibleIndexes, this.onVisibleRowsChanged);
const minimumViewTime = this._viewabilityConfig && this._viewabilityConfig.minimumViewTime
? this._viewabilityConfig.minimumViewTime
: 0;
this._diffArraysAndCallFunc(newVisibleItems, this._visibleIndexes, this.onVisibleRowsChanged, minimumViewTime);
this._diffArraysAndCallFunc(newEngagedItems, this._engagedIndexes, this.onEngagedRowsChanged);
this._visibleIndexes = newVisibleItems;
this._engagedIndexes = newEngagedItems;
}

private _diffArraysAndCallFunc(newItems: number[], oldItems: number[], func: TOnItemStatusChanged | null): void {
private checkMinimumViewTime = (all: number[], now: number[], notNow: number[], minimumViewTime: number, callbackFunc: TOnItemStatusChanged): void => {
const that = this;
const timeoutId = setTimeout(() => {
that.timers.delete(timeoutId);

const currAll = all.filter((index) => that._visibleIndexes.indexOf(index) >= 0);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the max array batch size of all: number[]

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all has all the list items that are visible in viewport at a time
So it depends on list items sizes

const currNow = currAll.filter((index) => that._lastReportedVisibleIndexes.indexOf(index) === -1);
const currNotNow = that._lastReportedVisibleIndexes.filter((index) => currAll.indexOf(index) === -1);

if (currAll.length > 0 && (currNow.length > 0 || currNotNow.length > 0)) {
that._lastReportedVisibleIndexes = currAll;
callbackFunc(currAll, currNow, currNotNow);
}
}, minimumViewTime);
this.timers.add(timeoutId);
}

private _diffArraysAndCallFunc(newItems: number[], oldItems: number[], func: TOnItemStatusChanged | null, minimumViewTime?: number): void {
if (func) {
const now = this._calculateArrayDiff(newItems, oldItems);
const notNow = this._calculateArrayDiff(oldItems, newItems);
if (now.length > 0 || notNow.length > 0) {
func([...newItems], now, notNow);
// Adding default minimum view time of 250ms for performance optimization
if (minimumViewTime && minimumViewTime >= Constants.DEFAULT_MIN_VIEW_TIME) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of not executing the minimumViewTime logic incase of DEFAULT_MIN_VIEW_TIME greater than minimumViewTime you could execute with minimumViewTime logic with default value.

this.checkMinimumViewTime([...newItems], now, notNow, minimumViewTime, func);
} else {
func([...newItems], now, notNow);
}
}
}
}
Expand Down
20 changes: 17 additions & 3 deletions src/core/VirtualRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import ViewabilityTracker, { TOnItemStatusChanged, WindowCorrection } from "./Vi
import { ObjectUtil, Default } from "ts-object-utils";
import TSCast from "../utils/TSCast";
import { BaseDataProvider } from "./dependencies/DataProvider";
import { ViewabilityConfig } from "./RecyclerListView";

/***
* Renderer which keeps track of recyclable items and the currently rendered items. Notifies list view to re render if something changes, like scroll offset
Expand Down Expand Up @@ -52,11 +53,13 @@ export default class VirtualRenderer {
private _viewabilityTracker: ViewabilityTracker | null = null;
private _dimensions: Dimension | null;
private _optimizeForAnimations: boolean = false;
private _viewabilityConfig: ViewabilityConfig | undefined = undefined;

constructor(renderStackChanged: (renderStack: RenderStack) => void,
scrollOnNextUpdate: (point: Point) => void,
fetchStableId: StableIdProvider,
isRecyclingEnabled: boolean) {
isRecyclingEnabled: boolean,
viewabilityConfig?: ViewabilityConfig) {
//Keeps track of items that need to be rendered in the next render cycle
this._renderStack = {};

Expand All @@ -78,6 +81,8 @@ export default class VirtualRenderer {
this._startKey = 0;

this.onVisibleItemsChanged = null;

this._viewabilityConfig = viewabilityConfig;
}

public getLayoutDimension(): Dimension {
Expand Down Expand Up @@ -112,11 +117,19 @@ export default class VirtualRenderer {
this.onVisibleItemsChanged = callback;
}

public timerCleanup(): void {
if (!this._viewabilityTracker) {
return;
}
this._viewabilityTracker.timerCleanup();
}

public removeVisibleItemsListener(): void {
this.onVisibleItemsChanged = null;

if (this._viewabilityTracker) {
this._viewabilityTracker.onVisibleRowsChanged = null;
this._viewabilityTracker.timerCleanup();
}
}

Expand Down Expand Up @@ -192,9 +205,10 @@ export default class VirtualRenderer {
if (this._params) {
this._viewabilityTracker = new ViewabilityTracker(
Default.value<number>(this._params.renderAheadOffset, 0),
Default.value<number>(this._params.initialOffset, 0));
Default.value<number>(this._params.initialOffset, 0),
this._viewabilityConfig);
} else {
this._viewabilityTracker = new ViewabilityTracker(0, 0);
this._viewabilityTracker = new ViewabilityTracker(0, 0, this._viewabilityConfig);
}
this._prepareViewabilityTracker();
}
Expand Down
1 change: 1 addition & 0 deletions src/core/constants/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const Constants = {
CONTEXT_PROVIDER_OFFSET_KEY_SUFFIX : "_offset",
CONTEXT_PROVIDER_LAYOUT_KEY_SUFFIX: "_layouts",
DEFAULT_MIN_VIEW_TIME : 250,
};