feat: live reading during the speed test, and a 15 s limit that keeps what was measured
This commit is contained in:
1 parent
6441b4329d
commit
ab8751d436
4 files changed
+305
-32
No files matched your search
@@ -193,4 +193,7 @@ type SysSpeedResult struct {
|
|||||||
DownBps float64 `json:"down_bps"` // bits per second
|
DownBps float64 `json:"down_bps"` // bits per second
|
||||||
UpBps float64 `json:"up_bps"`
|
UpBps float64 `json:"up_bps"`
|
||||||
Relay bool `json:"relay,omitempty"` // the streams went through the relay
|
Relay bool `json:"relay,omitempty"` // the streams went through the relay
|
||||||
|
// Partial marks a test the server cut short: the numbers are what it had
|
||||||
|
// counted on its own side by then.
|
||||||
|
Partial bool `json:"partial,omitempty"`
|
||||||
}
|
}
|
||||||
@@ -20,8 +20,9 @@ const (
|
|||||||
// statCacheFor is how long one sample answers every open status page, so
|
// statCacheFor is how long one sample answers every open status page, so
|
||||||
// several tabs cost the agent a single reading.
|
// several tabs cost the agent a single reading.
|
||||||
statCacheFor = 2 * time.Second
|
statCacheFor = 2 * time.Second
|
||||||
// speedTaskTimeout covers the agent's ping + down + up windows with slack.
|
// speedTaskTimeout is how long a speed test may take. One that is not done
|
||||||
speedTaskTimeout = 30 * time.Second
|
// by then is answered with what the server has counted itself.
|
||||||
|
speedTaskTimeout = 15 * time.Second
|
||||||
// speedDownCap bounds one download stream; speedDownFor ends it if the
|
// speedDownCap bounds one download stream; speedDownFor ends it if the
|
||||||
// agent does not hang up first.
|
// agent does not hang up first.
|
||||||
speedDownCap = 256 << 20
|
speedDownCap = 256 << 20
|
||||||
@@ -141,6 +142,8 @@ func (s *Server) handleAgentSpeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
relay := r.URL.Query().Get("relay") != ""
|
relay := r.URL.Query().Get("relay") != ""
|
||||||
|
m := s.meters.start(id)
|
||||||
|
defer s.meters.stop(id, m)
|
||||||
var args []byte
|
var args []byte
|
||||||
if relay {
|
if relay {
|
||||||
rl := s.relays.get(id)
|
rl := s.relays.get(id)
|
||||||
@@ -153,8 +156,14 @@ func (s *Server) handleAgentSpeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
res, err := s.runTask(id, proto.TaskSysSpeed, args, speedTaskTimeout)
|
res, err := s.runTask(id, proto.TaskSysSpeed, args, speedTaskTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeJSON(w, proto.TaskResult{Error: err.Error()})
|
part, ok := m.result()
|
||||||
return
|
if !ok {
|
||||||
|
writeJSON(w, proto.TaskResult{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
part.Relay = relay
|
||||||
|
data, _ := json.Marshal(part)
|
||||||
|
res = proto.TaskResult{OK: true, Data: data}
|
||||||
}
|
}
|
||||||
res = oldAgentError(res)
|
res = oldAgentError(res)
|
||||||
if relay && res.OK {
|
if relay && res.OK {
|
||||||
@@ -168,6 +177,148 @@ func (s *Server) handleAgentSpeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, res)
|
writeJSON(w, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// speedMeter counts what flows through the streams of one speed test as the
|
||||||
|
// server sees it: for the live reading on the page, and for the result of a
|
||||||
|
// test that ran out of time.
|
||||||
|
type speedMeter struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
start time.Time
|
||||||
|
phase string // "ping", "down", "up"
|
||||||
|
pingAt time.Time
|
||||||
|
pingMs float64
|
||||||
|
down speedCount
|
||||||
|
up speedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
// speedCount is the bytes of one direction and when they went through. The
|
||||||
|
// first piece only starts the clock.
|
||||||
|
type speedCount struct {
|
||||||
|
n, skip int64
|
||||||
|
first, last time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *speedCount) add(n int) {
|
||||||
|
now := time.Now()
|
||||||
|
if c.first.IsZero() {
|
||||||
|
c.first, c.skip = now, int64(n)
|
||||||
|
}
|
||||||
|
c.n += int64(n)
|
||||||
|
c.last = now
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *speedCount) bps() float64 {
|
||||||
|
d := c.last.Sub(c.first).Seconds()
|
||||||
|
if d <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float64(c.n-c.skip) * 8 / d
|
||||||
|
}
|
||||||
|
|
||||||
|
// ping notes one of the agent's ping probes: they come one right after
|
||||||
|
// another, so the gap between two is its round trip.
|
||||||
|
func (m *speedMeter) ping() {
|
||||||
|
if m == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
now := time.Now()
|
||||||
|
if !m.pingAt.IsZero() {
|
||||||
|
if ms := float64(now.Sub(m.pingAt).Microseconds()) / 1000; m.pingMs == 0 || ms < m.pingMs {
|
||||||
|
m.pingMs = ms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m.pingAt = now
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *speedMeter) add(up bool, n int) {
|
||||||
|
if m == nil || n <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.mu.Lock()
|
||||||
|
if up {
|
||||||
|
m.phase = "up"
|
||||||
|
m.up.add(n)
|
||||||
|
} else {
|
||||||
|
m.phase = "down"
|
||||||
|
m.down.add(n)
|
||||||
|
}
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// result is the test as counted so far; false with nothing measured yet.
|
||||||
|
func (m *speedMeter) result() (proto.SysSpeedResult, bool) {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
res := proto.SysSpeedResult{PingMs: m.pingMs, DownBps: m.down.bps(), UpBps: m.up.bps(), Partial: true}
|
||||||
|
return res, res.DownBps > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// meterWriter counts what it passes on; a nil meter counts nothing.
|
||||||
|
type meterWriter struct {
|
||||||
|
w io.Writer
|
||||||
|
m *speedMeter
|
||||||
|
up bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c meterWriter) Write(p []byte) (int, error) {
|
||||||
|
n, err := c.w.Write(p)
|
||||||
|
c.m.add(c.up, n)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// speedMeters holds the meter of each agent under test.
|
||||||
|
type speedMeters struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
m map[string]*speedMeter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *speedMeters) start(agentID string) *speedMeter {
|
||||||
|
m := &speedMeter{start: time.Now(), phase: "ping"}
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.m == nil {
|
||||||
|
c.m = map[string]*speedMeter{}
|
||||||
|
}
|
||||||
|
c.m[agentID] = m
|
||||||
|
c.mu.Unlock()
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *speedMeters) get(agentID string) *speedMeter {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return c.m[agentID]
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop forgets the agent's meter if it is still m.
|
||||||
|
func (c *speedMeters) stop(agentID string, m *speedMeter) {
|
||||||
|
c.mu.Lock()
|
||||||
|
if c.m[agentID] == m {
|
||||||
|
delete(c.m, agentID)
|
||||||
|
}
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/panel/agents/{id}/speed/progress - the bytes counted so far in the
|
||||||
|
// agent's running speed test, for the page to turn into a live rate. An empty
|
||||||
|
// phase means no test is running.
|
||||||
|
func (s *Server) handleSpeedProgress(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var out struct {
|
||||||
|
Phase string `json:"phase"`
|
||||||
|
Down int64 `json:"down"`
|
||||||
|
Up int64 `json:"up"`
|
||||||
|
Ms int64 `json:"ms"` // since the test began, by the server's clock
|
||||||
|
}
|
||||||
|
if m := s.meters.get(r.PathValue("id")); m != nil {
|
||||||
|
m.mu.Lock()
|
||||||
|
out.Phase, out.Down, out.Up = m.phase, m.down.n, m.up.n
|
||||||
|
out.Ms = time.Since(m.start).Milliseconds()
|
||||||
|
m.mu.Unlock()
|
||||||
|
}
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
writeJSON(w, out)
|
||||||
|
}
|
||||||
|
|
||||||
// speedBlock is one chunk of incompressible bytes, made once and sent over and
|
// speedBlock is one chunk of incompressible bytes, made once and sent over and
|
||||||
// over, so a speed test costs the server no CPU beyond the copy.
|
// over, so a speed test costs the server no CPU beyond the copy.
|
||||||
var speedBlock = sync.OnceValue(func() []byte {
|
var speedBlock = sync.OnceValue(func() []byte {
|
||||||
@@ -186,16 +337,21 @@ func speedStreamHeaders(w http.ResponseWriter) {
|
|||||||
// GET /api/agent/speed/down?bytes=N - stream N bytes (default: as much as fits
|
// GET /api/agent/speed/down?bytes=N - stream N bytes (default: as much as fits
|
||||||
// in the time window) for the agent to time. bytes=0 is the ping probe.
|
// in the time window) for the agent to time. bytes=0 is the ping probe.
|
||||||
func (s *Server) handleSpeedDown(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSpeedDown(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.authenticateAgent(r) == nil {
|
a := s.authenticateAgent(r)
|
||||||
|
if a == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
m := s.meters.get(a.ID)
|
||||||
left := speedDownCap
|
left := speedDownCap
|
||||||
if v := r.URL.Query().Get("bytes"); v != "" {
|
if v := r.URL.Query().Get("bytes"); v != "" {
|
||||||
if n, err := strconv.Atoi(v); err == nil && n >= 0 && n < left {
|
if n, err := strconv.Atoi(v); err == nil && n >= 0 && n < left {
|
||||||
left = n
|
left = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if left == 0 {
|
||||||
|
m.ping()
|
||||||
|
}
|
||||||
speedStreamHeaders(w)
|
speedStreamHeaders(w)
|
||||||
block := speedBlock()
|
block := speedBlock()
|
||||||
deadline := time.Now().Add(speedDownFor)
|
deadline := time.Now().Add(speedDownFor)
|
||||||
@@ -208,17 +364,20 @@ func (s *Server) handleSpeedDown(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
m.add(false, n)
|
||||||
left -= n
|
left -= n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/agent/speed/up - swallow the agent's upload.
|
// POST /api/agent/speed/up - swallow the agent's upload.
|
||||||
func (s *Server) handleSpeedUp(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSpeedUp(w http.ResponseWriter, r *http.Request) {
|
||||||
if s.authenticateAgent(r) == nil {
|
a := s.authenticateAgent(r)
|
||||||
|
if a == nil {
|
||||||
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_, _ = io.Copy(io.Discard, http.MaxBytesReader(w, r.Body, speedUpCap))
|
sink := meterWriter{w: io.Discard, m: s.meters.get(a.ID), up: true}
|
||||||
|
_, _ = io.Copy(sink, http.MaxBytesReader(w, r.Body, speedUpCap))
|
||||||
w.WriteHeader(http.StatusNoContent)
|
w.WriteHeader(http.StatusNoContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,13 +482,14 @@ func (s *Server) handleSpeedRelayFeed(w http.ResponseWriter, r *http.Request) {
|
|||||||
// GET /api/panel/agents/{id}/speed/up - the agent's upload, for the browser to
|
// GET /api/panel/agents/{id}/speed/up - the agent's upload, for the browser to
|
||||||
// time. Ends when the agent is done sending.
|
// time. Ends when the agent is done sending.
|
||||||
func (s *Server) handleSpeedRelayDrain(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleSpeedRelayDrain(w http.ResponseWriter, r *http.Request) {
|
||||||
rl := s.relays.get(r.PathValue("id"))
|
id := r.PathValue("id")
|
||||||
|
rl := s.relays.get(id)
|
||||||
if rl == nil {
|
if rl == nil {
|
||||||
http.Error(w, "gone", http.StatusGone)
|
http.Error(w, "gone", http.StatusGone)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
speedStreamHeaders(w)
|
speedStreamHeaders(w)
|
||||||
_, _ = io.Copy(w, rl.upR)
|
_, _ = io.Copy(meterWriter{w: w, m: s.meters.get(id), up: true}, rl.upR)
|
||||||
// The browser may have left: don't let the agent keep sending to nobody.
|
// The browser may have left: don't let the agent keep sending to nobody.
|
||||||
rl.upR.CloseWithError(io.ErrClosedPipe)
|
rl.upR.CloseWithError(io.ErrClosedPipe)
|
||||||
}
|
}
|
||||||
@@ -350,7 +510,7 @@ func (s *Server) handleSpeedRelayDown(w http.ResponseWriter, r *http.Request) {
|
|||||||
speedStreamHeaders(w)
|
speedStreamHeaders(w)
|
||||||
// The agent waits on the headers, the browser may not have sent a byte yet.
|
// The agent waits on the headers, the browser may not have sent a byte yet.
|
||||||
_ = http.NewResponseController(w).Flush()
|
_ = http.NewResponseController(w).Flush()
|
||||||
_, _ = io.Copy(w, rl.downR)
|
_, _ = io.Copy(meterWriter{w: w, m: s.meters.get(a.ID)}, rl.downR)
|
||||||
// The agent hung up at the end of its window: stop the browser's upload.
|
// The agent hung up at the end of its window: stop the browser's upload.
|
||||||
rl.downR.CloseWithError(io.ErrClosedPipe)
|
rl.downR.CloseWithError(io.ErrClosedPipe)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ type Server struct {
|
|||||||
|
|
||||||
stats statCache // last load sample per agent, for the status page
|
stats statCache // last load sample per agent, for the status page
|
||||||
relays speedRelays // speed tests that run through the browser
|
relays speedRelays // speed tests that run through the browser
|
||||||
|
meters speedMeters // byte counts of the speed tests under way
|
||||||
}
|
}
|
||||||
|
|
||||||
// New builds the server, parsing embedded templates.
|
// New builds the server, parsing embedded templates.
|
||||||
@@ -234,6 +235,7 @@ func (s *Server) Handler() http.Handler {
|
|||||||
mux.Handle("GET /api/panel/agents/presence", s.requireAdmin(s.handlePresence))
|
mux.Handle("GET /api/panel/agents/presence", s.requireAdmin(s.handlePresence))
|
||||||
mux.Handle("GET /api/panel/agents/{id}/stat", s.requireAdmin(s.handleAgentStat))
|
mux.Handle("GET /api/panel/agents/{id}/stat", s.requireAdmin(s.handleAgentStat))
|
||||||
mux.Handle("POST /api/panel/agents/{id}/speed", s.requireAdmin(s.handleAgentSpeed))
|
mux.Handle("POST /api/panel/agents/{id}/speed", s.requireAdmin(s.handleAgentSpeed))
|
||||||
|
mux.Handle("GET /api/panel/agents/{id}/speed/progress", s.requireAdmin(s.handleSpeedProgress))
|
||||||
mux.Handle("GET /api/panel/ping", s.requireAdmin(s.handlePanelPing))
|
mux.Handle("GET /api/panel/ping", s.requireAdmin(s.handlePanelPing))
|
||||||
mux.Handle("POST /api/panel/agents/{id}/speed/relay", s.requireAdmin(s.handleSpeedRelayOpen))
|
mux.Handle("POST /api/panel/agents/{id}/speed/relay", s.requireAdmin(s.handleSpeedRelayOpen))
|
||||||
mux.Handle("POST /api/panel/agents/{id}/speed/down", s.requireAdmin(s.handleSpeedRelayFeed))
|
mux.Handle("POST /api/panel/agents/{id}/speed/down", s.requireAdmin(s.handleSpeedRelayFeed))
|
||||||
|
|||||||
@@ -19,6 +19,24 @@
|
|||||||
.speed { display:flex; gap:28px; flex-wrap:wrap; margin-top:16px; }
|
.speed { display:flex; gap:28px; flex-wrap:wrap; margin-top:16px; }
|
||||||
.speed b { display:block; font-size:20px; font-variant-numeric:tabular-nums; }
|
.speed b { display:block; font-size:20px; font-variant-numeric:tabular-nums; }
|
||||||
.speed span { color:var(--muted); font-size:13px; }
|
.speed span { color:var(--muted); font-size:13px; }
|
||||||
|
.live { display:flex; align-items:center; gap:28px; flex-wrap:wrap; margin-top:20px; }
|
||||||
|
.gauge { position:relative; width:240px; flex:none; }
|
||||||
|
.gauge svg { display:block; width:100%; }
|
||||||
|
.gauge path { fill:none; stroke-width:14; stroke-linecap:round; }
|
||||||
|
.gauge .track { stroke:#0c0f13; }
|
||||||
|
.gauge .fill { stroke:var(--accent); stroke-dasharray:251.4; stroke-dashoffset:251.4; transition:stroke-dashoffset .25s linear, stroke .3s; }
|
||||||
|
.gauge .num { position:absolute; left:0; right:0; bottom:26px; text-align:center; font-size:38px; font-weight:600; line-height:1; font-variant-numeric:tabular-nums; }
|
||||||
|
.gauge .unit { position:absolute; left:0; right:0; bottom:4px; text-align:center; color:var(--muted); font-size:13px; }
|
||||||
|
.live .side { flex:1; min-width:220px; }
|
||||||
|
.live .phase { font-size:15px; margin-bottom:8px; }
|
||||||
|
.live .phase i { font-style:normal; color:var(--accent); margin-right:6px; }
|
||||||
|
.live.up .phase i { color:var(--ok); }
|
||||||
|
.live.up .gauge .fill { stroke:var(--ok); }
|
||||||
|
.spark { display:block; width:100%; height:64px; }
|
||||||
|
.spark polyline { fill:none; stroke:var(--accent); stroke-width:2; vector-effect:non-scaling-stroke; stroke-linejoin:round; }
|
||||||
|
.spark polygon { fill:var(--accent); opacity:.15; }
|
||||||
|
.live.up .spark polyline { stroke:var(--ok); }
|
||||||
|
.live.up .spark polygon { fill:var(--ok); }
|
||||||
label.check { display:inline-flex; align-items:center; gap:8px; margin:0; color:var(--text); font-size:14px; cursor:pointer; }
|
label.check { display:inline-flex; align-items:center; gap:8px; margin:0; color:var(--text); font-size:14px; cursor:pointer; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -59,7 +77,7 @@
|
|||||||
<h2>Скорость соединения</h2>
|
<h2>Скорость соединения</h2>
|
||||||
<p class="muted">Клиент замеряет свой канал до хоста панели - тот же, по которому идут терминал, файлы и http-проброс.
|
<p class="muted">Клиент замеряет свой канал до хоста панели - тот же, по которому идут терминал, файлы и http-проброс.
|
||||||
С галочкой «До браузера» данные идут дальше, через панель до этого браузера и обратно: так виден весь путь клиент - панель - браузер.
|
С галочкой «До браузера» данные идут дальше, через панель до этого браузера и обратно: так виден весь путь клиент - панель - браузер.
|
||||||
Замер занимает около 7 секунд и на это время нагружает канал.</p>
|
Замер занимает около 7 секунд, не дольше 20, и на это время нагружает канал.</p>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<button id="speed-btn" type="button">Замерить скорость</button>
|
<button id="speed-btn" type="button">Замерить скорость</button>
|
||||||
<label class="check" title="Данные пойдут не только между клиентом и хостом панели, а через панель до этого браузера и обратно">
|
<label class="check" title="Данные пойдут не только между клиентом и хостом панели, а через панель до этого браузера и обратно">
|
||||||
@@ -67,6 +85,23 @@
|
|||||||
</label>
|
</label>
|
||||||
<span class="muted" id="speed-note"></span>
|
<span class="muted" id="speed-note"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="live" id="live" hidden>
|
||||||
|
<div class="gauge">
|
||||||
|
<svg viewBox="0 0 200 110">
|
||||||
|
<path class="track" d="M20 100 A80 80 0 0 1 180 100"/>
|
||||||
|
<path class="fill" id="lv-arc" d="M20 100 A80 80 0 0 1 180 100"/>
|
||||||
|
</svg>
|
||||||
|
<div class="num" id="lv-num">-</div>
|
||||||
|
<div class="unit" id="lv-unit"></div>
|
||||||
|
</div>
|
||||||
|
<div class="side">
|
||||||
|
<div class="phase"><i id="lv-ico"></i><span id="lv-phase"></span></div>
|
||||||
|
<svg class="spark" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||||
|
<polygon id="lv-area" points=""/>
|
||||||
|
<polyline id="lv-line" points=""/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="speed" id="speed" hidden>
|
<div class="speed" id="speed" hidden>
|
||||||
<div><b id="sp-ping"></b><span>задержка</span></div>
|
<div><b id="sp-ping"></b><span>задержка</span></div>
|
||||||
<div><b id="sp-down"></b><span id="sp-down-lbl"></span></div>
|
<div><b id="sp-down"></b><span id="sp-down-lbl"></span></div>
|
||||||
@@ -85,11 +120,12 @@
|
|||||||
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
while (n >= 1024 && i < u.length - 1) { n /= 1024; i++; }
|
||||||
return n.toFixed(n < 10 && i > 0 ? 1 : 0) + ' ' + u[i];
|
return n.toFixed(n < 10 && i > 0 ? 1 : 0) + ' ' + u[i];
|
||||||
}
|
}
|
||||||
function rate(bps) {
|
function rateParts(bps) {
|
||||||
var u = ['бит/с', 'Кбит/с', 'Мбит/с', 'Гбит/с'], i = 0;
|
var u = ['бит/с', 'Кбит/с', 'Мбит/с', 'Гбит/с'], i = 0;
|
||||||
while (bps >= 1000 && i < u.length - 1) { bps /= 1000; i++; }
|
while (bps >= 1000 && i < u.length - 1) { bps /= 1000; i++; }
|
||||||
return bps.toFixed(bps < 100 && i > 0 ? 1 : 0) + ' ' + u[i];
|
return [bps.toFixed(bps < 100 && i > 0 ? 1 : 0), u[i]];
|
||||||
}
|
}
|
||||||
|
function rate(bps) { return rateParts(bps).join(' '); }
|
||||||
function meter(name, pct, sub) {
|
function meter(name, pct, sub) {
|
||||||
pct = Math.max(0, Math.min(100, pct));
|
pct = Math.max(0, Math.min(100, pct));
|
||||||
$(name + '-val').textContent = Math.round(pct) + '%';
|
$(name + '-val').textContent = Math.round(pct) + '%';
|
||||||
@@ -174,63 +210,135 @@
|
|||||||
}
|
}
|
||||||
one().catch(function () {});
|
one().catch(function () {});
|
||||||
}
|
}
|
||||||
// Times what the client sends, from its first byte to the end of the stream.
|
// Reads what the client sends and times it from the first byte. It stops by
|
||||||
function drain(onFirst) {
|
// itself a little after the client's window, so bytes still queued on a slow
|
||||||
return fetch(base + '/speed/up', {credentials: 'same-origin', cache: 'no-store'}).then(function (r) {
|
// link don't hold the result back. st.n is the count so far, for the live
|
||||||
|
// reading.
|
||||||
|
function drain(st, onFirst) {
|
||||||
|
var ac = new AbortController(), last = 0;
|
||||||
|
st.stop = function () { ac.abort(); };
|
||||||
|
function done() { return st.start && last > st.start ? st.n * 8000 / (last - st.start) : 0; }
|
||||||
|
return fetch(base + '/speed/up', {credentials: 'same-origin', cache: 'no-store', signal: ac.signal}).then(function (r) {
|
||||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||||
var rd = r.body.getReader(), n = 0, start = 0;
|
var rd = r.body.getReader();
|
||||||
function pump() {
|
function pump() {
|
||||||
return rd.read().then(function (c) {
|
return rd.read().then(function (c) {
|
||||||
if (c.done) return start && n ? n * 8000 / Math.max(1, performance.now() - start) : 0;
|
if (c.done) return done();
|
||||||
if (!start) { start = performance.now(); onFirst(); }
|
last = performance.now();
|
||||||
n += c.value.length;
|
if (!st.start) { st.start = last; setTimeout(st.stop, 4000); onFirst(); }
|
||||||
|
else st.n += c.value.length;
|
||||||
return pump();
|
return pump();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return pump();
|
return pump();
|
||||||
});
|
}).catch(function (e) { if (st.start) return done(); throw e; });
|
||||||
}
|
}
|
||||||
// The test through the browser: the client's download is what this page
|
// The test through the browser: the client's download is what this page
|
||||||
// uploads, and its upload is timed here, where it arrives.
|
// uploads, and its upload is timed here, where it arrives.
|
||||||
function relayTest() {
|
function relayTest(st) {
|
||||||
return panelPing().then(function (ping) {
|
return panelPing().then(function (ping) {
|
||||||
return post('/speed/relay').then(function (res) {
|
return post('/speed/relay').then(function (res) {
|
||||||
if (!res.ok) return res;
|
if (!res.ok) return res;
|
||||||
var stop = new AbortController();
|
var stop = new AbortController();
|
||||||
var up = drain(function () { stop.abort(); });
|
var up = drain(st, function () { stop.abort(); });
|
||||||
up.catch(function () {});
|
up.catch(function () {});
|
||||||
feed(stop);
|
feed(stop);
|
||||||
return post('/speed?relay=1').then(function (res) {
|
return post('/speed?relay=1').then(function (res) {
|
||||||
stop.abort();
|
stop.abort();
|
||||||
|
// No upload under way by now: none is coming.
|
||||||
|
if (!res.ok || !st.start) { st.stop(); }
|
||||||
if (!res.ok) return res;
|
if (!res.ok) return res;
|
||||||
return up.then(function (bps) {
|
return up.catch(function () { return 0; }).then(function (bps) {
|
||||||
res.data.ping_ms += ping;
|
res.data.ping_ms += ping;
|
||||||
res.data.up_bps = bps;
|
if (bps) res.data.up_bps = bps;
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
}, function (e) { stop.abort(); throw e; });
|
}, function (e) { stop.abort(); st.stop(); throw e; });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The live reading: the rate over the last second, from the byte counts the
|
||||||
|
// panel keeps for the running test - or, for the client's upload in a test
|
||||||
|
// through the browser, from what has arrived here.
|
||||||
|
var ARC = 251.4, lv = null;
|
||||||
|
function liveStart(relay, st) {
|
||||||
|
lv = {relay: relay, st: st, phase: '', pts: [], hist: [], busy: false};
|
||||||
|
$('live').hidden = false;
|
||||||
|
liveShow('ping', 0);
|
||||||
|
lv.timer = setInterval(liveTick, 200);
|
||||||
|
}
|
||||||
|
function liveStop() {
|
||||||
|
if (!lv) return;
|
||||||
|
clearInterval(lv.timer);
|
||||||
|
lv = null;
|
||||||
|
$('live').hidden = true;
|
||||||
|
}
|
||||||
|
function liveTick() {
|
||||||
|
var cur = lv;
|
||||||
|
if (cur.relay && cur.st.start) { livePoint(cur, 'up', cur.st.n, performance.now()); return; }
|
||||||
|
if (cur.busy) return;
|
||||||
|
cur.busy = true;
|
||||||
|
fetch(base + '/speed/progress', {credentials: 'same-origin', cache: 'no-store'})
|
||||||
|
.then(json)
|
||||||
|
.then(function (d) {
|
||||||
|
if (cur !== lv || !d.phase || (cur.relay && cur.st.start)) return;
|
||||||
|
livePoint(cur, d.phase, d.phase === 'up' ? d.up : d.down, d.ms);
|
||||||
|
})
|
||||||
|
.catch(function () {})
|
||||||
|
.then(function () { cur.busy = false; });
|
||||||
|
}
|
||||||
|
function livePoint(cur, phase, bytes, ms) {
|
||||||
|
if (phase !== cur.phase) { cur.phase = phase; cur.pts = []; cur.hist = []; }
|
||||||
|
if (phase === 'ping') { liveShow(phase, 0); return; }
|
||||||
|
var pts = cur.pts;
|
||||||
|
pts.push([ms, bytes]);
|
||||||
|
while (pts.length > 2 && ms - pts[0][0] > 1000) pts.shift();
|
||||||
|
if (pts.length < 2 || ms <= pts[0][0]) return;
|
||||||
|
var bps = (bytes - pts[0][1]) * 8000 / (ms - pts[0][0]);
|
||||||
|
cur.hist.push(bps);
|
||||||
|
liveShow(phase, bps, cur);
|
||||||
|
}
|
||||||
|
function liveShow(phase, bps, cur) {
|
||||||
|
var relay = lv && lv.relay, p = rateParts(bps);
|
||||||
|
$('live').className = 'live' + (phase === 'up' ? ' up' : '');
|
||||||
|
$('lv-ico').textContent = phase === 'up' ? '↑' : phase === 'down' ? '↓' : '';
|
||||||
|
$('lv-phase').textContent = phase === 'ping' ? 'Задержка…'
|
||||||
|
: phase === 'up' ? (relay ? 'клиент → браузер' : 'клиент → хост')
|
||||||
|
: (relay ? 'браузер → клиент' : 'хост → клиент');
|
||||||
|
$('lv-num').textContent = phase === 'ping' ? '…' : p[0];
|
||||||
|
$('lv-unit').textContent = phase === 'ping' ? '' : p[1];
|
||||||
|
// A log scale up to 1 Гбит/с, so a slow link moves the needle too.
|
||||||
|
var f = Math.min(1, Math.log(1 + bps / 1e6) / Math.log(1001));
|
||||||
|
$('lv-arc').style.strokeDashoffset = ARC * (1 - f);
|
||||||
|
var h = cur ? cur.hist : [], max = 1.25 * Math.max.apply(null, h.concat(1)), line = '';
|
||||||
|
for (var i = 0; i < h.length; i++) {
|
||||||
|
line += (h.length > 1 ? 100 * i / (h.length - 1) : 0).toFixed(1) + ',' + (98 - 94 * h[i] / max).toFixed(1) + ' ';
|
||||||
|
}
|
||||||
|
$('lv-line').setAttribute('points', line);
|
||||||
|
$('lv-area').setAttribute('points', h.length > 1 ? '0,100 ' + line + '100,100' : '');
|
||||||
|
}
|
||||||
|
|
||||||
btn.onclick = function () {
|
btn.onclick = function () {
|
||||||
var relay = $('speed-relay').checked;
|
var relay = $('speed-relay').checked, st = {n: 0, start: 0, stop: function () {}};
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
note.textContent = 'Идёт замер…';
|
note.textContent = 'Идёт замер…';
|
||||||
(relay ? relayTest() : post('/speed'))
|
$('speed').hidden = true;
|
||||||
|
liveStart(relay, st);
|
||||||
|
(relay ? relayTest(st) : post('/speed'))
|
||||||
.then(function (res) {
|
.then(function (res) {
|
||||||
if (!res.ok) { note.textContent = res.error || 'ошибка'; return; }
|
if (!res.ok) { note.textContent = res.error || 'ошибка'; return; }
|
||||||
var d = res.data;
|
var d = res.data;
|
||||||
$('sp-ping').textContent = d.ping_ms.toFixed(d.ping_ms < 10 ? 1 : 0) + ' мс';
|
$('sp-ping').textContent = d.ping_ms ? d.ping_ms.toFixed(d.ping_ms < 10 ? 1 : 0) + ' мс' : '-';
|
||||||
$('sp-down').textContent = rate(d.down_bps);
|
$('sp-down').textContent = rate(d.down_bps);
|
||||||
$('sp-up').textContent = rate(d.up_bps);
|
$('sp-up').textContent = d.up_bps ? rate(d.up_bps) : '-';
|
||||||
$('sp-down-lbl').textContent = relay ? 'браузер → клиент' : 'хост → клиент';
|
$('sp-down-lbl').textContent = relay ? 'браузер → клиент' : 'хост → клиент';
|
||||||
$('sp-up-lbl').textContent = relay ? 'клиент → браузер' : 'клиент → хост';
|
$('sp-up-lbl').textContent = relay ? 'клиент → браузер' : 'клиент → хост';
|
||||||
$('speed').hidden = false;
|
$('speed').hidden = false;
|
||||||
note.textContent = '';
|
note.textContent = d.partial ? 'Замер остановлен по времени - показано то, что успело набраться' : '';
|
||||||
})
|
})
|
||||||
.catch(function (e) { note.textContent = 'Не удалось: ' + e.message; })
|
.catch(function (e) { note.textContent = 'Не удалось: ' + e.message; })
|
||||||
.then(function () { btn.disabled = false; });
|
.then(function () { liveStop(); btn.disabled = false; });
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in new issue
Block a user