package agent import ( "context" "crypto/rand" "encoding/json" "io" "net/http" "time" "github.com/x-grk/longpolling-remote-access/internal/proto" ) const ( // speedWindow is how long each direction is measured for. speedWindow = 3 * 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) if err != nil { return proto.TaskResult{Error: err.Error()} } data, _ := json.Marshal(res) return proto.TaskResult{OK: true, Data: data} } func (a *Agent) measureSpeed(ctx context.Context) (proto.SysSpeedResult, error) { var res proto.SysSpeedResult // Ping: the first request may pay for a new connection, so keep the best. for i := 0; i < 4; i++ { start := time.Now() resp, err := a.speedRequest(ctx, http.MethodGet, "/api/agent/speed/down?bytes=0", nil) if err != nil { return res, err } io.Copy(io.Discard, resp.Body) resp.Body.Close() ms := float64(time.Since(start).Microseconds()) / 1000 if i == 0 || ms < res.PingMs { res.PingMs = ms } } // 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) 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) // 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) if err != nil { return res, err } io.Copy(io.Discard, resp.Body) resp.Body.Close() res.UpBps = bps(int64(speedUpCap-body.left), time.Since(start)) return res, 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 { return nil, err } a.authHeaders(req) resp, err := a.http.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent { resp.Body.Close() return nil, errStatus(resp.StatusCode) } return resp, nil } func bps(n int64, d time.Duration) float64 { if d <= 0 { return 0 } return float64(n) * 8 / d.Seconds() } // speedSource is an upload body of incompressible bytes that ends at the // deadline or after `left` bytes. type speedSource struct { deadline time.Time left int block []byte } func (s *speedSource) Read(p []byte) (int, error) { if s.left <= 0 || !time.Now().Before(s.deadline) { return 0, io.EOF } if s.block == nil { s.block = make([]byte, 64<<10) rand.Read(s.block) } n := copy(p, s.block) if n > s.left { n = s.left } s.left -= n return n, nil }