Add hysteresis and EMA to ping to avoid flickering network badge (#7197)

* Don't use WEAK and yellow

* fmt && lint && tsc

* Fix up the rebase & dark mode colors

* Update src/hooks/useNetworkStatus.tsx

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>

* Change Weak to Ok

* Change Connected to Strong

* fmt

* Sync selectors for start sketch

* Remove unused test-util brought back in a rebase

* Align the other OKs

* Add an else statement to overallState

---------

Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
Zookeeper Lee
2025-05-30 15:50:05 -04:00
committed by GitHub
parent 5fccaad0e7
commit 1c07e8af5b
4 changed files with 69 additions and 24 deletions

View File

@ -823,7 +823,7 @@ test('theme persists', async ({ page, context, homePage }) => {
uploadThroughput: -1,
})
await expect(networkToggle).toContainText('Connected')
await expect(networkToggle).toContainText('Network health (Strong)')
await expect(page.getByText('building scene')).not.toBeVisible()

View File

@ -14,8 +14,10 @@ test.describe('Test network related behaviors', () => {
'simulate network down and network little widget',
{ tag: '@skipLocalEngine' },
async ({ page, homePage }) => {
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
const networkToggleConnectedText = page.getByText(
'Network health (Strong)'
)
const networkToggleWeakText = page.getByText('Network health (Ok)')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -98,8 +100,10 @@ test.describe('Test network related behaviors', () => {
{ tag: '@skipLocalEngine' },
async ({ page, homePage, toolbar, scene, cmdBar }) => {
const networkToggle = page.getByTestId('network-toggle')
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
const networkToggleConnectedText = page.getByText(
'Network health (Strong)'
)
const networkToggleWeakText = page.getByText('Network health (Ok)')
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
@ -282,8 +286,10 @@ profile001 = startProfile(sketch001, at = [12.34, -12.34])
{ tag: ['@desktop', '@skipLocalEngine'] },
async ({ page, homePage, scene, cmdBar, toolbar, tronApp }) => {
const networkToggle = page.getByTestId('network-toggle')
const networkToggleConnectedText = page.getByText('Connected')
const networkToggleWeakText = page.getByText('Network health (Weak)')
const networkToggleConnectedText = page.getByText(
'Network health (Strong)'
)
const networkToggleWeakText = page.getByText('Network health (Ok)')
if (!tronApp) {
fail()

View File

@ -10,8 +10,8 @@ import { reportRejection } from '@src/lib/trap'
import { toSync } from '@src/lib/utils'
export const NETWORK_HEALTH_TEXT: Record<NetworkHealthState, string> = {
[NetworkHealthState.Ok]: 'Connected',
[NetworkHealthState.Weak]: 'Weak',
[NetworkHealthState.Ok]: 'Strong',
[NetworkHealthState.Weak]: 'Ok',
[NetworkHealthState.Issue]: 'Problem',
[NetworkHealthState.Disconnected]: 'Offline',
}
@ -53,8 +53,8 @@ const overallConnectionStateColor: Record<NetworkHealthState, IconColorConfig> =
bg: 'bg-succeed-10/30 dark:bg-succeed-80/50',
},
[NetworkHealthState.Weak]: {
icon: 'text-warn-80 dark:text-warn-10',
bg: 'bg-warn-10 dark:bg-warn-80/80',
icon: 'text-succeed-50 dark:text-succeed-30',
bg: 'bg-lime-300/70 dark:bg-lime-300/30',
},
[NetworkHealthState.Issue]: {
icon: 'text-destroy-80 dark:text-destroy-10',

View File

@ -48,7 +48,8 @@ export function useNetworkStatus() {
const [overallState, setOverallState] = useState<NetworkHealthState>(
NetworkHealthState.Disconnected
)
const [ping, setPing] = useState<undefined | number>(undefined)
const [pingRaw, setPingRaw] = useState<undefined | number>(undefined)
const [pingEMA, setPingEMA] = useState<undefined | number>(undefined)
const [hasCopied, setHasCopied] = useState<boolean>(false)
const [error, setError] = useState<ErrorType | undefined>(undefined)
@ -66,16 +67,54 @@ export function useNetworkStatus() {
const [hasIssues, setHasIssues] = useState<boolean | undefined>(undefined)
useEffect(() => {
setOverallState(
!internetConnected
? NetworkHealthState.Disconnected
: hasIssues || hasIssues === undefined
? NetworkHealthState.Issue
: (ping ?? 0) > 16.6 * 3 // we consider ping longer than 3 frames as weak
? NetworkHealthState.Weak
: NetworkHealthState.Ok
)
}, [hasIssues, internetConnected, ping])
if (immediateState.type === EngineConnectionStateType.Disconnecting) {
// Reset our running average.
setPingRaw(undefined)
setPingEMA(undefined)
}
}, [immediateState])
useEffect(() => {
if (!pingRaw) return
// We use an exponential running average to smooth out ping values.
const pingDataPointsToConsider = 10
const multiplier = 2 / (pingDataPointsToConsider + 1)
let pingEMANext = ((pingEMA ?? 0) + pingRaw) / 2
pingEMANext = pingEMANext * multiplier + (pingEMA ?? 0) * (1 - multiplier)
setPingEMA(pingEMANext)
}, [pingRaw])
useEffect(() => {
// We consider ping longer than 3 frames as weak
const WEAK_PING = 16.6 * 3
const OK_PING = 16.6 * 2
// A is used in the literature to specify the "window" of switching
const A = 1.25
const CENTER = (WEAK_PING + OK_PING) / 2
const THRESHOLD_GOOD = CENTER / A // Lower bound
const THRESHOLD_WEAK = CENTER * A // Upper bound
let nextOverallState = overallState
if (!internetConnected) {
nextOverallState = NetworkHealthState.Disconnected
} else if (hasIssues || hasIssues === undefined) {
nextOverallState = NetworkHealthState.Issue
} else if (pingEMA && pingEMA < THRESHOLD_GOOD) {
nextOverallState = NetworkHealthState.Ok
} else if (pingEMA && pingEMA > THRESHOLD_WEAK) {
nextOverallState = NetworkHealthState.Weak
} else {
nextOverallState = NetworkHealthState.Ok
}
if (nextOverallState === overallState) return
setOverallState(nextOverallState)
}, [hasIssues, internetConnected, pingEMA, overallState])
useEffect(() => {
const onlineCallback = () => {
@ -126,7 +165,7 @@ export function useNetworkStatus() {
useEffect(() => {
const onPingPongChange = ({ detail: state }: CustomEvent) => {
setPing(state)
setPingRaw(state)
}
const onConnectionStateChange = ({
@ -231,6 +270,6 @@ export function useNetworkStatus() {
error,
setHasCopied,
hasCopied,
ping,
ping: pingEMA !== undefined ? Math.trunc(pingEMA) : pingEMA,
}
}