Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions parsely/src/main/java/com/parsely/parselyandroid/Clock.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.parsely.parselyandroid

import java.util.Calendar
import java.util.TimeZone
import kotlin.time.Duration.Companion.milliseconds

open class Clock {
open val now
get() = Calendar.getInstance(TimeZone.getTimeZone("UTC")).timeInMillis.milliseconds
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ class EngagementManager {
private TimerTask waitingTimerTask;
private long latestDelayMillis, totalTime;
private Calendar startTime;
private final UpdateEngagementIntervalCalculator intervalCalculator;
private final HeartbeatIntervalCalculator intervalCalculator;

public EngagementManager(
ParselyTracker parselyTracker,
Timer parentTimer,
long intervalMillis,
Map<String, Object> baseEvent,
UpdateEngagementIntervalCalculator intervalCalculator
HeartbeatIntervalCalculator intervalCalculator
) {
this.parselyTracker = parselyTracker;
this.baseEvent = baseEvent;
Expand Down Expand Up @@ -70,7 +70,7 @@ private void scheduleNextExecution(long delay) {
TimerTask task = new TimerTask() {
public void run() {
doEnqueue(scheduledExecutionTime());
latestDelayMillis = intervalCalculator.updateLatestInterval(startTime);
latestDelayMillis = intervalCalculator.calculate(startTime);
scheduleNextExecution(latestDelayMillis);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.parsely.parselyandroid

import java.util.Calendar
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

internal open class HeartbeatIntervalCalculator(private val clock: Clock) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  1. Praise (❤️): Great work on this, I love how this class is now using duration and the x.seconds/minutes/hours approach, much more readable. Also, kudos for fixing the bugs of unwanted rounding (f273ee1)! 🥇

It also covers the calculator with unit tests and changes the calculator implementation to use less error-prone kotlin.time.Duration class to make arithmetical operations between different times.

  1. Suggestion (💡): When converting a Java class to Kotlin (9823815), consider using the Extra commit for .java > .kt renames option while committing this change. This will make it easier for anyone reviewing this change later-on as it will show an actual diffing of files instead of showing the Java file being deleted and a new Kotlin file being added.

image

FYI: For more context on check this discussion I had with an external contributor a while back. Let me know your thoughts. 🙏

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Thanks for sharing about Extra commit option, TIL!


open fun calculate(startTime: Calendar): Long {
val startTimeDuration = startTime.time.time.milliseconds
val nowDuration = clock.now

val totalTrackedTime = nowDuration - startTimeDuration
val totalWithOffset = totalTrackedTime + OFFSET_MATCHING_BASE_INTERVAL
val newInterval = totalWithOffset * BACKOFF_PROPORTION
val clampedNewInterval = minOf(MAX_TIME_BETWEEN_HEARTBEATS, newInterval)
return clampedNewInterval.inWholeMilliseconds
}

companion object {
const val BACKOFF_PROPORTION = 0.3
val OFFSET_MATCHING_BASE_INTERVAL = 35.seconds
val MAX_TIME_BETWEEN_HEARTBEATS = 15.minutes
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR fixes the minor off difference when calculating heartbeat intervals (magnitude of up to 900ms). I won't change anything drastically, yet it's better to be in sync with iOS and have a clear implementation, without "hidden casting" issues.

👍

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public class ParselyTracker {
private String lastPageviewUuid = null;
@NonNull
private final EventsBuilder eventsBuilder;
@NonNull final UpdateEngagementIntervalCalculator intervalCalculator = new UpdateEngagementIntervalCalculator();
@NonNull final HeartbeatIntervalCalculator intervalCalculator = new HeartbeatIntervalCalculator(new Clock());

/**
* Create a new ParselyTracker instance.
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ internal class EngagementManagerTest {
}
}

class FakeIntervalCalculator : UpdateEngagementIntervalCalculator() {
override fun updateLatestInterval(startTime: Calendar): Long {
class FakeIntervalCalculator : HeartbeatIntervalCalculator(Clock()) {
override fun calculate(startTime: Calendar): Long {
return DEFAULT_INTERVAL_MILLIS
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.parsely.parselyandroid

import com.parsely.parselyandroid.HeartbeatIntervalCalculator.Companion.MAX_TIME_BETWEEN_HEARTBEATS
import java.util.Calendar
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test

internal class HeartbeatIntervalCalculatorTest {

private lateinit var sut: HeartbeatIntervalCalculator
private val fakeClock = FakeClock()

@Before
fun setUp() {
sut = HeartbeatIntervalCalculator(fakeClock)
}

@Test
fun `given the same time of start and current time, when calculating interval, return offset times backoff proportion`() {
// given
fakeClock.fakeNow = Duration.ZERO
val startTime = Calendar.getInstance().apply {
timeInMillis = 0
}

// when
val result = sut.calculate(startTime)

// then
// ((currentTime + offset) * backoff) and then in milliseconds
// (0 + 35) * 0.3 * 1000 = 10500
assertThat(result).isEqualTo(10500)
}

@Test
fun `given a time that will cause the interval to surpass the MAX_TIME_BETWEEN_HEARTBEATS, when calculating interval, then return the MAX_TIME_BETWEEN_HEARTBEATS`() {
// given
// "excessiveTime" is a calculated point in time where the resulting interval would
// surpass MAX_TIME_BETWEEN_HEARTBEATS
// (currentTime + offset) * backoff = max
// currentTime = (max / backoff) - offset, so
// (15 minutes / 0.3) - 35 seconds = 2965 seconds. Add 1 second to be over the limit
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Praise (❤️): Thanks for adding these comments and making it clear how these numbers are calculated. 🙇

val excessiveTime = 2965.seconds + 1.seconds
fakeClock.fakeNow = excessiveTime
val startTime = Calendar.getInstance().apply {
timeInMillis = 0
}

// when
val result = sut.calculate(startTime)

// then
assertThat(result).isEqualTo(MAX_TIME_BETWEEN_HEARTBEATS.inWholeMilliseconds)
}

@Test
fun `given a specific time point, when updating latest interval, it correctly calculates the interval`() {
// given
val startTime = Calendar.getInstance().apply {
timeInMillis = 0
}
fakeClock.fakeNow = 2.seconds

// when
val result = sut.calculate(startTime)

// then
// ((currentTime + offset) * backoff) and then in milliseconds
// (2 + 35) * 0.3 * 1000 = 11100
assertThat(result).isEqualTo(11100)
}

class FakeClock : Clock() {
var fakeNow = Duration.ZERO
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Praise (❤️): Nice!


override val now: Duration
get() = fakeNow
}
}