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

Use only DOMHighResTimeStamp to calculate duration #143

Merged
merged 1 commit into from
Aug 18, 2021

Conversation

asakusuma
Copy link
Contributor

@asakusuma asakusuma commented Jun 17, 2021

Previously, we were combining Date.now() and DOMHighResTimeStamp values to calculate duration. These two value types use different clocks, so we should avoid combining the two value types where possible, since using two different clocks leaves us vulnerable to asymetrical issues. The native intersection observer uses DOMHighResTimeStamp, so we should use that type wherever possible in calculations.

This change also starts running the tests with and without native intersection observer. Previously we were not running tests with native intersection observer.

This change also removes the native-* files, which are superseded by USE_NATIVE_IO and never part of the public API.

Thanks to @xg-wang for pointing out some potential issues affecting one clock and not the other, which could in turn cause asymmetrical bugs:
w3c/hr-time#115
mdn/content#4713

target: SpanielTrackedElement;
}

export interface SpanielObserverEntry extends IntersectionObserverEntryInit {
export interface SpanielRect extends Partial<DOMRectReadOnly> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this just Readonly<Required<DOMRectInit>>?
And why do we make this change for the duration fix?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The previous code was not accurately typing things. SpanielObserver operates of entries from the intersection observer, which can either be the native intersection observer or the spaniel intersection observer. But the entry was always being typed as the native IntersectionObserverEntry, whereas the entries from the spaniel intersection observer polyfill are not exactly the same because they are missing a few sub fields. When I addressed this issue, there was a cascading effect. Basically the typings have to respect the fact that the entries could be from native or polyfill IO.

payload: record.payload,
label: state.threshold.label,
entering: false,
rootBounds: emptyRect,
boundingClientRect: boundingClientRect || emptyRect,
intersectionRect: emptyRect,
duration: time - state.lastVisible,
duration: state.lastVisible.highResTime - perfTime,
Copy link
Contributor

Choose a reason for hiding this comment

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

perfTime - state.lastVisible.highResTime? I guess test pass because we calculate opts.visibleTime = entry.unixTime - entry.duration; later.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

oh ya. I'll add a test for the cases where this duration doesn't get overwritten

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok looks like this always gets overwritten anyways. really need to re-write this code.

Comment on lines +222 to +227
x: interLeft,
y: interTop,
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these new? I might have missed where are x and y read?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

no, it's just that we previously were not actually returning a https://developer.mozilla.org/en-US/docs/Web/API/DOMRect
so adding it to be closer to the native behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like ClientRect is a subset of SpanielRect so there's no need to do ClientRect & SpanielRect. I also noticed there's built-in DOMRectReadOnly from https://github.com/microsoft/TypeScript/blob/main/lib/lib.dom.d.ts. So

export interface DOMRectReadOnly extends DOMRectInit, DOMMargin {}
and #143 (comment) might not be needed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok I created export type DOMRectPojo = Omit<DOMRectReadOnly, 'toJSON'>;
Instead of ClientRect & SpanielRect

src/intersection-observer.ts Outdated Show resolved Hide resolved
if (el.style.display === 'none') {
return {
unixTime: frame.dateNow,
Copy link
Contributor

Choose a reason for hiding this comment

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

Same above. Can we keep only one of time and unixTime

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed

let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry;
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId];
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart;
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time;
const unixTime = this.usingNativeIo
Copy link
Contributor

Choose a reason for hiding this comment

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

This ultimately becomes visibleTime in the watcher callback right?

We should stick to Date.now for start time (visibleTime), and use hr time for calculating the duration. Mixing timeOrigin / timing.navigationStart and Date.now is not reliable.

Pausing the hr time clock during sleep is okay for us to calculate impression duration.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if we use Date.now when using native intersection observer, that's also lossy because the callback is async. So capturing a Date.now() in the callback is not necessarily when the event happened. do you have an opinion on which approach is less bad?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, I don't think we are ever mixing timeOrigin / timing.navigationStart and Date.now for any individual entry (as in, https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry). I would rather use the HR time all throughout the entire entry or Date throughout the entire entry.

Copy link
Contributor

Choose a reason for hiding this comment

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

The line below, Math.floor(w.performance.timeOrigin || w.performance.timing.navigationStart + time) is time unix time? The correct should be hrTime which is relative to timeOrigin. Logic looks incorrect to me.

Left another related comment at line 209.

Copy link
Contributor

Choose a reason for hiding this comment

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

If the logic is indeed incorrect, do we have a test to cover?

Copy link
Contributor Author

@asakusuma asakusuma Aug 16, 2021

Choose a reason for hiding this comment

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

The correct should be hrTime which is relative to timeOrigin

Sort of, it's correct if we want to mirror native IO, but that is a breaking change.

This change just modifies how duration is calculated.

Added tests for visibleTime earlier today: https://github.com/linkedin/spaniel/blob/v2-release/test/playwright/modules/visible-time.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah wait I see what you mean, how using native IO previously meant using hr time. I will add another test for this behavior first.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

wait no we always are using unix timestamp time for the time field, even for native IO: https://github.com/linkedin/spaniel/blob/v2-release/src/spaniel-observer.ts#L141
So we should keep that behavior for now until a breaking change. I'll add a test regardless

src/watcher.ts Outdated Show resolved Hide resolved
src/interfaces.ts Outdated Show resolved Hide resolved
src/interfaces.ts Outdated Show resolved Hide resolved
record.thresholdStates.forEach((state: SpanielThresholdState) => {
const boundingClientRect = record.lastSeenEntry && record.lastSeenEntry.boundingClientRect;
this.handleThresholdExiting(
{
intersectionRatio: -1,
isIntersecting: false,
unixTime: time,
time,
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like this is not used anywhere anymore, can we remove?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While it's true that it's technically not needed anymore (because time is a unix timestamp), in next major version, I want to make time the high res timestamp to mirror the native observer API as closely as possible, get rid of the highRes property, and keep the unixTime property if you need unix time (which we do need a unix timestamp for visibleTime.

Copy link
Contributor Author

@asakusuma asakusuma Aug 16, 2021

Choose a reason for hiding this comment

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

spanielEntry.entering = false;
state.visible = false;
this.queuedEntries.push(spanielEntry);
}

clearTimeout(state.timeoutId);
}
private handleObserverEntry(entry: IntersectionObserverEntry) {
private handleObserverEntry(entry: InternalIntersectionObserverEntry) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure about this.
handleObserverEntry is bound as IO callback, and receives hrTime, why switch to use InternalIntersectionObserverEntry which has extra fields? And it seems we generates unix time inside spaniel, but internalCallback will pass in hrTime.

let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry;
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId];
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart;
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time;
const unixTime = this.usingNativeIo
Copy link
Contributor

Choose a reason for hiding this comment

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

The line below, Math.floor(w.performance.timeOrigin || w.performance.timing.navigationStart + time) is time unix time? The correct should be hrTime which is relative to timeOrigin. Logic looks incorrect to me.

Left another related comment at line 209.

let { intersectionRatio, rootBounds, boundingClientRect, intersectionRect, isIntersecting, time, target } = entry;
let record = this.recordStore[(<SpanielTrackedElement>target).__spanielId];
const timeOrigin = w.performance.timeOrigin || w.performance.timing.navigationStart;
const unixTime = this.usingNativeIo ? Math.floor(timeOrigin + time) : time;
const unixTime = this.usingNativeIo
Copy link
Contributor

Choose a reason for hiding this comment

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

If the logic is indeed incorrect, do we have a test to cover?

@asakusuma asakusuma force-pushed the asa/no-cross-clock branch 5 times, most recently from 4f4c383 to 23a148b Compare August 18, 2021 18:43
Previously, we were combining Date.now() and DOMHighResTimeStamp values to calculate duration. These two value types use different clocks, so we should avoid combining the two value types where possible, since using two different clocks leaves us vulnerable to asymetrical issues. The native intersection observer uses DOMHighResTimeStamp, so we should use that type wherever possible in calculations.

This change also removes the native-* files, which are superseded by USE_NATIVE_IO and never part of the public API.

Thanks to @xg-wang for pointing out some potential issues affecting one clock and not the other, which could in turn cause asymmetrical bugs:
w3c/hr-time#115
mdn/content#4713
@asakusuma asakusuma merged commit c8d1c9c into linkedin:v2-release Aug 18, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants