mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-15 23:03:14 -06:00
Merge branch 'fix/playlist-refresh-not-once-per-item'
This commit is contained in:
commit
ce626c7e8e
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.remotedisplay.player.service
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #234 follow-up: how often a device may ask the server to re-send its playlist.
|
||||||
|
*
|
||||||
|
* "Refresh" is not cheap. requestPlaylistRefresh() emits a full `device:register`, and the server's
|
||||||
|
* register handler runs 7+ statements plus the whole fingerprint/identity path and rebuilds the
|
||||||
|
* playlist payload. PlaylistController.next() was calling it on EVERY item advance, so a panel on a
|
||||||
|
* 10-second image re-registered six times a minute, forever — and each reply pushed a full playlist
|
||||||
|
* back down, which is what kept feeding the restart loop behind #234.
|
||||||
|
*
|
||||||
|
* It was also redundant: the heartbeat already refreshes every 4th beat (60s), so the periodic pull
|
||||||
|
* this was duplicating exists either way. Throttling at the single chokepoint keeps every caller's
|
||||||
|
* intent — recovery paths still refresh, they just cannot stack up — without having to rank them.
|
||||||
|
*
|
||||||
|
* Pure so the interval arithmetic is testable without a device or a socket.
|
||||||
|
*/
|
||||||
|
object RefreshThrottle {
|
||||||
|
/** Just under the heartbeat's own 60s pull, so the two interleave instead of cancelling out. */
|
||||||
|
const val MIN_INTERVAL_MS = 55_000L
|
||||||
|
|
||||||
|
fun shouldRefresh(lastAtMs: Long, nowMs: Long): Boolean {
|
||||||
|
if (lastAtMs <= 0L) return true // never refreshed — always allow the first
|
||||||
|
val since = nowMs - lastAtMs
|
||||||
|
if (since < 0L) return true // clock corrected backwards; never wedge on it
|
||||||
|
return since >= MIN_INTERVAL_MS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -845,8 +845,17 @@ class WebSocketService : Service() {
|
||||||
connect()
|
connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Volatile private var lastRefreshAt = 0L
|
||||||
|
|
||||||
fun requestPlaylistRefresh() {
|
fun requestPlaylistRefresh() {
|
||||||
if (socket?.connected() != true || config.deviceId.isEmpty()) return
|
if (socket?.connected() != true || config.deviceId.isEmpty()) return
|
||||||
|
// #234 follow-up: this emits a FULL device:register (7+ server statements + the identity
|
||||||
|
// path + a playlist rebuild), and PlaylistController.next() calls it on every item advance.
|
||||||
|
// A 10-second image therefore re-registered six times a minute. The heartbeat already pulls
|
||||||
|
// a fresh playlist every 60s, so the per-item call bought nothing and cost a great deal.
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
if (!RefreshThrottle.shouldRefresh(lastRefreshAt, now)) return
|
||||||
|
lastRefreshAt = now
|
||||||
Log.i("WebSocketService", "Requesting playlist refresh")
|
Log.i("WebSocketService", "Requesting playlist refresh")
|
||||||
try {
|
try {
|
||||||
val data = org.json.JSONObject().apply {
|
val data = org.json.JSONObject().apply {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
package com.remotedisplay.player.service
|
||||||
|
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* #234 follow-up: a playlist refresh emits a full device:register, and PlaylistController.next()
|
||||||
|
* asked for one on EVERY item advance. On a 10-second image that is six full re-registrations a
|
||||||
|
* minute, per device, forever — each running 7+ server statements and the identity path, and each
|
||||||
|
* replying with a full playlist push. Measured on the reproduction: 9 item plays, 9 registrations.
|
||||||
|
*
|
||||||
|
* The heartbeat already pulls a fresh playlist every 60s, so the per-item call was duplicating a
|
||||||
|
* refresh that happens anyway.
|
||||||
|
*/
|
||||||
|
class RefreshThrottleTest {
|
||||||
|
|
||||||
|
private val NOW = 5_000_000L
|
||||||
|
private val MIN = RefreshThrottle.MIN_INTERVAL_MS
|
||||||
|
|
||||||
|
@Test fun the_very_first_refresh_always_goes_through() {
|
||||||
|
// A device that has never asked must not be held back by an empty timestamp.
|
||||||
|
assertTrue(RefreshThrottle.shouldRefresh(lastAtMs = 0L, nowMs = NOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun THE_BUG_a_second_refresh_moments_later_is_suppressed() {
|
||||||
|
// Two item advances a few seconds apart: the second must not re-register.
|
||||||
|
assertFalse(RefreshThrottle.shouldRefresh(NOW - 3_000, NOW))
|
||||||
|
assertFalse(RefreshThrottle.shouldRefresh(NOW - 10_000, NOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun once_the_interval_has_passed_it_refreshes_again() {
|
||||||
|
assertTrue(RefreshThrottle.shouldRefresh(NOW - MIN, NOW))
|
||||||
|
assertTrue(RefreshThrottle.shouldRefresh(NOW - (MIN + 1), NOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun just_under_the_interval_is_still_suppressed() {
|
||||||
|
assertFalse(RefreshThrottle.shouldRefresh(NOW - (MIN - 1), NOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun it_sits_under_the_heartbeat_pull_so_the_two_interleave() {
|
||||||
|
// The heartbeat refreshes every 60s. A window at or above that would systematically
|
||||||
|
// suppress the heartbeat's own pull, which is the one we are relying on to remain.
|
||||||
|
assertTrue(MIN < 60_000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun a_backwards_clock_never_wedges_refreshing() {
|
||||||
|
// Signage panels correct their clocks. A future 'last' must not disable refresh until the
|
||||||
|
// clock catches up — that would strand a device on a stale playlist for hours.
|
||||||
|
assertTrue(RefreshThrottle.shouldRefresh(NOW + 3_600_000, NOW))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test fun a_ten_second_item_collapses_from_six_refreshes_a_minute_to_about_one() {
|
||||||
|
// Walk a minute of 10s items and count what actually gets through.
|
||||||
|
var last = 0L
|
||||||
|
var allowed = 0
|
||||||
|
var t = NOW
|
||||||
|
repeat(6) {
|
||||||
|
if (RefreshThrottle.shouldRefresh(last, t)) { allowed++; last = t }
|
||||||
|
t += 10_000
|
||||||
|
}
|
||||||
|
assertTrue("expected roughly one refresh per minute, got $allowed", allowed <= 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue