feat: speed test through the browser, client - panel - browser (0.0.6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
redteaandClaude Opus 5.5 committed 2026-10-13 20:31:41 +03:00
1 parent b51363bf72
commit 87921a1381
9 files changed
+344 -33

No files matched your search

+56 -17
View File
@@ -4,6 +4,7 @@ import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"io"
"net/http"
"time"
@@ -14,14 +15,22 @@ import (
const (
// speedWindow is how long each direction is measured for.
speedWindow = 3 * 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 = 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.
func (a *Agent) doSpeed(ctx context.Context) proto.TaskResult {
res, err := a.measureSpeed(ctx)
// empty request, then how much flows each way in speedWindow. 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)
if err != nil {
return proto.TaskResult{Error: err.Error()}
}
@@ -29,8 +38,12 @@ func (a *Agent) doSpeed(ctx context.Context) proto.TaskResult {
return proto.TaskResult{OK: true, Data: data}
}
func (a *Agent) measureSpeed(ctx context.Context) (proto.SysSpeedResult, error) {
var res proto.SysSpeedResult
func (a *Agent) measureSpeed(ctx context.Context, relay bool) (proto.SysSpeedResult, error) {
res := proto.SysSpeedResult{Relay: relay}
streams := "/api/agent/speed"
if relay {
streams += "/relay"
}
// Ping: the first request may pay for a new connection, so keep the best.
for i := 0; i < 4; i++ {
start := time.Now()
@@ -46,24 +59,16 @@ func (a *Agent) measureSpeed(ctx context.Context) (proto.SysSpeedResult, error)
}
}
// Down: read what the server streams until the window closes.
dctx, cancel := context.WithTimeout(ctx, speedWindow)
resp, err := a.speedRequest(dctx, http.MethodGet, "/api/agent/speed/down", nil)
down, err := a.speedDown(ctx, streams+"/down")
if err != nil {
cancel()
return res, err
}
start := time.Now()
n, _ := io.Copy(io.Discard, resp.Body)
elapsed := time.Since(start)
resp.Body.Close()
cancel()
res.DownBps = bps(n, elapsed)
res.DownBps = down
// Up: send until the window closes or the cap is hit.
body := &speedSource{deadline: time.Now().Add(speedWindow), left: speedUpCap}
start = time.Now()
resp, err = a.speedRequest(ctx, http.MethodPost, "/api/agent/speed/up", body)
start := time.Now()
resp, err := a.speedRequest(ctx, http.MethodPost, streams+"/up", body)
if err != nil {
return res, err
}
@@ -73,6 +78,40 @@ func (a *Agent) measureSpeed(ctx context.Context) (proto.SysSpeedResult, error)
return res, nil
}
// speedDown reads what the server streams for speedWindow, counted from the
// first byte, and returns the rate.
func (a *Agent) speedDown(ctx context.Context, path string) (float64, error) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
t := time.AfterFunc(speedWait, cancel)
defer t.Stop()
resp, err := a.speedRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return 0, err
}
defer resp.Body.Close()
var (
n int64
start time.Time
buf = make([]byte, 64<<10)
)
for {
k, err := resp.Body.Read(buf)
if k > 0 && start.IsZero() {
start = time.Now()
t.Reset(speedWindow)
}
n += int64(k)
if err != nil {
break
}
}
if start.IsZero() {
return 0, errors.New("нет данных для замера")
}
return bps(n, time.Since(start)), nil
}
func (a *Agent) speedRequest(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, a.cfg.Host+path, body)
if err != nil {