From 5155bc36cd92d3e6fc87f9233f209daae0a8c080 Mon Sep 17 00:00:00 2001
From: iRedTea
Date: Tue, 13 Oct 2026 23:12:30 +0300
Subject: [PATCH] feat: measure each direction of the speed test for 7 s
(0.0.7)
---
VERSION.txt | 2 +-
internal/agent/speed.go | 28 ++++++++++++++---------
internal/proto/proto.go | 3 +++
internal/server/handlers_panel_stat.go | 16 +++++++------
internal/server/web/templates/status.html | 4 ++--
5 files changed, 32 insertions(+), 21 deletions(-)
diff --git a/VERSION.txt b/VERSION.txt
index 1750564..5a5831a 100644
--- a/VERSION.txt
+++ b/VERSION.txt
@@ -1 +1 @@
-0.0.6
+0.0.7
diff --git a/internal/agent/speed.go b/internal/agent/speed.go
index bd4df53..ccb6409 100644
--- a/internal/agent/speed.go
+++ b/internal/agent/speed.go
@@ -13,24 +13,30 @@ import (
)
const (
- // speedWindow is how long each direction is measured for.
- speedWindow = 3 * time.Second
+ // speedWindow is how long each direction is measured for, unless the
+ // server asks for another window; speedWindowMax bounds what it may ask.
+ speedWindow = 3 * time.Second
+ speedWindowMax = 30 * time.Second
// speedWait is how long the download may take to start: in a relayed test
// its bytes come from the browser.
speedWait = 10 * time.Second
- // speedUpCap bounds the upload, whatever the link does in speedWindow.
+ // speedUpCap bounds the upload, whatever the link does in the window.
speedUpCap = 48 << 20
)
// doSpeed measures the agent's HTTP link to the server: round-trip time of an
-// empty request, then how much flows each way in speedWindow. In a relayed
+// empty request, then how much flows each way in the window. In a relayed
// test the server pipes both streams to the browser, so they run end to end.
func (a *Agent) doSpeed(ctx context.Context, env proto.TaskEnvelope) proto.TaskResult {
var args proto.SysSpeedArgs
if len(env.Args) > 0 {
_ = json.Unmarshal(env.Args, &args)
}
- res, err := a.measureSpeed(ctx, args.Relay)
+ window := speedWindow
+ if args.WindowMs > 0 {
+ window = min(time.Duration(args.WindowMs)*time.Millisecond, speedWindowMax)
+ }
+ res, err := a.measureSpeed(ctx, args.Relay, window)
if err != nil {
return proto.TaskResult{Error: err.Error()}
}
@@ -38,7 +44,7 @@ func (a *Agent) doSpeed(ctx context.Context, env proto.TaskEnvelope) proto.TaskR
return proto.TaskResult{OK: true, Data: data}
}
-func (a *Agent) measureSpeed(ctx context.Context, relay bool) (proto.SysSpeedResult, error) {
+func (a *Agent) measureSpeed(ctx context.Context, relay bool, window time.Duration) (proto.SysSpeedResult, error) {
res := proto.SysSpeedResult{Relay: relay}
streams := "/api/agent/speed"
if relay {
@@ -59,14 +65,14 @@ func (a *Agent) measureSpeed(ctx context.Context, relay bool) (proto.SysSpeedRes
}
}
- down, err := a.speedDown(ctx, streams+"/down")
+ down, err := a.speedDown(ctx, streams+"/down", window)
if err != nil {
return res, err
}
res.DownBps = down
// Up: send until the window closes or the cap is hit.
- body := &speedSource{deadline: time.Now().Add(speedWindow), left: speedUpCap}
+ body := &speedSource{deadline: time.Now().Add(window), left: speedUpCap}
start := time.Now()
resp, err := a.speedRequest(ctx, http.MethodPost, streams+"/up", body)
if err != nil {
@@ -78,9 +84,9 @@ func (a *Agent) measureSpeed(ctx context.Context, relay bool) (proto.SysSpeedRes
return res, nil
}
-// speedDown reads what the server streams for speedWindow, counted from the
+// speedDown reads what the server streams for the window, counted from the
// first byte, and returns the rate.
-func (a *Agent) speedDown(ctx context.Context, path string) (float64, error) {
+func (a *Agent) speedDown(ctx context.Context, path string, window time.Duration) (float64, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
t := time.AfterFunc(speedWait, cancel)
@@ -99,7 +105,7 @@ func (a *Agent) speedDown(ctx context.Context, path string) (float64, error) {
k, err := resp.Body.Read(buf)
if k > 0 && start.IsZero() {
start = time.Now()
- t.Reset(speedWindow)
+ t.Reset(window)
}
n += int64(k)
if err != nil {
diff --git a/internal/proto/proto.go b/internal/proto/proto.go
index 30080b0..ca004f9 100644
--- a/internal/proto/proto.go
+++ b/internal/proto/proto.go
@@ -184,6 +184,9 @@ type SysSpeedArgs struct {
// Relay makes the agent use the relayed streams, which the server pipes
// to and from the browser.
Relay bool `json:"relay,omitempty"`
+ // WindowMs is how long to measure each direction for; 0 leaves it to the
+ // agent (3 s, all an agent older than 0.0.7 does).
+ WindowMs int `json:"window_ms,omitempty"`
}
// SysSpeedResult is returned for TaskSysSpeed: the agent's HTTP link to the
diff --git a/internal/server/handlers_panel_stat.go b/internal/server/handlers_panel_stat.go
index f51b9de..4c3df3a 100644
--- a/internal/server/handlers_panel_stat.go
+++ b/internal/server/handlers_panel_stat.go
@@ -20,17 +20,20 @@ const (
// statCacheFor is how long one sample answers every open status page, so
// several tabs cost the agent a single reading.
statCacheFor = 2 * time.Second
+ // speedWindow is how long the agent is asked to measure each direction
+ // for. An agent older than 0.0.7 keeps to its own 3 s.
+ speedWindow = 7 * time.Second
// speedTaskTimeout is how long a speed test may take. One that is not done
// by then is answered with what the server has counted itself.
- speedTaskTimeout = 15 * time.Second
+ speedTaskTimeout = 35 * time.Second
// speedDownCap bounds one download stream; speedDownFor ends it if the
// agent does not hang up first.
- speedDownCap = 256 << 20
- speedDownFor = 6 * time.Second
+ speedDownCap = 1 << 30
+ speedDownFor = 2 * speedWindow
speedUpCap = 64 << 20
// speedRelayUpFor is how long the agent's upload is passed on to the
// browser: the agent's own window with slack.
- speedRelayUpFor = 4 * time.Second
+ speedRelayUpFor = speedWindow + time.Second
)
// statCache holds the last sample per agent and serializes its refresh.
@@ -71,7 +74,7 @@ func (s *Server) handleStatusPage(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
return
}
- s.render(w, "status.html", map[string]any{"Agent": a})
+ s.render(w, "status.html", map[string]any{"Agent": a, "RelayUpMs": speedRelayUpFor.Milliseconds()})
}
// runTask runs a one-shot task on the agent and waits for its result.
@@ -144,7 +147,6 @@ func (s *Server) handleAgentSpeed(w http.ResponseWriter, r *http.Request) {
relay := r.URL.Query().Get("relay") != ""
m := s.meters.start(id)
defer s.meters.stop(id, m)
- var args []byte
if relay {
rl := s.relays.get(id)
if rl == nil {
@@ -152,8 +154,8 @@ func (s *Server) handleAgentSpeed(w http.ResponseWriter, r *http.Request) {
return
}
defer s.relays.drop(id, rl)
- args, _ = json.Marshal(proto.SysSpeedArgs{Relay: true})
}
+ args, _ := json.Marshal(proto.SysSpeedArgs{Relay: relay, WindowMs: int(speedWindow.Milliseconds())})
res, err := s.runTask(id, proto.TaskSysSpeed, args, speedTaskTimeout)
if err != nil {
part, ok := m.result()
diff --git a/internal/server/web/templates/status.html b/internal/server/web/templates/status.html
index 6d90b4f..db0ad05 100644
--- a/internal/server/web/templates/status.html
+++ b/internal/server/web/templates/status.html
@@ -77,7 +77,7 @@
Скорость соединения
Клиент замеряет свой канал до хоста панели - тот же, по которому идут терминал, файлы и http-проброс.
С галочкой «До браузера» данные идут дальше, через панель до этого браузера и обратно: так виден весь путь клиент - панель - браузер.
- Замер занимает около 7 секунд, не дольше 20, и на это время нагружает канал.
+ Замер занимает около 15 секунд, не дольше 40, и на это время нагружает канал.