From 1672544cfdf79fc1feabd52bd7d5aa8d47694d23 Mon Sep 17 00:00:00 2001 From: Soumalya Das Date: Tue, 18 Aug 2026 20:37:40 +0530 Subject: [PATCH] fix: current speed stuck at 0 B/s due to unstable spring animation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit animate.Spring multiplied velocity by 1 - damping*dt. At the UI tick interval (130ms) this evaluates to 1 - 12*0.13 = -0.56, a negative damping factor that flips the velocity sign and grows its magnitude every tick. The animated throughput value collapsed to deep negative values, which FormatBpsExt clamps to zero — so the current download/ upload readout stayed at 0 B/s while the sparkline and peak (which read raw history samples) showed real traffic. Damping now uses exp(-damping*dt), which stays positive for any step size, keeping the spring stable and convergent. Adds a regression test at the UI tick interval. Bump VERSION to 0.2.2. Closes #29 --- CHANGELOG.md | 8 ++++++++ VERSION | 2 +- cmd/flow/main.go | 2 +- internal/animate/ease.go | 2 +- internal/animate/ease_test.go | 19 +++++++++++++++++++ 5 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bbcc34..1f67716 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [0.2.2] - 2026-08-18 + +### Fixed +- Current download/upload speed always displaying `0 B/s` — the spring animation in `animate.Spring` was numerically unstable at the UI tick interval (130ms), since `1 - damping*dt` evaluated to a negative damping factor. Velocity flipped sign and grew every tick, driving the animated value deeply negative, where `FormatBpsExt` clamped it to `0 B/s`. Damping now uses an exponential factor (`exp(-damping*dt)`), keeping the spring stable and convergent at any step size. The sparkline and peak values were unaffected because they read raw history samples directly. + +### Changed +- VERSION bumped to 0.2.2. + ## [0.2.1] - 2026-07-23 ### Added diff --git a/VERSION b/VERSION index 7dff5b8..ee1372d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.2.2 diff --git a/cmd/flow/main.go b/cmd/flow/main.go index eb06e7e..40629ca 100644 --- a/cmd/flow/main.go +++ b/cmd/flow/main.go @@ -16,7 +16,7 @@ import ( "github.com/programmersd21/flow/internal/ui" ) -var version = "0.2.1" +var version = "0.2.2" func main() { flagTiny := flag.Bool("tiny", false, "single-line mode for tmux/status bars") diff --git a/internal/animate/ease.go b/internal/animate/ease.go index 3c05537..b558773 100644 --- a/internal/animate/ease.go +++ b/internal/animate/ease.go @@ -32,7 +32,7 @@ func Clamp01(t float64) float64 { func Spring(current, target float64, velocity *float64, dt float64) float64 { force := stiffness * (target - current) *velocity += force * dt - *velocity *= 1.0 - damping*dt + *velocity *= math.Exp(-damping * dt) if *velocity < 0.0001 && *velocity > -0.0001 { *velocity = 0 } diff --git a/internal/animate/ease_test.go b/internal/animate/ease_test.go index 3429a2f..6cd77df 100644 --- a/internal/animate/ease_test.go +++ b/internal/animate/ease_test.go @@ -48,3 +48,22 @@ func TestSpringConverges(t *testing.T) { t.Errorf("Spring did not converge: %f (want ~100)", val) } } + +func TestSpringStableAtUITickInterval(t *testing.T) { + const target = 34603008.0 + var vel float64 + val := 0.0 + minVal := val + for i := 0; i < 1000; i++ { + val = Spring(val, target, &vel, 0.13) + if val < minVal { + minVal = val + } + } + if math.Abs(val-target) > target*0.01 { + t.Errorf("Spring did not converge at dt=0.13: %f (want ~%f)", val, target) + } + if minVal < 0 { + t.Errorf("Spring oscillated negative at dt=0.13 (min %f) — value would render as 0 B/s", minVal) + } +}