├── .go-version
├── .gitignore
├── docs
├── release.md
├── saving_mode_experimental_result.md
└── experimental_result.md
├── go.mod
├── speedtest
├── debug.go
├── internal
│ ├── README.md
│ ├── welford.go
│ └── welford_test.go
├── user_test.go
├── output.go
├── unit_test.go
├── transport
│ ├── udp.go
│ └── tcp.go
├── user.go
├── speedtest_test.go
├── data_manager_test.go
├── unit.go
├── request_test.go
├── location.go
├── loss.go
├── server_test.go
├── speedtest.go
├── server.go
├── request.go
└── data_manager.go
├── .github
└── workflows
│ ├── release.yml
│ └── ci.yaml
├── example
├── multi
│ └── main.go
├── naive
│ └── main.go
└── packet_loss
│ └── main.go
├── LICENSE
├── Dockerfile
├── .goreleaser.yml
├── go.sum
├── task.go
├── speedtest.go
└── README.md
/.go-version:
--------------------------------------------------------------------------------
1 | 1.16.5
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 | .goxc.local.json
26 | dist/*
27 | speedtest-go
28 |
29 | .idea/
30 | .run/
--------------------------------------------------------------------------------
/docs/release.md:
--------------------------------------------------------------------------------
1 | # Release flow
2 |
3 | This is a note for repo owner.
4 |
5 | ```bach
6 | $ git checkout -b release/vX.Y.Z
7 | # edit `var version` at speedtest/speedtest.go
8 | $ git commit -am 'Release vX.Y.Z'
9 | $ git push origin release/vX.Y.Z
10 | ```
11 |
12 | merge PR
13 |
14 | ```bach
15 | $ git checkout master
16 | $ git pull origin master
17 | $ git tag vX.Y.Z
18 | $ git push origin vX.Y.Z
19 | # run GitHub Action to build packages and make release.
20 | ```
21 |
22 | update brew formula
23 | https://github.com/showwin/homebrew-speedtest/blob/master/speedtest.rb#L3
24 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/showwin/speedtest-go
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/chelnak/ysmrr v0.5.0
7 | gopkg.in/alecthomas/kingpin.v2 v2.2.6
8 | )
9 |
10 | require (
11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
12 | github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
13 | github.com/fatih/color v1.18.0 // indirect
14 | github.com/mattn/go-colorable v0.1.13 // indirect
15 | github.com/mattn/go-isatty v0.0.20 // indirect
16 | golang.org/x/sys v0.28.0 // indirect
17 | golang.org/x/term v0.27.0 // indirect
18 | )
19 |
--------------------------------------------------------------------------------
/speedtest/debug.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "log"
5 | "os"
6 | )
7 |
8 | type Debug struct {
9 | dbg *log.Logger
10 | flag bool
11 | }
12 |
13 | func NewDebug() *Debug {
14 | return &Debug{dbg: log.New(os.Stdout, "[DBG]", log.Ldate|log.Ltime)}
15 | }
16 |
17 | func (d *Debug) Enable() {
18 | d.flag = true
19 | }
20 |
21 | func (d *Debug) Println(v ...any) {
22 | if d.flag {
23 | d.dbg.Println(v...)
24 | }
25 | }
26 |
27 | func (d *Debug) Printf(format string, v ...any) {
28 | if d.flag {
29 | d.dbg.Printf(format, v...)
30 | }
31 | }
32 |
33 | var dbg = NewDebug()
34 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: goreleaser
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | jobs:
9 | goreleaser:
10 | runs-on: ubuntu-latest
11 | steps:
12 | -
13 | name: Checkout
14 | uses: actions/checkout@v3
15 | with:
16 | fetch-depth: 0
17 | -
18 | name: Set up Go
19 | uses: actions/setup-go@v3
20 | with:
21 | go-version: 1.23.4
22 | -
23 | name: Run GoReleaser
24 | uses: goreleaser/goreleaser-action@v4
25 | with:
26 | distribution: goreleaser
27 | version: latest
28 | args: release --clean
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 |
--------------------------------------------------------------------------------
/example/multi/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/showwin/speedtest-go/speedtest"
7 | "log"
8 | )
9 |
10 | func main() {
11 | serverList, _ := speedtest.FetchServers()
12 | targets, _ := serverList.FindServer([]int{})
13 |
14 | if len(targets) > 0 {
15 | // Use s as main server and use targets as auxiliary servers.
16 | // The main server is loaded at a greater proportion than the auxiliary servers.
17 | s := targets[0]
18 | checkError(s.MultiDownloadTestContext(context.TODO(), targets))
19 | checkError(s.MultiUploadTestContext(context.TODO(), targets))
20 | fmt.Printf("Download: %s, Upload: %s\n", s.DLSpeed, s.ULSpeed)
21 | }
22 | }
23 |
24 | func checkError(err error) {
25 | if err != nil {
26 | log.Fatal(err)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/docs/saving_mode_experimental_result.md:
--------------------------------------------------------------------------------
1 | # --saving-mode Experimental Result
2 |
3 | `--saving-mode` vs normal mode.
4 |
5 | `N - Download` means normal mode download speed results.
6 | `S - RAM` means `--saving-mode` memory usage.
7 |
8 | | exp No. | N - Download | N - Upload | N - RAM | S - Download | S - Upload | S- RAM |
9 | | :-- | :--: | :--: | :--: | :--: | :--: | :--: |
10 | | 1 | 54.13 Mbit/s | 61.07 Mbit/s | 85.82MB | 50.65 Mbit/s | 29.25 Mbit/s | 5.39MB |
11 | | 2 | 47.65 Mbit/s | 57.94 Mbit/s | 93.84MB | 48.39 Mbit/s | 44.04 Mbit/s | 9.39MB |
12 | | 3 | 52.58 Mbit/s | 56.43 Mbit/s | 109.86MB | 24.34 Mbit/s | 41.68 Mbit/s | 9.39MB |
13 | | 4 | 43.73 Mbit/s | 49.46 Mbit/s | 109.86MB | 32.10 Mbit/s | 36.51 Mbit/s | 9.39MB |
14 | | 5 | 54.66 Mbit/s | 42.81 Mbit/s | 101.84MB | 40.08 Mbit/s | 10.37 Mbit/s | 5.39MB |
15 |
--------------------------------------------------------------------------------
/speedtest/internal/README.md:
--------------------------------------------------------------------------------
1 | # Issue #192
2 |
3 |
4 |
5 | 1. Use welford alg to quickly calculate standard deviation and mean.
6 | 2. The welford alg integrated moving window feature, This allows us to ignore early data with excessive volatility.
7 | 3. Use the coefficient of variation(c.v) to reflect the confidence of the test result datasets.
8 | 4. When the data becomes stable(converge), the c.v value will become smaller. When the c.v < 0.05, we terminate this test. We set the tolerance condition as the window buffer being more than half filled and triggering more than five times with c.v < 0.05.
9 | 5. Perform EWMA operation on real-time global average, and use c.v as part of the EWMA feedback parameter.
10 | 6. The ewma value calculated is the result value of our test.
11 | 7. When the test data converge quickly, we can stop early and speed up the testing process. Of course this depends on network/device conditions.
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 ITO Shogo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/speedtest/user_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "strconv"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | func TestFetchUserInfo(t *testing.T) {
10 | client := New()
11 |
12 | user, err := client.FetchUserInfo()
13 | if err != nil {
14 | t.Errorf(err.Error())
15 | }
16 | if user == nil {
17 | t.Error("empty user info")
18 | return
19 | }
20 | // IP
21 | if len(user.IP) < 7 || len(user.IP) > 15 {
22 | t.Errorf("invalid IP length. got: %v;", user.IP)
23 | }
24 | if strings.Count(user.IP, ".") != 3 {
25 | t.Errorf("invalid IP format. got: %v", user.IP)
26 | }
27 |
28 | // Lat
29 | lat, err := strconv.ParseFloat(user.Lat, 64)
30 | if err != nil {
31 | t.Errorf(err.Error())
32 | }
33 | if lat < -90 || 90 < lat {
34 | t.Errorf("invalid Latitude. got: %v, expected between -90 and 90", user.Lat)
35 | }
36 |
37 | // Lon
38 | lon, err := strconv.ParseFloat(user.Lon, 64)
39 | if err != nil {
40 | t.Errorf(err.Error())
41 | }
42 | if lon < -180 || 180 < lon {
43 | t.Errorf("invalid Longitude. got: %v, expected between -180 and 180", user.Lon)
44 | }
45 |
46 | // Isp
47 | if len(user.Isp) == 0 {
48 | t.Errorf("invalid Iso. got: %v;", user.Isp)
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/speedtest/output.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "time"
7 | )
8 |
9 | type fullOutput struct {
10 | Timestamp outputTime `json:"timestamp"`
11 | UserInfo *User `json:"user_info"`
12 | Servers Servers `json:"servers"`
13 | }
14 |
15 | type singleServerOutput struct {
16 | Timestamp outputTime `json:"timestamp"`
17 | UserInfo *User `json:"user_info"`
18 | Server *Server `json:"server"`
19 | }
20 |
21 | type outputTime time.Time
22 |
23 | func (t outputTime) MarshalJSON() ([]byte, error) {
24 | stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("2006-01-02 15:04:05.000"))
25 | return []byte(stamp), nil
26 | }
27 |
28 | func (s *Speedtest) JSON(servers Servers) ([]byte, error) {
29 | return json.Marshal(
30 | fullOutput{
31 | Timestamp: outputTime(time.Now()),
32 | UserInfo: s.User,
33 | Servers: servers,
34 | },
35 | )
36 | }
37 |
38 | // JSONL outputs a single server result in JSON format
39 | func (s *Speedtest) JSONL(server *Server) ([]byte, error) {
40 | return json.Marshal(
41 | singleServerOutput{
42 | Timestamp: outputTime(time.Now()),
43 | UserInfo: s.User,
44 | Server: server,
45 | },
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | # Build stage
4 | FROM --platform=$BUILDPLATFORM golang:1.24.2-alpine AS builder
5 |
6 | WORKDIR /app
7 |
8 | # Install build dependencies
9 | RUN apk add --no-cache git
10 |
11 | # Copy go mod files
12 | COPY go.mod go.sum ./
13 | RUN go mod download
14 |
15 | # Copy source code
16 | COPY . .
17 |
18 | # Build the application
19 | ARG TARGETPLATFORM
20 | ARG BUILDPLATFORM
21 | RUN GOOS=$(echo $TARGETPLATFORM | cut -d/ -f1) \
22 | GOARCH=$(echo $TARGETPLATFORM | cut -d/ -f2) \
23 | go build -o speedtest-go
24 |
25 | # Final stage
26 | FROM alpine:latest
27 |
28 | # Install bash
29 | RUN apk add --no-cache bash
30 |
31 | # Create non-root user
32 | RUN adduser -D -h /home/speedtest speedtest
33 |
34 | WORKDIR /home/speedtest
35 |
36 | # Copy the binary from builder
37 | COPY --from=builder /app/speedtest-go /usr/local/bin/
38 |
39 | # Switch to non-root user
40 | USER speedtest
41 |
42 | # Set default shell
43 | SHELL ["/bin/bash", "-c"]
44 |
45 | # Set the entrypoint to bash, we do this rather than using the speedtest command directly
46 | # such that you can also use this container in an interactive way to run speedtests.
47 | # see the README for more info and examples.
48 | CMD ["/bin/bash"]
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: go-ci
2 |
3 | on: [push]
4 |
5 | jobs:
6 | setup:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: set up
10 | uses: actions/setup-go@v3
11 | with:
12 | go-version: ^1.19
13 | id: go
14 | - name: check out
15 | uses: actions/checkout@v3
16 | - name: Cache
17 | uses: actions/cache@v4
18 | with:
19 | path: ~/go/pkg/mod
20 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
21 | restore-keys: |
22 | ${{ runner.os }}-go-
23 |
24 | build:
25 | needs: setup
26 | runs-on: ubuntu-latest
27 | steps:
28 | - uses: actions/checkout@v3
29 | - name: build
30 | run: go build ./...
31 |
32 | test:
33 | needs: setup
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: actions/checkout@v3
37 | - name: test
38 | run: go test ./speedtest -v
39 |
40 | lint:
41 | needs: setup
42 | runs-on: ubuntu-latest
43 | steps:
44 | - uses: actions/setup-go@v3
45 | with:
46 | go-version: 1.19
47 | - uses: actions/checkout@v3
48 | - name: golangci-lint
49 | uses: golangci/golangci-lint-action@v3
50 | with:
51 | version: v1.53.2
52 |
--------------------------------------------------------------------------------
/example/naive/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/showwin/speedtest-go/speedtest"
6 | "log"
7 | )
8 |
9 | func main() {
10 | // _, _ = speedtest.FetchUserInfo()
11 | // Get a list of servers near a specified location
12 | // user.SetLocationByCity("Tokyo")
13 | // user.SetLocation("Osaka", 34.6952, 135.5006)
14 |
15 | // Select a network card as the data interface.
16 | // speedtest.WithUserConfig(&speedtest.UserConfig{Source: "192.168.1.101"})(speedtestClient)
17 |
18 | // Search server using serverID.
19 | // eg: fetch server with ID 28910.
20 | // speedtest.ErrEmptyServers will be returned if the server cannot be found.
21 | // server, err := speedtest.FetchServerByID("28910")
22 |
23 | serverList, _ := speedtest.FetchServers()
24 | targets, _ := serverList.FindServer([]int{})
25 |
26 | for _, s := range targets {
27 | // Please make sure your host can access this test server,
28 | // otherwise you will get an error.
29 | // It is recommended to replace a server at this time
30 | checkError(s.PingTest(nil))
31 | checkError(s.DownloadTest())
32 | checkError(s.UploadTest())
33 |
34 | // Note: The unit of s.DLSpeed, s.ULSpeed is bytes per second, this is a float64.
35 | fmt.Printf("Latency: %s, Download: %s, Upload: %s\n", s.Latency, s.DLSpeed, s.ULSpeed)
36 | s.Context.Reset()
37 | }
38 | }
39 |
40 | func checkError(err error) {
41 | if err != nil {
42 | log.Fatal(err)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | # This is an example goreleaser.yaml file with some sane defaults.
2 | # Make sure to check the documentation at http://goreleaser.com
3 | before:
4 | hooks:
5 | # You may remove this if you don't use go modules.
6 | - go mod download
7 | # you may remove this if you don't need go generate
8 | - go generate ./...
9 | builds:
10 | - env:
11 | - CGO_ENABLED=0
12 | goos:
13 | - linux
14 | - windows
15 | - darwin
16 | - openbsd
17 | - freebsd
18 | goarch:
19 | - amd64
20 | - arm
21 | - arm64
22 | - 386
23 | - s390x
24 | - ppc64
25 | - ppc64le
26 | - riscv64
27 | - mips
28 | - mips64
29 | - mipsle
30 | - mips64le
31 | - loong64
32 | goarm:
33 | - 5
34 | - 6
35 | - 7
36 | gomips:
37 | - hardfloat
38 | - softfloat
39 | ldflags:
40 | - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }}
41 | archives:
42 | - name_template: >-
43 | {{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "openbsd" }}OpenBSD{{ else }}{{ title .Os }}{{ end }}_{{ if eq .Arch "386" }}i386{{ else if eq .Arch "amd64" }}x86_64{{ else }}{{ .Arch }}{{ end }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
44 |
45 | checksum:
46 | name_template: 'checksums.txt'
47 | snapshot:
48 | name_template: "{{ .Tag }}-next"
49 | changelog:
50 | sort: asc
51 | filters:
52 | exclude:
53 | - '^docs:'
54 | - '^test:'
55 |
--------------------------------------------------------------------------------
/speedtest/unit_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func BenchmarkFmt(b *testing.B) {
8 | bt := ByteRate(1002031.0)
9 | for i := 0; i < b.N; i++ {
10 | _ = bt.Byte(UnitTypeDecimalBits)
11 | }
12 | }
13 |
14 | func BenchmarkDefaultFmt(b *testing.B) {
15 | bt := ByteRate(1002031.0)
16 | for i := 0; i < b.N; i++ {
17 | _ = bt.String()
18 | }
19 | }
20 |
21 | func TestFmt(t *testing.T) {
22 | testData := []struct {
23 | rate ByteRate
24 | format string
25 | t UnitType
26 | }{
27 | {123123123.123, "984.98 Mbps", UnitTypeDecimalBits},
28 | {1231231231.123, "9.85 Gbps", UnitTypeDecimalBits},
29 | {123123.123, "984.98 Kbps", UnitTypeDecimalBits},
30 | {123.1, "984.80 bps", UnitTypeDecimalBits},
31 |
32 | {123123123.123, "123.12 MB/s", UnitTypeDecimalBytes},
33 | {1231231231.123, "1.23 GB/s", UnitTypeDecimalBytes},
34 | {123123.123, "123.12 KB/s", UnitTypeDecimalBytes},
35 | {123.1, "123.10 B/s", UnitTypeDecimalBytes},
36 |
37 | {123123123.123, "939.35 KiMbps", UnitTypeBinaryBits},
38 | {1231231231.123, "9.17 KiGbps", UnitTypeBinaryBits},
39 | {123123.123, "961.90 Kibps", UnitTypeBinaryBits},
40 | {123.1, "0.96 Kibps", UnitTypeBinaryBits},
41 |
42 | {123123123.123, "117.42 MiB/s", UnitTypeBinaryBytes},
43 | {1231231231.123, "1.15 GiB/s", UnitTypeBinaryBytes},
44 | {123123.123, "120.24 KiB/s", UnitTypeBinaryBytes},
45 | {123.1, "0.12 KiB/s", UnitTypeBinaryBytes},
46 |
47 | {-1, "N/A", UnitTypeBinaryBytes},
48 | {0, "0.00 Mbps", UnitTypeDecimalBits},
49 | }
50 |
51 | if testData[0].rate.String() != testData[0].format {
52 | t.Errorf("got: %s, want: %s", testData[0].rate.String(), testData[0].format)
53 | }
54 |
55 | for _, v := range testData {
56 | if got := v.rate.Byte(v.t); got != v.format {
57 | t.Errorf("got: %s, want: %s", got, v.format)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/speedtest/transport/udp.go:
--------------------------------------------------------------------------------
1 | package transport
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "crypto/rand"
7 | "fmt"
8 | mrand "math/rand"
9 | "net"
10 | "strconv"
11 | "strings"
12 | "time"
13 | )
14 |
15 | var (
16 | loss = []byte{0x4c, 0x4f, 0x53, 0x53}
17 | )
18 |
19 | type PacketLossSender struct {
20 | ID string // UUID
21 | nounce int64 // Random int (maybe) [0,10000000000)
22 | withTimestamp bool // With timestamp (ten seconds level)
23 | conn net.Conn // UDP Conn
24 | raw []byte
25 | host string
26 | dialer *net.Dialer
27 | }
28 |
29 | func NewPacketLossSender(uuid string, dialer *net.Dialer) (*PacketLossSender, error) {
30 | rd := mrand.New(mrand.NewSource(time.Now().UnixNano()))
31 | nounce := rd.Int63n(10000000000)
32 | p := &PacketLossSender{
33 | ID: strings.ToUpper(uuid),
34 | nounce: nounce,
35 | withTimestamp: false, // we close it as default, we won't be able to use it right now.
36 | dialer: dialer,
37 | }
38 | p.raw = []byte(fmt.Sprintf("%s %d %s %s", loss, nounce, "#", uuid))
39 | return p, nil
40 | }
41 |
42 | func (ps *PacketLossSender) Connect(ctx context.Context, host string) (err error) {
43 | ps.host = host
44 | ps.conn, err = ps.dialer.DialContext(ctx, "udp", ps.host)
45 | return err
46 | }
47 |
48 | // Send
49 | // @param order the value will be sent
50 | func (ps *PacketLossSender) Send(order int) error {
51 | payload := bytes.Replace(ps.raw, []byte{0x23}, []byte(strconv.Itoa(order)), 1)
52 | _, err := ps.conn.Write(payload)
53 | return err
54 | }
55 |
56 | func generateUUID() (string, error) {
57 | randUUID := make([]byte, 16)
58 | _, err := rand.Read(randUUID)
59 | if err != nil {
60 | return "", err
61 | }
62 | randUUID[8] = randUUID[8]&^0xc0 | 0x80
63 | randUUID[6] = randUUID[6]&^0xf0 | 0x40
64 | return fmt.Sprintf("%x-%x-%x-%x-%x", randUUID[0:4], randUUID[4:6], randUUID[6:8], randUUID[8:10], randUUID[10:]), nil
65 | }
66 |
--------------------------------------------------------------------------------
/speedtest/user.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "encoding/xml"
6 | "errors"
7 | "fmt"
8 | "net/http"
9 | )
10 |
11 | const speedTestConfigUrl = "https://www.speedtest.net/speedtest-config.php"
12 |
13 | // User represents information determined about the caller by speedtest.net
14 | type User struct {
15 | IP string `xml:"ip,attr"`
16 | Lat string `xml:"lat,attr"`
17 | Lon string `xml:"lon,attr"`
18 | Isp string `xml:"isp,attr"`
19 | }
20 |
21 | // Users for decode xml
22 | type Users struct {
23 | Users []User `xml:"client"`
24 | }
25 |
26 | // FetchUserInfo returns information about caller determined by speedtest.net
27 | func (s *Speedtest) FetchUserInfo() (*User, error) {
28 | return s.FetchUserInfoContext(context.Background())
29 | }
30 |
31 | // FetchUserInfo returns information about caller determined by speedtest.net
32 | func FetchUserInfo() (*User, error) {
33 | return defaultClient.FetchUserInfo()
34 | }
35 |
36 | // FetchUserInfoContext returns information about caller determined by speedtest.net, observing the given context.
37 | func (s *Speedtest) FetchUserInfoContext(ctx context.Context) (*User, error) {
38 | dbg.Printf("Retrieving user info: %s\n", speedTestConfigUrl)
39 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, speedTestConfigUrl, nil)
40 | if err != nil {
41 | return nil, err
42 | }
43 |
44 | resp, err := s.doer.Do(req)
45 | if err != nil {
46 | return nil, err
47 | }
48 |
49 | defer resp.Body.Close()
50 |
51 | // Decode xml
52 | decoder := xml.NewDecoder(resp.Body)
53 |
54 | var users Users
55 | if err = decoder.Decode(&users); err != nil {
56 | return nil, err
57 | }
58 |
59 | if len(users.Users) == 0 {
60 | return nil, errors.New("failed to fetch user information")
61 | }
62 |
63 | s.User = &users.Users[0]
64 | return s.User, nil
65 | }
66 |
67 | // FetchUserInfoContext returns information about caller determined by speedtest.net, observing the given context.
68 | func FetchUserInfoContext(ctx context.Context) (*User, error) {
69 | return defaultClient.FetchUserInfoContext(ctx)
70 | }
71 |
72 | // String representation of User
73 | func (u *User) String() string {
74 | extInfo := ""
75 | return fmt.Sprintf("%s (%s) [%s, %s] %s", u.IP, u.Isp, u.Lat, u.Lon, extInfo)
76 | }
77 |
--------------------------------------------------------------------------------
/example/packet_loss/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/showwin/speedtest-go/speedtest"
6 | "github.com/showwin/speedtest-go/speedtest/transport"
7 | "log"
8 | "sync"
9 | "time"
10 | )
11 |
12 | // Note: The current packet loss analyzer does not support udp over http.
13 | // This means we cannot get packet loss through a proxy.
14 | func main() {
15 | // 0. Fetching servers
16 | serverList, err := speedtest.FetchServers()
17 | checkError(err)
18 |
19 | // 1. Retrieve available servers
20 | targets := serverList.Available()
21 |
22 | // 2. Create a packet loss analyzer, use default options
23 | analyzer := speedtest.NewPacketLossAnalyzer(&speedtest.PacketLossAnalyzerOptions{
24 | PacketSendingInterval: time.Millisecond * 100,
25 | })
26 |
27 | wg := &sync.WaitGroup{}
28 | // 3. Perform packet loss analysis on all available servers
29 | for _, server := range *targets {
30 | wg.Add(1)
31 | //ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
32 | //go func(server *speedtest.Server, analyzer *speedtest.PacketLossAnalyzer, ctx context.Context, cancel context.CancelFunc) {
33 | go func(server *speedtest.Server, analyzer *speedtest.PacketLossAnalyzer) {
34 | //defer cancel()
35 | defer wg.Done()
36 | // Note: Please call ctx.cancel at the appropriate time to release resources if you use analyzer.RunWithContext
37 | // we using analyzer.Run() here.
38 | err = analyzer.Run(server.Host, func(packetLoss *transport.PLoss) {
39 | fmt.Println(packetLoss, server.Host, server.Name)
40 | })
41 | //err = analyzer.RunWithContext(ctx, server.Host, func(packetLoss *transport.PLoss) {
42 | // fmt.Println(packetLoss, server.Host, server.Name)
43 | //})
44 | if err != nil {
45 | fmt.Println(err)
46 | }
47 | //}(server, analyzer, ctx, cancel)
48 | }(server, analyzer)
49 | }
50 | wg.Wait()
51 |
52 | // use mixed PacketLoss
53 | mixed, err := analyzer.RunMulti(serverList.Hosts())
54 | checkError(err)
55 | fmt.Printf("Mixed packets lossed: %.2f%%\n", mixed.LossPercent())
56 | fmt.Printf("Mixed packets lossed: %.2f\n", mixed.Loss())
57 | fmt.Printf("Mixed packets lossed: %s\n", mixed)
58 | }
59 |
60 | func checkError(err error) {
61 | if err != nil {
62 | log.Fatal(err)
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/speedtest/speedtest_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 | )
8 |
9 | func BenchmarkLogSpeed(b *testing.B) {
10 | s := New()
11 | config := &UserConfig{
12 | UserAgent: DefaultUserAgent,
13 | Debug: false,
14 | }
15 | WithUserConfig(config)(s)
16 | for i := 0; i < b.N; i++ {
17 | dbg.Printf("hello %s\n", "s20080123") // ~1ns/op
18 | }
19 | }
20 |
21 | func TestNew(t *testing.T) {
22 | t.Run("DefaultDoer", func(t *testing.T) {
23 | c := New()
24 |
25 | if c.doer == nil {
26 | t.Error("doer is nil by")
27 | }
28 | })
29 |
30 | t.Run("CustomDoer", func(t *testing.T) {
31 | doer := &http.Client{}
32 |
33 | c := New(WithDoer(doer))
34 | if c.doer != doer {
35 | t.Error("doer is not the same")
36 | }
37 | })
38 | }
39 |
40 | func TestUserAgent(t *testing.T) {
41 | testServer := func(expectedUserAgent string) *httptest.Server {
42 | return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
43 | if r.UserAgent() == "" {
44 | t.Error("did not receive User-Agent header")
45 | } else if r.UserAgent() != expectedUserAgent {
46 | t.Errorf("incorrect User-Agent header: %s, expected: %s", r.UserAgent(), expectedUserAgent)
47 | }
48 | }))
49 | }
50 |
51 | t.Run("DefaultUserAgent", func(t *testing.T) {
52 | c := New(WithUserConfig(&UserConfig{UserAgent: DefaultUserAgent}))
53 | s := testServer(DefaultUserAgent)
54 | _, err := c.doer.Get(s.URL)
55 | if err != nil {
56 | t.Errorf(err.Error())
57 | }
58 | })
59 |
60 | t.Run("CustomUserAgent", func(t *testing.T) {
61 | testAgent := "1234"
62 | s := testServer(testAgent)
63 | c := New(WithUserConfig(&UserConfig{UserAgent: testAgent}))
64 | _, err := c.doer.Get(s.URL)
65 | if err != nil {
66 | t.Errorf(err.Error())
67 | }
68 | })
69 |
70 | // Test that With
71 | t.Run("CustomUserAgentAndDoer", func(t *testing.T) {
72 | testAgent := "4321"
73 | doer := &http.Client{}
74 | s := testServer(testAgent)
75 | c := New(WithDoer(doer), WithUserConfig(&UserConfig{UserAgent: testAgent}))
76 | if c.doer != doer {
77 | t.Error("doer is not the same")
78 | }
79 | _, err := c.doer.Get(s.URL)
80 | if err != nil {
81 | t.Errorf(err.Error())
82 | }
83 | })
84 | }
85 |
--------------------------------------------------------------------------------
/speedtest/data_manager_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "fmt"
5 | "sync"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func BenchmarkDataManager_NewDataChunk(b *testing.B) {
11 | dmp := NewDataManager()
12 | dmp.Reset()
13 | for i := 0; i < b.N; i++ {
14 | dmp.NewChunk()
15 | }
16 | }
17 |
18 | func BenchmarkDataManager_AddTotalDownload(b *testing.B) {
19 | dmp := NewDataManager()
20 | for i := 0; i < b.N; i++ {
21 | dmp.AddTotalDownload(43521)
22 | }
23 | }
24 |
25 | func TestDataManager_AddTotalDownload(t *testing.T) {
26 | dmp := NewDataManager()
27 | wg := sync.WaitGroup{}
28 | for i := 0; i < 1000; i++ {
29 | wg.Add(1)
30 | go func() {
31 | for j := 0; j < 1000; j++ {
32 | dmp.AddTotalDownload(43521)
33 | }
34 | wg.Done()
35 | }()
36 | }
37 | wg.Wait()
38 | if dmp.download.GetTotalDataVolume() != 43521000000 {
39 | t.Fatal()
40 | }
41 | }
42 |
43 | func TestDataManager_GetAvgDownloadRate(t *testing.T) {
44 | dm := NewDataManager()
45 | dm.download.totalDataVolume = 3000000
46 | dm.captureTime = time.Second * 10
47 |
48 | result := dm.GetAvgDownloadRate()
49 | if result != 2.4 {
50 | t.Fatal()
51 | }
52 | }
53 |
54 | func TestDynamicRate(t *testing.T) {
55 |
56 | server, _ := CustomServer("http://shenzhen.cmcc.speedtest.shunshiidc.com:8080/speedtest/upload.php")
57 | //server, _ := CustomServer("http://192.168.5.237:8080/speedtest/upload.php")
58 |
59 | oldDownTotal := server.Context.Manager.GetTotalDownload()
60 | oldUpTotal := server.Context.Manager.GetTotalUpload()
61 |
62 | server.Context.Manager.SetRateCaptureFrequency(time.Millisecond * 100)
63 | server.Context.Manager.SetCaptureTime(time.Second)
64 | go func() {
65 | for i := 0; i < 2; i++ {
66 | time.Sleep(time.Second)
67 | newDownTotal := server.Context.Manager.GetTotalDownload()
68 | newUpTotal := server.Context.Manager.GetTotalUpload()
69 |
70 | downRate := float64(newDownTotal-oldDownTotal) * 8 / 1000 / 1000
71 | upRate := float64(newUpTotal-oldUpTotal) * 8 / 1000 / 1000
72 | oldDownTotal = newDownTotal
73 | oldUpTotal = newUpTotal
74 | fmt.Printf("downRate: %.2fMbps | upRate: %.2fMbps\n", downRate, upRate)
75 | }
76 | }()
77 |
78 | err := server.DownloadTest()
79 | if err != nil {
80 | fmt.Println("Warning: not found server")
81 | //t.Error(err)
82 | }
83 |
84 | server.Context.Manager.Wait()
85 |
86 | err = server.UploadTest()
87 | if err != nil {
88 | fmt.Println("Warning: not found server")
89 | //t.Error(err)
90 | }
91 |
92 | fmt.Printf(" \n")
93 |
94 | fmt.Printf("Download: %5.2f Mbit/s\n", server.DLSpeed)
95 | fmt.Printf("Upload: %5.2f Mbit/s\n\n", server.ULSpeed)
96 | valid := server.CheckResultValid()
97 | if !valid {
98 | fmt.Println("Warning: result seems to be wrong. Please test again.")
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/speedtest/unit.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | type UnitType int
8 |
9 | // IEC and SI
10 | const (
11 | UnitTypeDecimalBits = UnitType(iota) // auto scaled
12 | UnitTypeDecimalBytes // auto scaled
13 | UnitTypeBinaryBits // auto scaled
14 | UnitTypeBinaryBytes // auto scaled
15 | UnitTypeDefaultMbps // fixed
16 | )
17 |
18 | var (
19 | DecimalBitsUnits = []string{"bps", "Kbps", "Mbps", "Gbps"}
20 | DecimalBytesUnits = []string{"B/s", "KB/s", "MB/s", "GB/s"}
21 | BinaryBitsUnits = []string{"Kibps", "KiMbps", "KiGbps"}
22 | BinaryBytesUnits = []string{"KiB/s", "MiB/s", "GiB/s"}
23 | )
24 |
25 | var unitMaps = map[UnitType][]string{
26 | UnitTypeDecimalBits: DecimalBitsUnits,
27 | UnitTypeDecimalBytes: DecimalBytesUnits,
28 | UnitTypeBinaryBits: BinaryBitsUnits,
29 | UnitTypeBinaryBytes: BinaryBytesUnits,
30 | }
31 |
32 | const (
33 | B = 1.0
34 | KB = 1000 * B
35 | MB = 1000 * KB
36 | GB = 1000 * MB
37 |
38 | IB = 1
39 | KiB = 1024 * IB
40 | MiB = 1024 * KiB
41 | GiB = 1024 * MiB
42 | )
43 |
44 | type ByteRate float64
45 |
46 | var globalByteRateUnit UnitType
47 |
48 | func (r ByteRate) String() string {
49 | if r == 0 {
50 | return "0.00 Mbps"
51 | }
52 | if r == -1 {
53 | return "N/A"
54 | }
55 | if globalByteRateUnit != UnitTypeDefaultMbps {
56 | return r.Byte(globalByteRateUnit)
57 | }
58 | return strconv.FormatFloat(float64(r/125000.0), 'f', 2, 64) + " Mbps"
59 | }
60 |
61 | // SetUnit Set global output units
62 | func SetUnit(unit UnitType) {
63 | globalByteRateUnit = unit
64 | }
65 |
66 | func (r ByteRate) Mbps() float64 {
67 | return float64(r) / 125000.0
68 | }
69 |
70 | func (r ByteRate) Gbps() float64 {
71 | return float64(r) / 125000000.0
72 | }
73 |
74 | // Byte Specifies the format output byte rate
75 | func (r ByteRate) Byte(formatType UnitType) string {
76 | if r == 0 {
77 | return "0.00 Mbps"
78 | }
79 | if r == -1 {
80 | return "N/A"
81 | }
82 | return format(float64(r), formatType)
83 | }
84 |
85 | func format(byteRate float64, i UnitType) string {
86 | val := byteRate
87 | if i%2 == 0 {
88 | val *= 8
89 | }
90 | if i < UnitTypeBinaryBits {
91 | switch {
92 | case byteRate >= GB:
93 | return strconv.FormatFloat(val/GB, 'f', 2, 64) + " " + unitMaps[i][3]
94 | case byteRate >= MB:
95 | return strconv.FormatFloat(val/MB, 'f', 2, 64) + " " + unitMaps[i][2]
96 | case byteRate >= KB:
97 | return strconv.FormatFloat(val/KB, 'f', 2, 64) + " " + unitMaps[i][1]
98 | default:
99 | return strconv.FormatFloat(val/B, 'f', 2, 64) + " " + unitMaps[i][0]
100 | }
101 | }
102 | switch {
103 | case byteRate >= GiB:
104 | return strconv.FormatFloat(val/GiB, 'f', 2, 64) + " " + unitMaps[i][2]
105 | case byteRate >= MiB:
106 | return strconv.FormatFloat(val/MiB, 'f', 2, 64) + " " + unitMaps[i][1]
107 | default:
108 | return strconv.FormatFloat(val/KiB, 'f', 2, 64) + " " + unitMaps[i][0]
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
3 | github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
4 | github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
5 | github.com/chelnak/ysmrr v0.5.0 h1:aCLTtiJbzJVhiRTL1zyTGnWSCdK3R44QeFklPZRt8tg=
6 | github.com/chelnak/ysmrr v0.5.0/go.mod h1:Eg/IrbWqE3hOD5itwl2GlekRD7um93ap4gHOsxe+KvQ=
7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
10 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
11 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
12 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
13 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
14 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
15 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
16 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
17 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
18 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
19 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
20 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
21 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
22 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
23 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
24 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
25 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
26 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
27 | golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
28 | golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
29 | golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
30 | golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
31 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
32 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
33 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
35 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
36 |
--------------------------------------------------------------------------------
/docs/experimental_result.md:
--------------------------------------------------------------------------------
1 | # Experimental Result
2 | I randomly select 9 servers to speedtest considering distance from my house. Try speedtesting twice to each server. Testing order is like [speedtest.net] -> [speedtest-go] -> [speedtest-cli] -> [speedtest-cli] -> [speedtest-go] -> [speedtest.net]. To the next server, starting from [speedtest-go], like [speedtest-go] -> [speedtest-cli] -> [speedtest.net] -> [speedtest.net] -> [speedtest-cli] -> [speedtest-go].
3 |
4 | ## Downlaod (Mbps)
5 |
6 | | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) |
7 | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
8 | | 0 - 1000 (9km) | 6691 | 93.34 | 93.51 | 89.13 | 88.40 | 88.21 | 83.34 |
9 | | 0 - 1000 (194km) | 6368 | 93.45 | 94.20 | 92.93 | 92.26 | 88.45 | 77.07 |
10 | | 0 - 1000 (865km) | 6842 | 88.31 | 89.91 | 91.26 | 93.30 | 67.33 | 87.52 |
11 | | 1000 - 8000 (1982km) | 2589 | 90.24 | 94.05 | 79.61 | 79.52 | 79.16 | 70.36 |
12 | | 1000 - 8000 (4801km) | 3296 | 75.61 | 52.83 | 69.89 | 69.86 | 58.19 | 55.93 |
13 | | 1000 - 8000 (6627km) | 1718 | 42.04 | 43.92 | 39.81 | 54.35 | 44.47 | 31.27 |
14 | | 8000 - 20000 (8972km) | 3409 | 22.11 | 22.74 | 28.21 | 21.82 | 24.78 | 30.59 |
15 | | 8000 - 20000 (13781km) | 3162 | 15.78 | 7.73 | 1.94 | 1.02 | 3.32 | 2.40 |
16 | | 8000 - 20000 (17805km) | 4256 | 1.15 | 1.50 | 1.79 | 1.79 | 6.41 | 3.69 |
17 |
18 | ## Upload (Mbps)
19 |
20 | | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) |
21 | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
22 | | 0 - 1000 (9km) | 6691 | 52.03 | 48.82 | 40.72 | 43.84 | 36.03 | 36.21
23 | | 0 - 1000 (194km) | 6368 | 78.30 | 66.92 | 53.98 | 44.07 | 37.26 | 46.98
24 | | 0 - 1000 (865km) | 6842 | 65.34 | 81.96 | 50.27 | 52.57 | 29.42 | 31.08
25 | | 1000 - 8000 (1982km) | 2589 | 89.29 | 58.69 | 80.68 | 56.59 | 42.66 | 44.34
26 | | 1000 - 8000 (4801km) | 3296 | 50.16 | 50.78 | 54.94 | 54.18 | 20.59 | 20.23
27 | | 1000 - 8000 (6627km) | 1718 | 48.48 | 50.71 | 39.81 | 42.21 | 16.16 | 16.72
28 | | 8000 - 20000 (8972km) | 3409 | 18.07 | 20.30 | 20.35 | 24.92 | 5.87 | 3.37
29 | | 8000 - 20000 (13781km) | 3162 | 1.45 | 0.33 | 1.78 | 0.77 | 1.08 | 1.34
30 | | 8000 - 20000 (17805km) | 4256 | 1.16 | 0.30 | 1.00 | 1.11 | 2.37 | 1.42
31 |
32 | ## Testing Time (sec)
33 |
34 | | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) |
35 | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: |
36 | | 0 - 1000 (9km) | 6691 | 44.87 | 45.76 | 24.45 | 22.40 | 20.82 | 16.51
37 | | 0 - 1000 (194km) | 6368 | 43.85 | 42.97 | 19.60 | 22.36 | 22.25 | 24.01
38 | | 0 - 1000 (865km) | 6842 | 46.62 | 46.12 | 21.83 | 26.40 | 39.07 | 24.10
39 | | 1000 - 8000 (1982km) | 2589 | 45.38 | 44.18 | 18.71 | 21.35 | 26.15 | 25.71
40 | | 1000 - 8000 (4801km) | 3296 | 47.92 | 50.52 | 24.71 | 23.47 | 32.07 | 28.53
41 | | 1000 - 8000 (6627km) | 1718 | 40.15 | 41.16 | 28.05 | 30.42 | 31.01 | 27.65
42 | | 8000 - 20000 (8972km) | 3409 | 51.53 | 56.89 | 38.13 | 41.55 | 36.87 | 34.47
43 | | 8000 - 20000 (13781km) | 3162 | 52.75 | 58.10 | 28.78 | 29.91 | 40.75 | 46.88
44 | | 8000 - 20000 (17805km) | 4256 | 37.73 | 40.82 | 33.33 | 32.79 | 37.82 | 50.78
45 |
--------------------------------------------------------------------------------
/task.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/chelnak/ysmrr"
6 | "os"
7 | "strings"
8 | )
9 |
10 | type TaskManager struct {
11 | sm ysmrr.SpinnerManager
12 | isOut bool
13 | noProgress bool
14 | }
15 |
16 | type Task struct {
17 | spinner *ysmrr.Spinner
18 | manager *TaskManager
19 | title string
20 | }
21 |
22 | func InitTaskManager(jsonOutput, unixOutput bool) *TaskManager {
23 | isOut := !jsonOutput || unixOutput
24 | tm := &TaskManager{sm: ysmrr.NewSpinnerManager(), isOut: isOut, noProgress: unixOutput}
25 | if isOut && !unixOutput {
26 | tm.sm.Start()
27 | }
28 | return tm
29 | }
30 |
31 | func (tm *TaskManager) Reset() {
32 | if tm.isOut && !tm.noProgress {
33 | tm.sm.Stop()
34 | tm.sm = ysmrr.NewSpinnerManager()
35 | tm.sm.Start()
36 | }
37 | }
38 |
39 | func (tm *TaskManager) Stop() {
40 | if tm.isOut && !tm.noProgress {
41 | tm.sm.Stop()
42 | }
43 | }
44 |
45 | func (tm *TaskManager) Println(message string) {
46 | if tm.noProgress {
47 | fmt.Println(message)
48 | return
49 | }
50 | if tm.isOut {
51 | context := &Task{manager: tm}
52 | context.spinner = tm.sm.AddSpinner(message)
53 | context.Complete()
54 | }
55 | }
56 |
57 | func (tm *TaskManager) RunWithTrigger(enable bool, title string, callback func(task *Task)) {
58 | if enable {
59 | tm.Run(title, callback)
60 | }
61 | }
62 |
63 | func (tm *TaskManager) Run(title string, callback func(task *Task)) {
64 | context := &Task{manager: tm, title: title}
65 | if tm.isOut {
66 | if tm.noProgress {
67 | //fmt.Println(title)
68 | } else {
69 | context.spinner = tm.sm.AddSpinner(title)
70 | }
71 | }
72 | callback(context)
73 | }
74 |
75 | func (tm *TaskManager) AsyncRun(title string, callback func(task *Task)) {
76 | context := &Task{manager: tm, title: title}
77 | if tm.isOut {
78 | if tm.noProgress {
79 | //fmt.Println(title)
80 | } else {
81 | context.spinner = tm.sm.AddSpinner(title)
82 | }
83 | }
84 | go callback(context)
85 | }
86 |
87 | func (t *Task) Complete() {
88 | if t.manager.noProgress {
89 | return
90 | }
91 | if t.spinner == nil {
92 | return
93 | }
94 | t.spinner.Complete()
95 | }
96 |
97 | func (t *Task) Updatef(format string, a ...interface{}) {
98 | if t.spinner == nil || t.manager.noProgress {
99 | return
100 | }
101 | t.spinner.UpdateMessagef(format, a...)
102 | }
103 |
104 | func (t *Task) Update(format string) {
105 | if t.spinner == nil || t.manager.noProgress {
106 | return
107 | }
108 | t.spinner.UpdateMessage(format)
109 | }
110 |
111 | func (t *Task) Println(message string) {
112 | if t.manager.noProgress {
113 | fmt.Println(message)
114 | return
115 | }
116 | if t.spinner == nil {
117 | return
118 | }
119 | t.spinner.UpdateMessage(message)
120 | }
121 |
122 | func (t *Task) Printf(format string, a ...interface{}) {
123 | if t.manager.noProgress {
124 | fmt.Printf(format+"\n", a...)
125 | return
126 | }
127 | if t.spinner == nil {
128 | return
129 | }
130 | t.spinner.UpdateMessagef(format, a...)
131 | }
132 |
133 | func (t *Task) CheckError(err error) {
134 | if err != nil {
135 | if t.spinner != nil {
136 | t.Printf("Fatal: %s, err: %v", strings.ToLower(t.title), err)
137 | t.spinner.Error()
138 | t.manager.Stop()
139 | } else {
140 | fmt.Printf("Fatal: %s, err: %v", strings.ToLower(t.title), err)
141 | }
142 | os.Exit(0)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/speedtest/request_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "runtime"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestDownloadTestContext(t *testing.T) {
12 | idealSpeed := 0.1 * 8 * float64(runtime.NumCPU()) * 10 / 0.1 // one mockRequest per second with all CPU cores
13 | delta := 0.15
14 | latency, _ := time.ParseDuration("5ms")
15 | server := Server{
16 | URL: "https://dummy.com/upload.php",
17 | Latency: latency,
18 | Context: defaultClient,
19 | }
20 |
21 | server.Context.Manager.Reset()
22 | server.Context.SetRateCaptureFrequency(time.Millisecond)
23 | server.Context.SetCaptureTime(time.Second)
24 |
25 | err := server.downloadTestContext(
26 | context.Background(),
27 | mockRequest,
28 | )
29 | if err != nil {
30 | t.Errorf(err.Error())
31 | }
32 | value := server.Context.Manager.GetAvgDownloadRate()
33 | if value < idealSpeed*(1-delta) || idealSpeed*(1+delta) < value {
34 | t.Errorf("got unexpected server.DLSpeed '%v', expected between %v and %v", value, idealSpeed*(1-delta), idealSpeed*(1+delta))
35 | }
36 | if server.TestDuration.Download == nil || *server.TestDuration.Download != *server.TestDuration.Total {
37 | t.Errorf("can't count test duration, server.TestDuration.Download=%v, server.TestDuration.Total=%v", server.TestDuration.Download, server.TestDuration.Total)
38 | }
39 | }
40 |
41 | func TestUploadTestContext(t *testing.T) {
42 | idealSpeed := 0.1 * 8 * float64(runtime.NumCPU()) * 10 / 0.1 // one mockRequest per second with all CPU cores
43 | delta := 0.15 // tolerance scope (-0.05, +0.05)
44 |
45 | latency, _ := time.ParseDuration("5ms")
46 | server := Server{
47 | URL: "https://dummy.com/upload.php",
48 | Latency: latency,
49 | Context: defaultClient,
50 | }
51 |
52 | server.Context.Manager.Reset()
53 | server.Context.SetRateCaptureFrequency(time.Millisecond)
54 | server.Context.SetCaptureTime(time.Second)
55 |
56 | err := server.uploadTestContext(
57 | context.Background(),
58 | mockRequest,
59 | )
60 | if err != nil {
61 | t.Errorf(err.Error())
62 | }
63 | value := server.Context.Manager.GetAvgUploadRate()
64 | if value < idealSpeed*(1-delta) || idealSpeed*(1+delta) < value {
65 | t.Errorf("got unexpected server.ULSpeed '%v', expected between %v and %v", value, idealSpeed*(1-delta), idealSpeed*(1+delta))
66 | }
67 | if server.TestDuration.Upload == nil || *server.TestDuration.Upload != *server.TestDuration.Total {
68 | t.Errorf("can't count test duration, server.TestDuration.Upload=%v, server.TestDuration.Total=%v", server.TestDuration.Upload, server.TestDuration.Total)
69 | }
70 | }
71 |
72 | func mockRequest(ctx context.Context, s *Server, w int) error {
73 | fmt.Sprintln(w)
74 | dc := s.Context.Manager.NewChunk()
75 | // (0.1MegaByte * 8bit * nConn * 10loop) / 0.1s = n*80Megabit
76 | // sleep has bad deviation on windows
77 | // ref https://github.com/golang/go/issues/44343
78 | dc.GetParent().AddTotalDownload(1 * 1000 * 1000)
79 | dc.GetParent().AddTotalUpload(1 * 1000 * 1000)
80 | time.Sleep(time.Millisecond * 100)
81 | return nil
82 | }
83 |
84 | func TestPautaFilter(t *testing.T) {
85 | //vector := []float64{6, 6, 6, 6, 6, 6, 6, 6, 6, 6}
86 | vector0 := []int64{26, 23, 32}
87 | vector1 := []int64{3, 4, 5, 6, 6, 6, 1, 7, 9, 5, 200}
88 | _, _, std, _, _ := StandardDeviation(vector0)
89 | if std != 3 {
90 | t.Fail()
91 | }
92 |
93 | result := pautaFilter(vector1)
94 | if len(result) != 10 {
95 | t.Fail()
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/speedtest/location.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | type Location struct {
11 | Name string
12 | CC string
13 | Lat float64
14 | Lon float64
15 | }
16 |
17 | // Locations TODO more location need to added
18 | var Locations = map[string]*Location{
19 | "brasilia": {"brasilia", "br", -15.793876, -47.8835327},
20 | "hongkong": {"hongkong", "hk", 22.3106806, 114.1700546},
21 | "tokyo": {"tokyo", "jp", 35.680938, 139.7674114},
22 | "london": {"london", "uk", 51.5072493, -0.1288861},
23 | "moscow": {"moscow", "ru", 55.7497248, 37.615989},
24 | "beijing": {"beijing", "cn", 39.8721243, 116.4077473},
25 | "paris": {"paris", "fr", 48.8626026, 2.3477229},
26 | "sanfrancisco": {"sanfrancisco", "us", 37.7540028, -122.4429967},
27 | "newyork": {"newyork", "us", 40.7200876, -74.0220945},
28 | "yishun": {"yishun", "sg", 1.4230218, 103.8404728},
29 | "delhi": {"delhi", "in", 28.6251287, 77.1960896},
30 | "monterrey": {"monterrey", "mx", 25.6881435, -100.3073485},
31 | "berlin": {"berlin", "de", 52.5168128, 13.4009469},
32 | "maputo": {"maputo", "mz", -25.9579267, 32.5760444},
33 | "honolulu": {"honolulu", "us", 20.8247065, -156.918706},
34 | "seoul": {"seoul", "kr", 37.6086268, 126.7179721},
35 | "osaka": {"osaka", "jp", 34.6952743, 135.5006967},
36 | "shanghai": {"shanghai", "cn", 31.2292105, 121.4661666},
37 | "urumqi": {"urumqi", "cn", 43.8256624, 87.6058564},
38 | "ottawa": {"ottawa", "ca", 45.4161836, -75.7035467},
39 | "capetown": {"capetown", "za", -33.9391993, 18.4316716},
40 | "sydney": {"sydney", "au", -33.8966622, 151.1731861},
41 | "perth": {"perth", "au", -31.9551812, 115.8591904},
42 | "warsaw": {"warsaw", "pl", 52.2396659, 21.0129345},
43 | "kampala": {"kampala", "ug", 0.3070027, 32.5675581},
44 | "bangkok": {"bangkok", "th", 13.7248936, 100.493026},
45 | }
46 |
47 | func PrintCityList() {
48 | fmt.Println("Available city labels (case insensitive): ")
49 | fmt.Println(" CC\t\tCityLabel\tLocation")
50 | for k, v := range Locations {
51 | fmt.Printf("(%v)\t%20s\t[%v, %v]\n", v.CC, k, v.Lat, v.Lon)
52 | }
53 | }
54 |
55 | func GetLocation(locationName string) (*Location, error) {
56 | loc, ok := Locations[strings.ToLower(locationName)]
57 | if ok {
58 | return loc, nil
59 | }
60 | return nil, errors.New("not found location")
61 | }
62 |
63 | // NewLocation new a Location
64 | func NewLocation(locationName string, latitude float64, longitude float64) *Location {
65 | var loc Location
66 | loc.Lat = latitude
67 | loc.Lon = longitude
68 | loc.Name = locationName
69 | Locations[locationName] = &loc
70 | return &loc
71 | }
72 |
73 | // ParseLocation parse latitude and longitude string
74 | func ParseLocation(locationName string, coordinateStr string) (*Location, error) {
75 | ll := strings.Split(coordinateStr, ",")
76 | if len(ll) == 2 {
77 | // parameters check
78 | lat, err := betweenRange(ll[0], 90)
79 | if err != nil {
80 | return nil, err
81 | }
82 | lon, err := betweenRange(ll[1], 180)
83 | if err != nil {
84 | return nil, err
85 | }
86 | name := "Custom-%s"
87 | if len(locationName) == 0 {
88 | name = "Custom-Default"
89 | }
90 | return NewLocation(fmt.Sprintf(name, locationName), lat, lon), nil
91 | }
92 | return nil, fmt.Errorf("invalid location input: %s", coordinateStr)
93 | }
94 |
95 | func (l *Location) String() string {
96 | return fmt.Sprintf("(%s) [%v, %v]", l.Name, l.Lat, l.Lon)
97 | }
98 |
99 | // betweenRange latitude and longitude range check
100 | func betweenRange(inputStrValue string, interval float64) (float64, error) {
101 | value, err := strconv.ParseFloat(inputStrValue, 64)
102 | if err != nil {
103 | return 0, fmt.Errorf("invalid input: %v", inputStrValue)
104 | }
105 | if value < -interval || interval < value {
106 | return 0, fmt.Errorf("invalid input. got: %v, expected between -%v and %v", inputStrValue, interval, interval)
107 | }
108 | return value, nil
109 | }
110 |
--------------------------------------------------------------------------------
/speedtest/internal/welford.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "math"
6 | "time"
7 | )
8 |
9 | // Welford Fast standard deviation calculation with moving window
10 | // ref Welford, B. P. (1962). Note on a Method for Calculating Corrected Sums of Squares and Products. Technometrics, 4(3), 419–420. https://doi.org/10.1080/00401706.1962.10490022
11 | type Welford struct {
12 | n int // data size
13 | cap int // queue capacity
14 | vector []float64 // data set
15 | mean float64 // mean
16 | sum float64 // sum
17 | eraseIndex int // the value will be erased next time
18 | currentStdDev float64
19 | consecutiveStableIterations int
20 | consecutiveStableIterationsThreshold int
21 | cv float64
22 | ewmaMean float64
23 | steps int
24 | minSteps int
25 | beta float64
26 | scale float64
27 | movingVector []float64 // data set
28 | movingAvg float64
29 | }
30 |
31 | // NewWelford recommended windowSize = cycle / sampling frequency
32 | func NewWelford(cycle, frequency time.Duration) *Welford {
33 | windowSize := int(cycle / frequency)
34 | return &Welford{
35 | vector: make([]float64, windowSize),
36 | movingVector: make([]float64, windowSize),
37 | cap: windowSize,
38 | consecutiveStableIterationsThreshold: windowSize / 3, // 33%
39 | minSteps: windowSize * 2, // set minimum steps with 2x windowSize.
40 | beta: 2 / (float64(windowSize) + 1), // ewma beta ratio
41 | scale: float64(time.Second / frequency),
42 | }
43 | }
44 |
45 | // Update Enter the given value into the measuring system.
46 | // return bool stability evaluation
47 | func (w *Welford) Update(globalAvg, value float64) bool {
48 | value = value * w.scale
49 | if w.n == w.cap {
50 | delta := w.vector[w.eraseIndex] - w.mean
51 | w.mean -= delta / float64(w.n-1)
52 | w.sum -= delta * (w.vector[w.eraseIndex] - w.mean)
53 | // the calc error is approximated to zero
54 | if w.sum < 0 {
55 | w.sum = 0
56 | }
57 | w.vector[w.eraseIndex] = globalAvg
58 | w.movingAvg -= w.movingVector[w.eraseIndex]
59 | w.movingVector[w.eraseIndex] = value
60 | w.movingAvg += value
61 | w.eraseIndex++
62 | if w.eraseIndex == w.cap {
63 | w.eraseIndex = 0
64 | }
65 | } else {
66 | w.vector[w.n] = globalAvg
67 | w.movingVector[w.n] = value
68 | w.movingAvg += value
69 | w.n++
70 | }
71 | delta := globalAvg - w.mean
72 | w.mean += delta / float64(w.n)
73 | w.sum += delta * (globalAvg - w.mean)
74 | w.currentStdDev = math.Sqrt(w.Variance())
75 | // update C.V
76 | if w.mean != 0 {
77 | w.cv = w.currentStdDev / w.mean
78 | }
79 | w.ewmaMean = value*w.beta + w.ewmaMean*(1-w.beta)
80 | // acc consecutiveStableIterations
81 | if w.n == w.cap && w.cv < 0.03 {
82 | w.consecutiveStableIterations++
83 | } else if w.consecutiveStableIterations > 0 {
84 | w.consecutiveStableIterations--
85 | }
86 | w.steps++
87 | return w.consecutiveStableIterations >= w.consecutiveStableIterationsThreshold && w.steps > w.minSteps
88 | }
89 |
90 | func (w *Welford) Mean() float64 {
91 | return w.mean
92 | }
93 |
94 | func (w *Welford) CV() float64 {
95 | return w.cv
96 | }
97 |
98 | func (w *Welford) Variance() float64 {
99 | if w.n < 2 {
100 | return 0
101 | }
102 | return w.sum / float64(w.n-1)
103 | }
104 |
105 | func (w *Welford) StandardDeviation() float64 {
106 | return w.currentStdDev
107 | }
108 |
109 | func (w *Welford) EWMA() float64 {
110 | return w.ewmaMean*0.5 + w.movingAvg/float64(w.n)*0.5
111 | }
112 |
113 | func (w *Welford) String() string {
114 | return fmt.Sprintf("Mean: %.2f, Standard Deviation: %.2f, C.V: %.2f, EWMA: %.2f", w.Mean(), w.StandardDeviation(), w.CV(), w.EWMA())
115 | }
116 |
--------------------------------------------------------------------------------
/speedtest/loss.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "github.com/showwin/speedtest-go/speedtest/transport"
6 | "net"
7 | "sync"
8 | "time"
9 | )
10 |
11 | type PacketLossAnalyzerOptions struct {
12 | RemoteSamplingInterval time.Duration
13 | SamplingDuration time.Duration
14 | PacketSendingInterval time.Duration
15 | PacketSendingTimeout time.Duration
16 | SourceInterface string // source interface
17 | TCPDialer *net.Dialer // tcp dialer for sampling
18 | UDPDialer *net.Dialer // udp dialer for sending packet
19 |
20 | }
21 |
22 | type PacketLossAnalyzer struct {
23 | options *PacketLossAnalyzerOptions
24 | }
25 |
26 | func NewPacketLossAnalyzer(options *PacketLossAnalyzerOptions) *PacketLossAnalyzer {
27 | if options == nil {
28 | options = &PacketLossAnalyzerOptions{}
29 | }
30 | if options.SamplingDuration == 0 {
31 | options.SamplingDuration = time.Second * 30
32 | }
33 | if options.RemoteSamplingInterval == 0 {
34 | options.RemoteSamplingInterval = 1 * time.Second
35 | }
36 | if options.PacketSendingInterval == 0 {
37 | options.PacketSendingInterval = 67 * time.Millisecond
38 | }
39 | if options.PacketSendingTimeout == 0 {
40 | options.PacketSendingTimeout = 5 * time.Second
41 | }
42 | if options.TCPDialer == nil {
43 | options.TCPDialer = &net.Dialer{
44 | Timeout: options.PacketSendingTimeout,
45 | }
46 | }
47 | if options.UDPDialer == nil {
48 | var addr net.Addr
49 | if len(options.SourceInterface) > 0 {
50 | // skip error and using auto-select
51 | addr, _ = net.ResolveUDPAddr("udp", options.SourceInterface)
52 | }
53 | options.UDPDialer = &net.Dialer{
54 | Timeout: options.PacketSendingTimeout,
55 | LocalAddr: addr,
56 | }
57 | }
58 | return &PacketLossAnalyzer{
59 | options: options,
60 | }
61 | }
62 |
63 | // RunMulti Mix all servers to get the average packet loss.
64 | func (pla *PacketLossAnalyzer) RunMulti(hosts []string) (*transport.PLoss, error) {
65 | ctx, cancel := context.WithTimeout(context.Background(), pla.options.SamplingDuration)
66 | defer cancel()
67 | return pla.RunMultiWithContext(ctx, hosts)
68 | }
69 |
70 | func (pla *PacketLossAnalyzer) RunMultiWithContext(ctx context.Context, hosts []string) (*transport.PLoss, error) {
71 | results := make(map[string]*transport.PLoss)
72 | mutex := &sync.Mutex{}
73 | wg := &sync.WaitGroup{}
74 | for _, host := range hosts {
75 | wg.Add(1)
76 | go func(h string) {
77 | defer wg.Done()
78 | _ = pla.RunWithContext(ctx, h, func(packetLoss *transport.PLoss) {
79 | if packetLoss.Sent != 0 {
80 | mutex.Lock()
81 | results[h] = packetLoss
82 | mutex.Unlock()
83 | }
84 | })
85 | }(host)
86 | }
87 | wg.Wait()
88 | if len(results) == 0 {
89 | return nil, transport.ErrUnsupported
90 | }
91 | var pLoss transport.PLoss
92 | for _, hostPacketLoss := range results {
93 | pLoss.Sent += hostPacketLoss.Sent
94 | pLoss.Dup += hostPacketLoss.Dup
95 | pLoss.Max += hostPacketLoss.Max
96 | }
97 | return &pLoss, nil
98 | }
99 |
100 | func (pla *PacketLossAnalyzer) Run(host string, callback func(packetLoss *transport.PLoss)) error {
101 | ctx, cancel := context.WithTimeout(context.Background(), pla.options.SamplingDuration)
102 | defer cancel()
103 | return pla.RunWithContext(ctx, host, callback)
104 | }
105 |
106 | func (pla *PacketLossAnalyzer) RunWithContext(ctx context.Context, host string, callback func(packetLoss *transport.PLoss)) error {
107 | samplerClient, err := transport.NewClient(pla.options.TCPDialer)
108 | if err != nil {
109 | return transport.ErrUnsupported
110 | }
111 | senderClient, err := transport.NewPacketLossSender(samplerClient.ID(), pla.options.UDPDialer)
112 | if err != nil {
113 | return transport.ErrUnsupported
114 | }
115 |
116 | if err = samplerClient.Connect(ctx, host); err != nil {
117 | return transport.ErrUnsupported
118 | }
119 | if err = senderClient.Connect(ctx, host); err != nil {
120 | return transport.ErrUnsupported
121 | }
122 | if err = samplerClient.InitPacketLoss(); err != nil {
123 | return transport.ErrUnsupported
124 | }
125 | go pla.loopSender(ctx, senderClient)
126 | return pla.loopSampler(ctx, samplerClient, callback)
127 | }
128 |
129 | func (pla *PacketLossAnalyzer) loopSampler(ctx context.Context, client *transport.Client, callback func(packetLoss *transport.PLoss)) error {
130 | ticker := time.NewTicker(pla.options.RemoteSamplingInterval)
131 | defer ticker.Stop()
132 | for {
133 | select {
134 | case <-ticker.C:
135 | if pl, err1 := client.PacketLoss(); err1 == nil {
136 | if pl != nil {
137 | callback(pl)
138 | }
139 | } else {
140 | return err1
141 | }
142 | case <-ctx.Done():
143 | return nil
144 | }
145 | }
146 | }
147 |
148 | func (pla *PacketLossAnalyzer) loopSender(ctx context.Context, senderClient *transport.PacketLossSender) {
149 | order := 0
150 | sendTick := time.NewTicker(pla.options.PacketSendingInterval)
151 | defer sendTick.Stop()
152 | for {
153 | select {
154 | case <-sendTick.C:
155 | _ = senderClient.Send(order)
156 | order++
157 | case <-ctx.Done():
158 | return
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/speedtest/server_test.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "errors"
5 | "math"
6 | "math/rand"
7 | "testing"
8 | "time"
9 | )
10 |
11 | func TestFetchServerList(t *testing.T) {
12 | client := New()
13 | client.User = &User{
14 | IP: "111.111.111.111",
15 | Lat: "35.22",
16 | Lon: "138.44",
17 | Isp: "Hello",
18 | }
19 | servers, err := client.FetchServers()
20 | if err != nil {
21 | t.Errorf(err.Error())
22 | }
23 | if len(servers) == 0 {
24 | t.Errorf("Failed to fetch server list.")
25 | return
26 | }
27 | if len(servers[0].Country) == 0 {
28 | t.Errorf("got unexpected country name '%v'", servers[0].Country)
29 | }
30 | }
31 |
32 | func TestDistanceSame(t *testing.T) {
33 | for i := 0; i < 10000000; i++ {
34 | v1 := rand.Float64() * 90
35 | v2 := rand.Float64() * 180
36 | v3 := rand.Float64() * 90
37 | v4 := rand.Float64() * 180
38 | k1 := distance(v1, v2, v1, v2)
39 | k2 := distance(v1, v2, v3, v4)
40 | if math.IsNaN(k1) || math.IsNaN(k2) {
41 | t.Fatalf("NaN distance: %f, %f, %f, %f", v1, v2, v3, v4)
42 | }
43 | }
44 |
45 | testdata := [][]float64{
46 | {32.0803, 34.7805, 32.0803, 34.7805},
47 | {0, 0, 0, 0},
48 | {1, 1, 1, 1},
49 | {2, 2, 2, 2},
50 | {-123.23, 123.33, -123.23, 123.33},
51 | {90, 180, 90, 180},
52 | }
53 | for i := range testdata {
54 | k := distance(testdata[i][0], testdata[i][1], testdata[i][2], testdata[i][3])
55 | if math.IsNaN(k) {
56 | t.Fatalf("NaN distance: %f, %f, %f, %f", testdata[i][0], testdata[i][1], testdata[i][2], testdata[i][3])
57 | }
58 | }
59 | }
60 |
61 | func TestDistance(t *testing.T) {
62 | d := distance(0.0, 0.0, 1.0, 1.0)
63 | if d < 157 || 158 < d {
64 | t.Errorf("got: %v, expected between 157 and 158", d)
65 | }
66 |
67 | d = distance(0.0, 180.0, 0.0, -180.0)
68 | if d < 0 && d > 1 {
69 | t.Errorf("got: %v, expected 0", d)
70 | }
71 |
72 | d1 := distance(100.0, 100.0, 100.0, 101.0)
73 | d2 := distance(100.0, 100.0, 100.0, 99.0)
74 | if d1 != d2 {
75 | t.Errorf("%v and %v should be same value", d1, d2)
76 | }
77 |
78 | d = distance(35.0, 140.0, -40.0, -140.0)
79 | if d < 11000 || 12000 < d {
80 | t.Errorf("got: %v, expected ~11694.5122", d)
81 | }
82 | }
83 |
84 | func TestFindServer(t *testing.T) {
85 | servers := Servers{
86 | &Server{
87 | ID: "1",
88 | },
89 | &Server{
90 | ID: "2",
91 | },
92 | &Server{
93 | ID: "3",
94 | },
95 | }
96 |
97 | var serverID []int
98 | s, err := servers.FindServer(serverID)
99 | if err != nil {
100 | t.Errorf(err.Error())
101 | }
102 | if len(s) != 1 {
103 | t.Errorf("unexpected server length. got: %v, expected: 1", len(s))
104 | }
105 | if s[0].ID != "1" {
106 | t.Errorf("unexpected server ID. got: %v, expected: '1'", s[0].ID)
107 | }
108 |
109 | serverID = []int{2}
110 | s, err = servers.FindServer(serverID)
111 | if err != nil {
112 | t.Errorf(err.Error())
113 | }
114 | if len(s) != 1 {
115 | t.Errorf("unexpected server length. got: %v, expected: 1", len(s))
116 | }
117 | if s[0].ID != "2" {
118 | t.Errorf("unexpected server ID. got: %v, expected: '2'", s[0].ID)
119 | }
120 |
121 | serverID = []int{3, 1}
122 | s, err = servers.FindServer(serverID)
123 | if err != nil {
124 | t.Errorf(err.Error())
125 | }
126 | if len(s) != 2 {
127 | t.Errorf("unexpected server length. got: %v, expected: 2", len(s))
128 | }
129 | if s[0].ID != "3" {
130 | t.Errorf("unexpected server ID. got: %v, expected: '3'", s[0].ID)
131 | }
132 | if s[1].ID != "1" {
133 | t.Errorf("unexpected server ID. got: %v, expected: '1'", s[0].ID)
134 | }
135 | }
136 |
137 | func TestCustomServer(t *testing.T) {
138 | // Good server
139 | got, err := CustomServer("https://example.com/upload.php")
140 | if err != nil {
141 | t.Errorf(err.Error())
142 | }
143 | if got == nil {
144 | t.Error("empty server")
145 | return
146 | }
147 | if got.Host != "example.com" {
148 | t.Error("did not properly set the Host field on a custom server")
149 | }
150 | }
151 |
152 | func TestFetchServerByID(t *testing.T) {
153 | remoteList, err := FetchServers()
154 | if err != nil {
155 | t.Fatal(err)
156 | }
157 |
158 | if remoteList.Len() < 1 {
159 | t.Fatal(errors.New("server not found"))
160 | }
161 |
162 | testData := map[string]bool{
163 | remoteList[0].ID: true,
164 | "-99999999": false,
165 | "どうも": false,
166 | "hello": false,
167 | "你好": false,
168 | }
169 |
170 | for id, b := range testData {
171 | server, err := FetchServerByID(id)
172 | if err != nil && b {
173 | t.Error(err)
174 | }
175 | if server != nil && (server.ID == id) != b {
176 | t.Errorf("id %s == %s is not %v", id, server.ID, b)
177 | }
178 | }
179 | }
180 |
181 | func TestTotalDurationCount(t *testing.T) {
182 | server, _ := CustomServer("https://example.com/upload.php")
183 |
184 | uploadTime := time.Duration(10000805542)
185 | server.TestDuration.Upload = &uploadTime
186 | server.testDurationTotalCount()
187 | if server.TestDuration.Total.Nanoseconds() != 10000805542 {
188 | t.Error("addition in testDurationTotalCount didn't work")
189 | }
190 |
191 | downloadTime := time.Duration(10000403875)
192 | server.TestDuration.Download = &downloadTime
193 | server.testDurationTotalCount()
194 | if server.TestDuration.Total.Nanoseconds() != 20001209417 {
195 | t.Error("addition in testDurationTotalCount didn't work")
196 | }
197 |
198 | pingTime := time.Duration(2183156458)
199 | server.TestDuration.Ping = &pingTime
200 | server.testDurationTotalCount()
201 | if server.TestDuration.Total.Nanoseconds() != 22184365875 {
202 | t.Error("addition in testDurationTotalCount didn't work")
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/speedtest/speedtest.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net"
7 | "net/http"
8 | "net/url"
9 | "runtime"
10 | "strings"
11 | "syscall"
12 | "time"
13 | )
14 |
15 | var (
16 | version = "1.7.10"
17 | DefaultUserAgent = fmt.Sprintf("showwin/speedtest-go %s", version)
18 | )
19 |
20 | type Proto int
21 |
22 | const (
23 | HTTP Proto = iota
24 | TCP
25 | ICMP
26 | )
27 |
28 | // Speedtest is a speedtest client.
29 | type Speedtest struct {
30 | User *User
31 | Manager
32 |
33 | doer *http.Client
34 | config *UserConfig
35 | tcpDialer *net.Dialer
36 | ipDialer *net.Dialer
37 | }
38 |
39 | type UserConfig struct {
40 | T *http.Transport
41 | UserAgent string
42 | Proxy string
43 | Source string
44 | DnsBindSource bool
45 | DialerControl func(network, address string, c syscall.RawConn) error
46 | Debug bool
47 | PingMode Proto
48 |
49 | SavingMode bool
50 | MaxConnections int
51 |
52 | CityFlag string
53 | LocationFlag string
54 | Location *Location
55 |
56 | Keyword string // Fuzzy search
57 | }
58 |
59 | func parseAddr(addr string) (string, string) {
60 | prefixIndex := strings.Index(addr, "://")
61 | if prefixIndex != -1 {
62 | return addr[:prefixIndex], addr[prefixIndex+3:]
63 | }
64 | return "", addr // ignore address network prefix
65 | }
66 |
67 | func (s *Speedtest) NewUserConfig(uc *UserConfig) {
68 | if uc.Debug {
69 | dbg.Enable()
70 | }
71 |
72 | if uc.SavingMode {
73 | uc.MaxConnections = 1 // Set the number of concurrent connections to 1
74 | }
75 | s.SetNThread(uc.MaxConnections)
76 |
77 | if len(uc.CityFlag) > 0 {
78 | var err error
79 | uc.Location, err = GetLocation(uc.CityFlag)
80 | if err != nil {
81 | dbg.Printf("Warning: skipping command line arguments: --city. err: %v\n", err.Error())
82 | }
83 | }
84 | if len(uc.LocationFlag) > 0 {
85 | var err error
86 | uc.Location, err = ParseLocation(uc.CityFlag, uc.LocationFlag)
87 | if err != nil {
88 | dbg.Printf("Warning: skipping command line arguments: --location. err: %v\n", err.Error())
89 | }
90 | }
91 |
92 | var tcpSource net.Addr // If nil, a local address is automatically chosen.
93 | var icmpSource net.Addr
94 | var proxy = http.ProxyFromEnvironment
95 | s.config = uc
96 | if len(s.config.UserAgent) == 0 {
97 | s.config.UserAgent = DefaultUserAgent
98 | }
99 | if len(uc.Source) > 0 {
100 | _, address := parseAddr(uc.Source)
101 | addr0, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("[%s]:0", address)) // dynamic tcp port
102 | if err == nil {
103 | tcpSource = addr0
104 | } else {
105 | dbg.Printf("Warning: skipping parse the source address. err: %s\n", err.Error())
106 | }
107 | addr1, err := net.ResolveIPAddr("ip", address) // dynamic tcp port
108 | if err == nil {
109 | icmpSource = addr1
110 | } else {
111 | dbg.Printf("Warning: skipping parse the source address. err: %s\n", err.Error())
112 | }
113 | if uc.DnsBindSource {
114 | net.DefaultResolver.Dial = func(ctx context.Context, network, dnsServer string) (net.Conn, error) {
115 | dialer := &net.Dialer{
116 | Timeout: 5 * time.Second,
117 | LocalAddr: func(network string) net.Addr {
118 | switch network {
119 | case "udp", "udp4", "udp6":
120 | return &net.UDPAddr{IP: net.ParseIP(address)}
121 | case "tcp", "tcp4", "tcp6":
122 | return &net.TCPAddr{IP: net.ParseIP(address)}
123 | default:
124 | return nil
125 | }
126 | }(network),
127 | }
128 | return dialer.DialContext(ctx, network, dnsServer)
129 | }
130 | }
131 | }
132 |
133 | if len(uc.Proxy) > 0 {
134 | if parse, err := url.Parse(uc.Proxy); err != nil {
135 | dbg.Printf("Warning: skipping parse the proxy host. err: %s\n", err.Error())
136 | } else {
137 | proxy = func(_ *http.Request) (*url.URL, error) {
138 | return parse, err
139 | }
140 | }
141 | }
142 |
143 | s.tcpDialer = &net.Dialer{
144 | LocalAddr: tcpSource,
145 | Timeout: 30 * time.Second,
146 | KeepAlive: 30 * time.Second,
147 | Control: uc.DialerControl,
148 | }
149 |
150 | s.ipDialer = &net.Dialer{
151 | LocalAddr: icmpSource,
152 | Timeout: 30 * time.Second,
153 | KeepAlive: 30 * time.Second,
154 | Control: uc.DialerControl,
155 | }
156 |
157 | s.config.T = &http.Transport{
158 | Proxy: proxy,
159 | DialContext: s.tcpDialer.DialContext,
160 | ForceAttemptHTTP2: true,
161 | MaxIdleConns: 100,
162 | IdleConnTimeout: 90 * time.Second,
163 | TLSHandshakeTimeout: 10 * time.Second,
164 | ExpectContinueTimeout: 1 * time.Second,
165 | }
166 |
167 | s.doer.Transport = s
168 | }
169 |
170 | func (s *Speedtest) RoundTrip(req *http.Request) (*http.Response, error) {
171 | req.Header.Add("User-Agent", s.config.UserAgent)
172 | return s.config.T.RoundTrip(req)
173 | }
174 |
175 | // Option is a function that can be passed to New to modify the Client.
176 | type Option func(*Speedtest)
177 |
178 | // WithDoer sets the http.Client used to make requests.
179 | func WithDoer(doer *http.Client) Option {
180 | return func(s *Speedtest) {
181 | s.doer = doer
182 | }
183 | }
184 |
185 | // WithUserConfig adds a custom user config for speedtest.
186 | // This configuration may be overwritten again by WithDoer,
187 | // because client and transport are parent-child relationship:
188 | // `New(WithDoer(myDoer), WithUserAgent(myUserAgent), WithDoer(myDoer))`
189 | func WithUserConfig(userConfig *UserConfig) Option {
190 | return func(s *Speedtest) {
191 | s.NewUserConfig(userConfig)
192 | dbg.Printf("Source: %s\n", s.config.Source)
193 | dbg.Printf("Proxy: %s\n", s.config.Proxy)
194 | dbg.Printf("SavingMode: %v\n", s.config.SavingMode)
195 | dbg.Printf("Keyword: %v\n", s.config.Keyword)
196 | dbg.Printf("PingType: %v\n", s.config.PingMode)
197 | dbg.Printf("OS: %s, ARCH: %s, NumCPU: %d\n", runtime.GOOS, runtime.GOARCH, runtime.NumCPU())
198 | }
199 | }
200 |
201 | // New creates a new speedtest client.
202 | func New(opts ...Option) *Speedtest {
203 | s := &Speedtest{
204 | doer: http.DefaultClient,
205 | Manager: NewDataManager(),
206 | }
207 | // load default config
208 | s.NewUserConfig(&UserConfig{UserAgent: DefaultUserAgent})
209 |
210 | for _, opt := range opts {
211 | opt(s)
212 | }
213 | return s
214 | }
215 |
216 | func Version() string {
217 | return version
218 | }
219 |
220 | var defaultClient = New()
221 |
--------------------------------------------------------------------------------
/speedtest/transport/tcp.go:
--------------------------------------------------------------------------------
1 | package transport
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "context"
7 | "errors"
8 | "fmt"
9 | "net"
10 | "strconv"
11 | "time"
12 | )
13 |
14 | var (
15 | pingPrefix = []byte{0x50, 0x49, 0x4e, 0x47, 0x20}
16 | // downloadPrefix = []byte{0x44, 0x4F, 0x57, 0x4E, 0x4C, 0x4F, 0x41, 0x44, 0x20}
17 | // uploadPrefix = []byte{0x55, 0x50, 0x4C, 0x4F, 0x41, 0x44, 0x20}
18 | initPacket = []byte{0x49, 0x4e, 0x49, 0x54, 0x50, 0x4c, 0x4f, 0x53, 0x53}
19 | packetLoss = []byte{0x50, 0x4c, 0x4f, 0x53, 0x53}
20 | hiFormat = []byte{0x48, 0x49}
21 | quitFormat = []byte{0x51, 0x55, 0x49, 0x54}
22 | )
23 |
24 | var (
25 | ErrEchoData = errors.New("incorrect echo data")
26 | ErrEmptyConn = errors.New("empty conn")
27 | ErrUnsupported = errors.New("unsupported protocol") // Some servers have disabled ip:8080, we return this error.
28 | ErrUninitializedPacketLossInst = errors.New("uninitialized packet loss inst")
29 | )
30 |
31 | func pingFormat(locTime int64) []byte {
32 | return strconv.AppendInt(pingPrefix, locTime, 10)
33 | }
34 |
35 | type Client struct {
36 | id string
37 | conn net.Conn
38 | host string
39 | version string
40 |
41 | dialer *net.Dialer
42 |
43 | reader *bufio.Reader
44 | }
45 |
46 | func NewClient(dialer *net.Dialer) (*Client, error) {
47 | uuid, err := generateUUID()
48 | if err != nil {
49 | return nil, err
50 | }
51 | return &Client{
52 | id: uuid,
53 | dialer: dialer,
54 | }, nil
55 | }
56 |
57 | func (client *Client) ID() string {
58 | return client.id
59 | }
60 |
61 | func (client *Client) Connect(ctx context.Context, host string) (err error) {
62 | client.host = host
63 | client.conn, err = client.dialer.DialContext(ctx, "tcp", client.host)
64 | if err != nil {
65 | return err
66 | }
67 | client.reader = bufio.NewReader(client.conn)
68 | return nil
69 | }
70 |
71 | func (client *Client) Disconnect() (err error) {
72 | _, _ = client.conn.Write(quitFormat)
73 | client.conn = nil
74 | client.reader = nil
75 | client.version = ""
76 | return
77 | }
78 |
79 | func (client *Client) Write(data []byte) (err error) {
80 | if client.conn == nil {
81 | return ErrEmptyConn
82 | }
83 | _, err = fmt.Fprintf(client.conn, "%s\n", data)
84 | return
85 | }
86 |
87 | func (client *Client) Read() ([]byte, error) {
88 | if client.conn == nil {
89 | return nil, ErrEmptyConn
90 | }
91 | return client.reader.ReadBytes('\n')
92 | }
93 |
94 | func (client *Client) Version() string {
95 | if len(client.version) == 0 {
96 | err := client.Write(hiFormat)
97 | if err == nil {
98 | message, err := client.Read()
99 | if err != nil || len(message) < 8 {
100 | return "unknown"
101 | }
102 | client.version = string(message[6 : len(message)-1])
103 | }
104 | }
105 | return client.version
106 | }
107 |
108 | // PingContext Measure latency(RTT) between client and server.
109 | // We use the 2RTT method to obtain three RTT result in
110 | // order to get more data in less time (t2-t0, t4-t2, t3-t1).
111 | // And give lower weight to the delay measured by the server.
112 | // local factor = 0.4 * 2 and remote factor = 0.2
113 | // latency = 0.4 * (t2 - t0) + 0.4 * (t4 - t2) + 0.2 * (t3 - t1)
114 | // @return cumulative delay in nanoseconds
115 | func (client *Client) PingContext(ctx context.Context) (int64, error) {
116 | resultChan := make(chan error, 1)
117 |
118 | var accumulatedLatency int64 = 0
119 | var firstReceivedByServer int64 // t1
120 |
121 | go func() {
122 | for i := 0; i < 2; i++ {
123 | t0 := time.Now().UnixNano()
124 | if err := client.Write(pingFormat(t0)); err != nil {
125 | resultChan <- err
126 | return
127 | }
128 | data, err := client.Read()
129 | t2 := time.Now().UnixNano()
130 | if err != nil {
131 | resultChan <- err
132 | return
133 | }
134 | if len(data) != 19 {
135 | resultChan <- ErrEchoData
136 | return
137 | }
138 | tx, err := strconv.ParseInt(string(data[5:18]), 10, 64)
139 | if err != nil {
140 | resultChan <- err
141 | return
142 | }
143 | accumulatedLatency += (t2 - t0) * 4 / 10 // 0.4
144 | if i == 0 {
145 | firstReceivedByServer = tx
146 | } else {
147 | // append server-side latency result
148 | accumulatedLatency += (tx - firstReceivedByServer) * 1000 * 1000 * 2 / 10 // 0.2
149 | }
150 | }
151 | resultChan <- nil
152 | close(resultChan)
153 | }()
154 |
155 | select {
156 | case err := <-resultChan:
157 | return accumulatedLatency, err
158 | case <-ctx.Done():
159 | return 0, ctx.Err()
160 | }
161 | }
162 |
163 | func (client *Client) InitPacketLoss() error {
164 | id := client.id
165 | payload := append(hiFormat, 0x20)
166 | payload = append(payload, []byte(id)...)
167 | err := client.Write(payload)
168 | if err != nil {
169 | return err
170 | }
171 | return client.Write(initPacket)
172 | }
173 |
174 | // PLoss Packet loss statistics
175 | // The packet loss here generally refers to uplink packet loss.
176 | // We use the following formula to calculate the packet loss:
177 | // packetLoss = [1 - (Sent - Dup) / (Max + 1)] * 100%
178 | type PLoss struct {
179 | Sent int `json:"sent"` // Number of sent packets acknowledged by the remote.
180 | Dup int `json:"dup"` // Number of duplicate packets acknowledged by the remote.
181 | Max int `json:"max"` // The maximum index value received by the remote.
182 | }
183 |
184 | func (p PLoss) String() string {
185 | if p.Sent == 0 {
186 | // if p.Sent == 0, maybe all data is dropped by the upper gateway.
187 | // we believe this feature is not applicable on this server now.
188 | return "Packet Loss: N/A"
189 | }
190 | return fmt.Sprintf("Packet Loss: %.2f%% (Sent: %d/Dup: %d/Max: %d)", p.Loss()*100, p.Sent, p.Dup, p.Max)
191 | }
192 |
193 | func (p PLoss) Loss() float64 {
194 | if p.Sent == 0 {
195 | return -1
196 | }
197 | return 1 - (float64(p.Sent-p.Dup))/float64(p.Max+1)
198 | }
199 |
200 | func (p PLoss) LossPercent() float64 {
201 | if p.Sent == 0 {
202 | return -1
203 | }
204 | return p.Loss() * 100
205 | }
206 |
207 | func (client *Client) PacketLoss() (*PLoss, error) {
208 | err := client.Write(packetLoss)
209 | if err != nil {
210 | return nil, err
211 | }
212 | result, err := client.Read()
213 | if err != nil {
214 | return nil, err
215 | }
216 | splitResult := bytes.Split(result, []byte{0x20})
217 | if len(splitResult) < 3 || !bytes.Equal(splitResult[0], packetLoss) {
218 | return nil, nil
219 | }
220 | x0, err := strconv.Atoi(string(splitResult[1]))
221 | if err != nil {
222 | return nil, err
223 | }
224 | x1, err := strconv.Atoi(string(splitResult[2]))
225 | if err != nil {
226 | return nil, err
227 | }
228 | x2, err := strconv.Atoi(string(bytes.TrimRight(splitResult[3], "\n")))
229 | if err != nil {
230 | return nil, err
231 | }
232 | return &PLoss{
233 | Sent: x0,
234 | Dup: x1,
235 | Max: x2,
236 | }, nil
237 | }
238 |
239 | func (client *Client) Download() {
240 | panic("Unimplemented method: Client.Download()")
241 | }
242 |
243 | func (client *Client) Upload() {
244 | panic("Unimplemented method: Client.Upload()")
245 | }
246 |
--------------------------------------------------------------------------------
/speedtest/internal/welford_test.go:
--------------------------------------------------------------------------------
1 | package internal
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func BenchmarkWOM(b *testing.B) {
11 | w := NewWelford(time.Second*5, time.Millisecond*50)
12 | rd := rand.New(rand.NewSource(0))
13 | var arr []float64
14 | for i := 0; i < 100; i++ {
15 | arr = append(arr, rd.Float64())
16 | }
17 | for i := 0; i < b.N; i++ {
18 | w.Update(arr[i%100], arr[i%100])
19 | }
20 | }
21 |
22 | func TestWOM(t *testing.T) {
23 | data := []float64{0, 6.91552, 18.721692307692308, 23.04116556291391, 28.059485148514852, 31.118470119521916, 34.21727152317881, 35.15127065527066, 36.74378054862843, 38.05981374722838, 39.122272, 38.76271506352087, 39.60149084858569, 40.23165538461539, 40.72287589158345, 41.3182940397351, 41.36879800498754, 41.848093896713614, 42.29560044395117, 42.45441684210526, 42.27466666666667, 42.67902476190476, 42.68457142857143, 42.80731190269331, 42.75244, 42.6367104, 42.65657801691007, 42.68948038490007, 42.81048394004282, 42.64527084769124, 42.802427145708585, 42.92410838709678, 43.15598501872659, 43.32482909090909, 43.46838800705467, 43.5166464877213, 43.523846751804555, 43.73983171521036, 43.870409258285115, 43.84913230769231, 43.72752023988006, 43.85322926829268, 43.964127436994765, 44.027410506741056, 43.99742169768498, 44.093007552199026, 44.241321164710996, 44.287108464483204, 44.079746772178254, 44.180878367346935, 44.1128768, 44.17403607843137, 44.0556123076923, 44.20335873255375, 44.19607703703704, 44.2603155216285, 44.202986438258385, 44.2371857042747, 44.17902344827586, 44.31277559322034, 44.30876912840985, 44.378359108781126, 44.3999845410628, 44.37440913415794, 44.4760624609619, 44.471889264841586, 44.454861296184134, 44.37803098927294, 44.37345251396648, 44.47449043478261, 44.43488914285714, 44.50349070422535, 44.45223882254929, 44.433060821917806, 44.46777081081081, 44.4554752, 44.43440842105263, 44.55939356178609, 44.59433376057421, 44.633178734177214, 44.5508462884279, 44.58072493827161, 44.67904608632041, 44.662959537572256, 44.66403237324446, 44.59772449459332, 44.53588837209303, 44.586143382352944, 44.66949738933031, 44.66949618320611, 44.69251965356429, 44.628363956043955, 44.63443946968051, 44.607556129032254, 44.66488851063829, 44.66553062513155, 44.72077, 44.75405813234384, 44.821391020408164, 44.88321874368814, 44.9364192, 44.94112373787369, 44.958842352941176, 44.97548797517455, 44.94797846153846, 44.9455939047619, 44.910117647058826, 44.88617040358744, 44.86545696835092, 44.86558503026968, 44.90363345454546, 44.87501062871554, 44.93104802713802, 44.93590804597701, 44.90068409051044, 44.87467130434782, 44.891225650749874, 44.9156841025641, 44.92164610169491, 44.89092118971601, 44.9027482086319, 44.86017701453105, 44.87692428711898, 44.8651103526735, 44.8483070967742, 44.79754023356263, 44.787605776860815, 44.7891175822446, 44.778797499999996, 44.778773283743995, 44.79586406274028, 44.77517923664122, 44.62143350499849, 44.48119933904161, 44.38141850746269, 44.425831753554505, 44.390321423320096, 44.31049810218978, 44.124642318840586, 44.01107798561151, 44.02091885714285, 44.02881950942861, 44.019682253521125, 44.071147412587415, 44.08087753401833, 44.09411310344827, 44.12980821917809, 44.07751870493811, 44.09083445854713, 44.09876506104924, 44.08840634582056, 44.13032264900663, 44.09793658729115, 44.106114509803916, 44.06832567199065, 44.096165139981935, 44.116, 44.10581480066234, 44.09257851448817, 44.075851106639846, 44.074916, 44.091910073282826, 44.09682567901235, 44.07930895705522, 44.053896892138944, 44.06240019391589, 44.06116150788872, 44.1180531736527, 44.15990285714285, 44.134685126020585, 44.18049829431832, 44.158170252572496, 44.21358883720931, 44.16577849710982, 44.1291108045977, 44.16022671694664, 44.1493200045439, 44.1491597740113, 44.15741842696629, 44.177169162011175, 44.21226797022553, 44.166602585349686, 44.17985497692815, 44.168419672131144, 44.19028366481904, 44.173513455095645, 44.162090322580646, 44.123481608212145, 44.155531914893615, 44.18765714285714, 44.224294736842104, 44.21350366492147, 44.168821324448146, 44.168, 44.17472356215214, 44.187647999999996, 44.18084571428571, 44.18716962744899, 44.19440137359862, 44.1429404079992, 44.15704}
24 | // data := []float64{0, 1550.14016, 1102.92032, 759.1852799999999, 601.88672, 502.06617600000004, 435.71381333333335, 385.54944, 348.24736, 316.40547555555554, 289.261344, 267.38202181818184, 252.83786666666668, 245.42818461538462, 245.35842285714287, 235.63543466666667, 227.58704000000003, 220.20446117647057, 213.48643555555554, 207.48786526315791, 201.919424, 197.19868952380952, 192.2912581818182, 188.60752695652172, 185.09968, 181.7553664, 178.6282953846154, 175.99228444444446, 173.34433142857142, 170.91121655172415, 168.496128, 166.37638193548386, 163.99491999999998, 161.9206012121212, 160.24624, 158.76464457142856, 157.2420711111111, 155.45751351351353, 154.00374736842107, 152.63651282051282, 151.112704, 149.88837463414634, 149.02825142857142, 147.3117618604651, 144.63512, 145.58946844444446, 144.72223304347824, 143.76531744680852, 143.01374, 142.06400653061226, 141.0796992, 140.27383843137252, 139.51694153846157, 138.54825660377358, 138.22770370370372, 137.21800727272725, 136.98238857142854, 135.9439045614035, 135.45676137931034, 135.40977898305084, 134.70067622540847, 134.12748048540504, 133.63318709677418, 133.16014222222222, 132.703765, 132.20392861538463, 131.7192387878788, 131.27342328358208, 130.71865882352944, 130.23932753623188, 129.99216914285716, 129.28652169014086, 129.16500888888888, 128.67712438356165, 128.25776864864866, 127.85582506666665, 127.45831157894739, 127.03613922077922, 126.90191179487178, 126.59213772151901, 126.425936, 126.0965688888889, 125.77238634146343, 125.49601735357918, 125.1707276190476, 124.89910964705884, 124.60520186046512, 124.29531218390805, 124.02556363636364, 123.47192808988764, 123.56910222222223, 123.33042637362638, 123.09693913043478, 122.809328172043, 122.58635234042553, 122.41840168421052, 122.15547666666666, 121.92927999999999, 121.73630367346938, 121.48462222222223, 121.29761599999999, 121.06910415841584, 120.87151686274511, 120.68683805825242, 120.51469538461538, 120.2012220952381, 120.15477433962265, 119.2894474766355, 119.77253037037036, 119.57481394495413, 119.31698327272728, 119.10490234234233, 118.99255142857143, 118.76022088495574, 118.8147452631579, 118.6306587826087, 118.51604413793103, 118.38956854700855, 118.1484366101695, 117.98398924369748, 117.95854933333334, 117.80418115702479, 117.68285901639344, 117.32433170731707, 117.36720000000001, 117.21502720000001, 117.10605714285714, 117.01992566929134, 116.88315749999998, 116.77652093023256, 116.65271384615386, 116.51296488549619, 116.39656969696969, 116.29855518796992, 116.18375402985075, 116.00662992592594, 115.96002117647059, 115.85702540145985, 115.77244985507247, 115.67459145200748, 115.48589942857143, 115.44551716312057, 115.35635154929577, 115.21038993006992, 115.11760666666666, 115.01394537931034, 114.97817863013698, 114.80941278911564, 114.72184648648648, 114.64405261744967, 114.55408426666666, 114.41580291390729, 114.3308, 114.27059869281045, 114.18505260423433, 114.14871122580645, 113.99949128205128, 113.94442191082803, 113.83386734177216, 113.59835371069182, 113.515388, 113.6068849689441, 113.52565728395062, 113.47399852760736, 113.35184, 113.3104446060606, 113.19637204819277, 113.12842730538924, 113.09605333333333, 113.08127621301774, 112.81183811764706, 112.9294203508772, 112.81810418604651, 112.69997317919075, 112.63358712643677, 112.5452672, 112.50175090909092, 112.52005423728812, 112.43650516853933, 112.34864983240223, 112.20527644444445, 112.20484950276244, 112.19415912087912, 112.13252546448088, 112.04997913043476, 112.02885535135137, 111.93645075268819, 111.89874994652408, 111.80677787234042, 111.73926264550266, 111.75360336842105, 111.62827225130889, 111.64060833333332, 111.58019481865286, 111.51077113402062, 111.32106666666667, 111.43125714285713, 111.37313757741902, 111.2588913131313, 111.27499738693469, 111.22497689768976}
25 | w := NewWelford(time.Second*5, time.Millisecond*50)
26 | ok := false
27 | for i, x := range data {
28 | if w.Update(x, x) {
29 | ok = true
30 | break
31 | }
32 | fmt.Printf("[%d] %s\n", i, w)
33 | }
34 | if !ok {
35 | t.Fatal("TestWOM failed")
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/speedtest.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "log"
9 | "os"
10 | "strconv"
11 | "strings"
12 | "sync"
13 | "sync/atomic"
14 | "time"
15 |
16 | "github.com/showwin/speedtest-go/speedtest/transport"
17 | "gopkg.in/alecthomas/kingpin.v2"
18 |
19 | "github.com/showwin/speedtest-go/speedtest"
20 | )
21 |
22 | var (
23 | showList = kingpin.Flag("list", "Show available speedtest.net servers.").Short('l').Bool()
24 | serverIds = kingpin.Flag("server", "Select server id to run speedtest.").Short('s').Ints()
25 | customURL = kingpin.Flag("custom-url", "Specify the url of the server instead of fetching from speedtest.net.").String()
26 | savingMode = kingpin.Flag("saving-mode", "Test with few resources, though low accuracy (especially > 30Mbps).").Bool()
27 | jsonOutput = kingpin.Flag("json", "Output results in json format.").Bool()
28 | jsonlOutput = kingpin.Flag("jsonl", "Output results in jsonl format (one json object per line).").Bool()
29 | unixOutput = kingpin.Flag("unix", "Output results in unix like format.").Bool()
30 | location = kingpin.Flag("location", "Change the location with a precise coordinate (format: lat,lon).").String()
31 | city = kingpin.Flag("city", "Change the location with a predefined city label.").String()
32 | showCityList = kingpin.Flag("city-list", "List all predefined city labels.").Bool()
33 | proxy = kingpin.Flag("proxy", "Set a proxy(http[s] or socks) for the speedtest.").String()
34 | source = kingpin.Flag("source", "Bind a source interface for the speedtest.").String()
35 | dnsBindSource = kingpin.Flag("dns-bind-source", "DNS request binding source (experimental).").Bool()
36 | multi = kingpin.Flag("multi", "Enable multi-server mode.").Short('m').Bool()
37 | thread = kingpin.Flag("thread", "Set the number of concurrent connections.").Short('t').Int()
38 | search = kingpin.Flag("search", "Fuzzy search servers by a keyword.").String()
39 | userAgent = kingpin.Flag("ua", "Set the user-agent header for the speedtest.").String()
40 | noDownload = kingpin.Flag("no-download", "Disable download test.").Bool()
41 | noUpload = kingpin.Flag("no-upload", "Disable upload test.").Bool()
42 | pingMode = kingpin.Flag("ping-mode", "Select a method for Ping (support icmp/tcp/http).").Default("http").String()
43 | unit = kingpin.Flag("unit", "Set human-readable and auto-scaled rate units for output (options: decimal-bits/decimal-bytes/binary-bits/binary-bytes).").Short('u').String()
44 | debug = kingpin.Flag("debug", "Enable debug mode.").Short('d').Bool()
45 | )
46 |
47 | var (
48 | commit = "dev"
49 | date = "unknown"
50 | )
51 |
52 | func main() {
53 | kingpin.Version(fmt.Sprintf("speedtest-go v%s git-%s built at %s", speedtest.Version(), commit, date))
54 | kingpin.Parse()
55 | AppInfo()
56 |
57 | speedtest.SetUnit(parseUnit(*unit))
58 |
59 | // discard standard log.
60 | log.SetOutput(io.Discard)
61 |
62 | // start unix output for saving mode by default.
63 | if *savingMode && !*jsonOutput && !*jsonlOutput && !*unixOutput {
64 | *unixOutput = true
65 | }
66 |
67 | // 0. speed test setting
68 | var speedtestClient = speedtest.New(speedtest.WithUserConfig(
69 | &speedtest.UserConfig{
70 | UserAgent: *userAgent,
71 | Proxy: *proxy,
72 | Source: *source,
73 | DnsBindSource: *dnsBindSource,
74 | Debug: *debug,
75 | PingMode: parseProto(*pingMode), // TCP as default
76 | SavingMode: *savingMode,
77 | MaxConnections: *thread,
78 | CityFlag: *city,
79 | LocationFlag: *location,
80 | Keyword: *search,
81 | }))
82 |
83 | if *showCityList {
84 | speedtest.PrintCityList()
85 | return
86 | }
87 |
88 | // 1. retrieving user information
89 | taskManager := InitTaskManager(*jsonOutput || *jsonlOutput, *unixOutput)
90 | taskManager.AsyncRun("Retrieving User Information", func(task *Task) {
91 | u, err := speedtestClient.FetchUserInfo()
92 | task.CheckError(err)
93 | task.Printf("ISP: %s", u.String())
94 | task.Complete()
95 | })
96 |
97 | // 2. retrieving servers
98 | var err error
99 | var servers speedtest.Servers
100 | var targets speedtest.Servers
101 | taskManager.Run("Retrieving Servers", func(task *Task) {
102 | if len(*customURL) > 0 {
103 | var target *speedtest.Server
104 | target, err = speedtestClient.CustomServer(*customURL)
105 | task.CheckError(err)
106 | targets = []*speedtest.Server{target}
107 | task.Println("Skip: Using Custom Server")
108 | } else if len(*serverIds) > 0 {
109 | // TODO: need async fetch to speedup
110 | for _, id := range *serverIds {
111 | serverPtr, errFetch := speedtestClient.FetchServerByID(strconv.Itoa(id))
112 | if errFetch != nil {
113 | continue // Silently Skip all ids that actually don't exist.
114 | }
115 | targets = append(targets, serverPtr)
116 | }
117 | task.CheckError(err)
118 | task.Printf("Found %d Specified Public Server(s)", len(targets))
119 | } else {
120 | servers, err = speedtestClient.FetchServers()
121 | task.CheckError(err)
122 | task.Printf("Found %d Public Servers", len(servers))
123 | if *showList {
124 | task.Complete()
125 | task.manager.Reset()
126 | showServerList(servers)
127 | os.Exit(0)
128 | }
129 | targets, err = servers.FindServer(*serverIds)
130 | task.CheckError(err)
131 | }
132 | task.Complete()
133 | })
134 | taskManager.Reset()
135 |
136 | // 3. test each selected server with ping, download and upload.
137 | for _, server := range targets {
138 | if !*jsonOutput && !*jsonlOutput {
139 | fmt.Println()
140 | }
141 | taskManager.Println("Test Server: " + server.String())
142 | taskManager.Run("Latency: --", func(task *Task) {
143 | task.CheckError(server.PingTest(func(latency time.Duration) {
144 | task.Updatef("Latency: %v", latency)
145 | }))
146 | task.Printf("Latency: %v Jitter: %v Min: %v Max: %v", server.Latency, server.Jitter, server.MinLatency, server.MaxLatency)
147 | task.Complete()
148 | })
149 |
150 | // 3.0 create a packet loss analyzer, use default options
151 | analyzer := speedtest.NewPacketLossAnalyzer(&speedtest.PacketLossAnalyzerOptions{
152 | SourceInterface: *source,
153 | })
154 |
155 | blocker := sync.WaitGroup{}
156 | packetLossAnalyzerCtx, packetLossAnalyzerCancel := context.WithTimeout(context.Background(), time.Second*40)
157 | taskManager.Run("Packet Loss Analyzer", func(task *Task) {
158 | blocker.Add(1)
159 | go func() {
160 | defer blocker.Done()
161 | err = analyzer.RunWithContext(packetLossAnalyzerCtx, server.Host, func(packetLoss *transport.PLoss) {
162 | server.PacketLoss = *packetLoss
163 | })
164 | if errors.Is(err, transport.ErrUnsupported) {
165 | packetLossAnalyzerCancel() // cancel early
166 | }
167 | }()
168 | task.Println("Packet Loss Analyzer: Running in background (<= 30 Secs)")
169 | task.Complete()
170 | })
171 |
172 | // 3.1 create accompany Echo
173 | accEcho := newAccompanyEcho(server, time.Millisecond*500)
174 | taskManager.RunWithTrigger(!*noDownload, "Download", func(task *Task) {
175 | accEcho.Run()
176 | speedtestClient.SetCallbackDownload(func(downRate speedtest.ByteRate) {
177 | lc := accEcho.CurrentLatency()
178 | if lc == 0 {
179 | task.Updatef("Download: %s (Latency: --)", downRate)
180 | } else {
181 | task.Updatef("Download: %s (Latency: %dms)", downRate, lc/1000000)
182 | }
183 | })
184 | if *multi {
185 | task.CheckError(server.MultiDownloadTestContext(context.Background(), servers))
186 | } else {
187 | task.CheckError(server.DownloadTest())
188 | }
189 | accEcho.Stop()
190 | mean, _, std, minL, maxL := speedtest.StandardDeviation(accEcho.Latencies())
191 | task.Printf("Download: %s (Used: %.2fMB) (Latency: %dms Jitter: %dms Min: %dms Max: %dms)", server.DLSpeed, float64(server.Context.Manager.GetTotalDownload())/1000/1000, mean/1000000, std/1000000, minL/1000000, maxL/1000000)
192 | task.Complete()
193 | })
194 |
195 | taskManager.RunWithTrigger(!*noUpload, "Upload", func(task *Task) {
196 | accEcho.Run()
197 | speedtestClient.SetCallbackUpload(func(upRate speedtest.ByteRate) {
198 | lc := accEcho.CurrentLatency()
199 | if lc == 0 {
200 | task.Updatef("Upload: %s (Latency: --)", upRate)
201 | } else {
202 | task.Updatef("Upload: %s (Latency: %dms)", upRate, lc/1000000)
203 | }
204 | })
205 | if *multi {
206 | task.CheckError(server.MultiUploadTestContext(context.Background(), servers))
207 | } else {
208 | task.CheckError(server.UploadTest())
209 | }
210 | accEcho.Stop()
211 | mean, _, std, minL, maxL := speedtest.StandardDeviation(accEcho.Latencies())
212 | task.Printf("Upload: %s (Used: %.2fMB) (Latency: %dms Jitter: %dms Min: %dms Max: %dms)", server.ULSpeed, float64(server.Context.Manager.GetTotalUpload())/1000/1000, mean/1000000, std/1000000, minL/1000000, maxL/1000000)
213 | task.Complete()
214 | })
215 |
216 | if *noUpload && *noDownload {
217 | time.Sleep(time.Second * 30)
218 | }
219 | packetLossAnalyzerCancel()
220 | blocker.Wait()
221 | if !*jsonOutput && !*jsonlOutput {
222 | taskManager.Println(server.PacketLoss.String())
223 | }
224 | taskManager.Reset()
225 | speedtestClient.Manager.Reset()
226 | }
227 | taskManager.Stop()
228 |
229 | if *jsonOutput {
230 | json, errMarshal := speedtestClient.JSON(targets)
231 | if errMarshal != nil {
232 | panic(errMarshal)
233 | }
234 | fmt.Print(string(json))
235 | } else if *jsonlOutput {
236 | for _, server := range targets {
237 | json, errMarshal := speedtestClient.JSONL(server)
238 | if errMarshal != nil {
239 | panic(errMarshal)
240 | }
241 | fmt.Println(string(json))
242 | }
243 | }
244 | }
245 |
246 | type AccompanyEcho struct {
247 | stopEcho chan bool
248 | server *speedtest.Server
249 | currentLatency int64
250 | interval time.Duration
251 | latencies []int64
252 | }
253 |
254 | func newAccompanyEcho(server *speedtest.Server, interval time.Duration) *AccompanyEcho {
255 | return &AccompanyEcho{
256 | server: server,
257 | interval: interval,
258 | stopEcho: make(chan bool),
259 | }
260 | }
261 |
262 | func (ae *AccompanyEcho) Run() {
263 | ae.latencies = make([]int64, 0)
264 | ctx, cancel := context.WithCancel(context.Background())
265 | go func() {
266 | for {
267 | select {
268 | case <-ae.stopEcho:
269 | cancel()
270 | return
271 | default:
272 | latency, _ := ae.server.HTTPPing(ctx, 1, ae.interval, nil)
273 | if len(latency) > 0 {
274 | atomic.StoreInt64(&ae.currentLatency, latency[0])
275 | ae.latencies = append(ae.latencies, latency[0])
276 | }
277 | }
278 | }
279 | }()
280 | }
281 |
282 | func (ae *AccompanyEcho) Stop() {
283 | ae.stopEcho <- false
284 | }
285 |
286 | func (ae *AccompanyEcho) CurrentLatency() int64 {
287 | return atomic.LoadInt64(&ae.currentLatency)
288 | }
289 |
290 | func (ae *AccompanyEcho) Latencies() []int64 {
291 | return ae.latencies
292 | }
293 |
294 | func showServerList(servers speedtest.Servers) {
295 | for _, s := range servers {
296 | fmt.Printf("[%5s] %9.2fkm ", s.ID, s.Distance)
297 |
298 | if s.Latency == -1 {
299 | fmt.Printf("%v", "Timeout ")
300 | } else {
301 | fmt.Printf("%-dms ", s.Latency/time.Millisecond)
302 | }
303 | fmt.Printf("\t%s (%s) by %s \n", s.Name, s.Country, s.Sponsor)
304 | }
305 | }
306 |
307 | func parseUnit(str string) speedtest.UnitType {
308 | str = strings.ToLower(str)
309 | if str == "decimal-bits" {
310 | return speedtest.UnitTypeDecimalBits
311 | } else if str == "decimal-bytes" {
312 | return speedtest.UnitTypeDecimalBytes
313 | } else if str == "binary-bits" {
314 | return speedtest.UnitTypeBinaryBits
315 | } else if str == "binary-bytes" {
316 | return speedtest.UnitTypeBinaryBytes
317 | } else {
318 | return speedtest.UnitTypeDefaultMbps
319 | }
320 | }
321 |
322 | func parseProto(str string) speedtest.Proto {
323 | str = strings.ToLower(str)
324 | if str == "icmp" {
325 | return speedtest.ICMP
326 | } else if str == "tcp" {
327 | return speedtest.TCP
328 | } else {
329 | return speedtest.HTTP
330 | }
331 | }
332 |
333 | func AppInfo() {
334 | if !*jsonOutput && !*jsonlOutput {
335 | fmt.Println()
336 | fmt.Printf(" speedtest-go v%s (git-%s) @showwin\n", speedtest.Version(), commit)
337 | fmt.Println()
338 | }
339 | }
340 |
--------------------------------------------------------------------------------
/speedtest/server.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "encoding/xml"
7 | "errors"
8 | "fmt"
9 | "math"
10 | "net/http"
11 | "net/url"
12 | "sort"
13 | "strconv"
14 | "strings"
15 | "sync"
16 | "time"
17 |
18 | "github.com/showwin/speedtest-go/speedtest/transport"
19 | )
20 |
21 | const (
22 | speedTestServersUrl = "https://www.speedtest.net/api/js/servers"
23 | speedTestServersAlternativeUrl = "https://www.speedtest.net/speedtest-servers-static.php"
24 | speedTestServersAdvanced = "https://www.speedtest.net/api/ios-config.php"
25 | )
26 |
27 | type payloadType int
28 |
29 | const (
30 | typeJSONPayload payloadType = iota
31 | typeXMLPayload
32 | )
33 |
34 | var (
35 | ErrServerNotFound = errors.New("no server available or found")
36 | )
37 |
38 | // Server information
39 | type Server struct {
40 | URL string `xml:"url,attr" json:"url"`
41 | Lat string `xml:"lat,attr" json:"lat"`
42 | Lon string `xml:"lon,attr" json:"lon"`
43 | Name string `xml:"name,attr" json:"name"`
44 | Country string `xml:"country,attr" json:"country"`
45 | Sponsor string `xml:"sponsor,attr" json:"sponsor"`
46 | ID string `xml:"id,attr" json:"id"`
47 | Host string `xml:"host,attr" json:"host"`
48 | Distance float64 `json:"distance"`
49 | Latency time.Duration `json:"latency"`
50 | MaxLatency time.Duration `json:"max_latency"`
51 | MinLatency time.Duration `json:"min_latency"`
52 | Jitter time.Duration `json:"jitter"`
53 | DLSpeed ByteRate `json:"dl_speed"`
54 | ULSpeed ByteRate `json:"ul_speed"`
55 | TestDuration TestDuration `json:"test_duration"`
56 | PacketLoss transport.PLoss `json:"packet_loss"`
57 |
58 | Context *Speedtest `json:"-"`
59 | }
60 |
61 | type TestDuration struct {
62 | Ping *time.Duration `json:"ping"`
63 | Download *time.Duration `json:"download"`
64 | Upload *time.Duration `json:"upload"`
65 | Total *time.Duration `json:"total"`
66 | }
67 |
68 | // CustomServer use defaultClient, given a URL string, return a new Server object, with as much
69 | // filled in as we can
70 | func CustomServer(host string) (*Server, error) {
71 | return defaultClient.CustomServer(host)
72 | }
73 |
74 | // CustomServer given a URL string, return a new Server object, with as much
75 | // filled in as we can
76 | func (s *Speedtest) CustomServer(host string) (*Server, error) {
77 | u, err := url.Parse(host)
78 | if err != nil {
79 | return nil, err
80 | }
81 | u.Path = "/speedtest/upload.php"
82 | parseHost := u.String()
83 | return &Server{
84 | ID: "Custom",
85 | Lat: "?",
86 | Lon: "?",
87 | Country: "?",
88 | URL: parseHost,
89 | Name: u.Host,
90 | Host: u.Host,
91 | Sponsor: "?",
92 | Context: s,
93 | }, nil
94 | }
95 |
96 | // ServerList list of Server
97 | // Users(Client) also exists with @param speedTestServersAdvanced
98 | type ServerList struct {
99 | Servers []*Server `xml:"servers>server"`
100 | Users []User `xml:"client"`
101 | }
102 |
103 | // Servers for sorting servers.
104 | type Servers []*Server
105 |
106 | // ByDistance for sorting servers.
107 | type ByDistance struct {
108 | Servers
109 | }
110 |
111 | func (servers Servers) Available() *Servers {
112 | retServer := Servers{}
113 | for _, server := range servers {
114 | if server.Latency != PingTimeout {
115 | retServer = append(retServer, server)
116 | }
117 | }
118 | for i := 0; i < len(retServer)-1; i++ {
119 | for j := 0; j < len(retServer)-i-1; j++ {
120 | if retServer[j].Latency > retServer[j+1].Latency {
121 | retServer[j], retServer[j+1] = retServer[j+1], retServer[j]
122 | }
123 | }
124 | }
125 | return &retServer
126 | }
127 |
128 | // Len finds length of servers. For sorting servers.
129 | func (servers Servers) Len() int {
130 | return len(servers)
131 | }
132 |
133 | // Swap swaps i-th and j-th. For sorting servers.
134 | func (servers Servers) Swap(i, j int) {
135 | servers[i], servers[j] = servers[j], servers[i]
136 | }
137 |
138 | // Hosts return hosts of servers
139 | func (servers Servers) Hosts() []string {
140 | var retServer []string
141 | for _, server := range servers {
142 | retServer = append(retServer, server.Host)
143 | }
144 | return retServer
145 | }
146 |
147 | // Less compares the distance. For sorting servers.
148 | func (b ByDistance) Less(i, j int) bool {
149 | return b.Servers[i].Distance < b.Servers[j].Distance
150 | }
151 |
152 | // FetchServerByID retrieves a server by given serverID.
153 | func (s *Speedtest) FetchServerByID(serverID string) (*Server, error) {
154 | return s.FetchServerByIDContext(context.Background(), serverID)
155 | }
156 |
157 | // FetchServerByID retrieves a server by given serverID.
158 | func FetchServerByID(serverID string) (*Server, error) {
159 | return defaultClient.FetchServerByID(serverID)
160 | }
161 |
162 | // FetchServerByIDContext retrieves a server by given serverID, observing the given context.
163 | func (s *Speedtest) FetchServerByIDContext(ctx context.Context, serverID string) (*Server, error) {
164 | u, err := url.Parse(speedTestServersAdvanced)
165 | if err != nil {
166 | return nil, err
167 | }
168 | query := u.Query()
169 | query.Set(strings.ToLower("serverID"), serverID)
170 | u.RawQuery = query.Encode()
171 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
172 | if err != nil {
173 | return nil, err
174 | }
175 | resp, err := s.doer.Do(req)
176 | if err != nil {
177 | return nil, err
178 | }
179 | defer resp.Body.Close()
180 | var list ServerList
181 | decoder := xml.NewDecoder(resp.Body)
182 | if err = decoder.Decode(&list); err != nil {
183 | return nil, err
184 | }
185 |
186 | for i := range list.Servers {
187 | if list.Servers[i].ID == serverID {
188 | list.Servers[i].Context = s
189 | if len(list.Users) > 0 {
190 | sLat, _ := strconv.ParseFloat(list.Servers[i].Lat, 64)
191 | sLon, _ := strconv.ParseFloat(list.Servers[i].Lon, 64)
192 | uLat, _ := strconv.ParseFloat(list.Users[0].Lat, 64)
193 | uLon, _ := strconv.ParseFloat(list.Users[0].Lon, 64)
194 | list.Servers[i].Distance = distance(sLat, sLon, uLat, uLon)
195 | }
196 | return list.Servers[i], err
197 | }
198 | }
199 | return nil, ErrServerNotFound
200 | }
201 |
202 | // FetchServers retrieves a list of available servers
203 | func (s *Speedtest) FetchServers() (Servers, error) {
204 | return s.FetchServerListContext(context.Background())
205 | }
206 |
207 | // FetchServers retrieves a list of available servers
208 | func FetchServers() (Servers, error) {
209 | return defaultClient.FetchServers()
210 | }
211 |
212 | // FetchServerListContext retrieves a list of available servers, observing the given context.
213 | func (s *Speedtest) FetchServerListContext(ctx context.Context) (Servers, error) {
214 | u, err := url.Parse(speedTestServersUrl)
215 | if err != nil {
216 | return Servers{}, err
217 | }
218 | query := u.Query()
219 | if len(s.config.Keyword) > 0 {
220 | query.Set("search", s.config.Keyword)
221 | }
222 | if s.config.Location != nil {
223 | query.Set("lat", strconv.FormatFloat(s.config.Location.Lat, 'f', -1, 64))
224 | query.Set("lon", strconv.FormatFloat(s.config.Location.Lon, 'f', -1, 64))
225 | }
226 | u.RawQuery = query.Encode()
227 | dbg.Printf("Retrieving servers: %s\n", u.String())
228 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
229 | if err != nil {
230 | return Servers{}, err
231 | }
232 |
233 | resp, err := s.doer.Do(req)
234 | if err != nil {
235 | return Servers{}, err
236 | }
237 |
238 | _payloadType := typeJSONPayload
239 |
240 | if resp.ContentLength == 0 {
241 | _ = resp.Body.Close()
242 |
243 | req, err = http.NewRequestWithContext(ctx, http.MethodGet, speedTestServersAlternativeUrl, nil)
244 | if err != nil {
245 | return Servers{}, err
246 | }
247 |
248 | resp, err = s.doer.Do(req)
249 | if err != nil {
250 | return Servers{}, err
251 | }
252 |
253 | _payloadType = typeXMLPayload
254 | }
255 |
256 | defer resp.Body.Close()
257 |
258 | var servers Servers
259 |
260 | switch _payloadType {
261 | case typeJSONPayload:
262 | // Decode xml
263 | decoder := json.NewDecoder(resp.Body)
264 |
265 | if err = decoder.Decode(&servers); err != nil {
266 | return servers, err
267 | }
268 | case typeXMLPayload:
269 | var list ServerList
270 | // Decode xml
271 | decoder := xml.NewDecoder(resp.Body)
272 |
273 | if err = decoder.Decode(&list); err != nil {
274 | return servers, err
275 | }
276 |
277 | servers = list.Servers
278 | default:
279 | return servers, errors.New("response payload decoding not implemented")
280 | }
281 |
282 | dbg.Printf("Servers Num: %d\n", len(servers))
283 | // set doer of server
284 | for _, server := range servers {
285 | server.Context = s
286 | }
287 |
288 | // ping once
289 | var wg sync.WaitGroup
290 | pCtx, fc := context.WithTimeout(context.Background(), time.Second*4)
291 | dbg.Println("Echo each server...")
292 | for _, server := range servers {
293 | wg.Add(1)
294 | go func(gs *Server) {
295 | var latency []int64
296 | var errPing error
297 | if s.config.PingMode == TCP {
298 | latency, errPing = gs.TCPPing(pCtx, 1, time.Millisecond, nil)
299 | } else if s.config.PingMode == ICMP {
300 | latency, errPing = gs.ICMPPing(pCtx, 4*time.Second, 1, time.Millisecond, nil)
301 | } else {
302 | latency, errPing = gs.HTTPPing(pCtx, 1, time.Millisecond, nil)
303 | }
304 | if errPing != nil || len(latency) < 1 {
305 | gs.Latency = PingTimeout
306 | } else {
307 | gs.Latency = time.Duration(latency[0]) * time.Nanosecond
308 | }
309 | wg.Done()
310 | }(server)
311 | }
312 | wg.Wait()
313 | fc()
314 |
315 | // Calculate distance
316 | // If we don't call FetchUserInfo() before FetchServers(),
317 | // we don't calculate the distance, instead we use the
318 | // remote computing distance provided by Ookla as default.
319 | if s.User != nil {
320 | for _, server := range servers {
321 | sLat, _ := strconv.ParseFloat(server.Lat, 64)
322 | sLon, _ := strconv.ParseFloat(server.Lon, 64)
323 | uLat, _ := strconv.ParseFloat(s.User.Lat, 64)
324 | uLon, _ := strconv.ParseFloat(s.User.Lon, 64)
325 | server.Distance = distance(sLat, sLon, uLat, uLon)
326 | }
327 | }
328 |
329 | // Sort by distance
330 | sort.Sort(ByDistance{servers})
331 |
332 | if len(servers) <= 0 {
333 | return servers, ErrServerNotFound
334 | }
335 | return servers, nil
336 | }
337 |
338 | // FetchServerListContext retrieves a list of available servers, observing the given context.
339 | func FetchServerListContext(ctx context.Context) (Servers, error) {
340 | return defaultClient.FetchServerListContext(ctx)
341 | }
342 |
343 | func distance(lat1 float64, lon1 float64, lat2 float64, lon2 float64) float64 {
344 | radius := 6378.137
345 |
346 | phi1 := lat1 * math.Pi / 180.0
347 | phi2 := lat2 * math.Pi / 180.0
348 |
349 | deltaPhiHalf := (lat1 - lat2) * math.Pi / 360.0
350 | deltaLambdaHalf := (lon1 - lon2) * math.Pi / 360.0
351 | sinePhiHalf2 := math.Sin(deltaPhiHalf)*math.Sin(deltaPhiHalf) + math.Cos(phi1)*math.Cos(phi2)*math.Sin(deltaLambdaHalf)*math.Sin(deltaLambdaHalf) // phi half-angle sine ^ 2
352 | delta := 2 * math.Atan2(math.Sqrt(sinePhiHalf2), math.Sqrt(1-sinePhiHalf2)) // 2 arc sine
353 | return radius * delta // r * delta
354 | }
355 |
356 | // FindServer finds server by serverID in given server list.
357 | // If the id is not found in the given list, return the server with the lowest latency.
358 | func (servers Servers) FindServer(serverID []int) (Servers, error) {
359 | retServer := Servers{}
360 |
361 | if len(servers) <= 0 {
362 | return retServer, ErrServerNotFound
363 | }
364 |
365 | for _, sid := range serverID {
366 | for _, s := range servers {
367 | id, _ := strconv.Atoi(s.ID)
368 | if sid == id {
369 | retServer = append(retServer, s)
370 | break
371 | }
372 | }
373 | }
374 |
375 | if len(retServer) == 0 {
376 | // choose the lowest latency server
377 | var minLatency int64 = math.MaxInt64
378 | var minServerIndex int
379 | for index, server := range servers {
380 | if server.Latency <= 0 {
381 | continue
382 | }
383 | if minLatency > server.Latency.Milliseconds() {
384 | minLatency = server.Latency.Milliseconds()
385 | minServerIndex = index
386 | }
387 | }
388 | retServer = append(retServer, servers[minServerIndex])
389 | }
390 | return retServer, nil
391 | }
392 |
393 | // String representation of ServerList
394 | func (servers ServerList) String() string {
395 | slr := ""
396 | for _, server := range servers.Servers {
397 | slr += server.String()
398 | }
399 | return slr
400 | }
401 |
402 | // String representation of Servers
403 | func (servers Servers) String() string {
404 | slr := ""
405 | for _, server := range servers {
406 | slr += server.String()
407 | }
408 | return slr
409 | }
410 |
411 | // String representation of Server
412 | func (s *Server) String() string {
413 | if s.Sponsor == "?" {
414 | return fmt.Sprintf("[%4s] %s", s.ID, s.Name)
415 | }
416 | if len(s.Country) == 0 {
417 | return fmt.Sprintf("[%4s] %.2fkm %s by %s", s.ID, s.Distance, s.Name, s.Sponsor)
418 | }
419 | return fmt.Sprintf("[%4s] %.2fkm %s (%s) by %s", s.ID, s.Distance, s.Name, s.Country, s.Sponsor)
420 | }
421 |
422 | // CheckResultValid checks that results are logical given UL and DL speeds
423 | func (s *Server) CheckResultValid() bool {
424 | return !(s.DLSpeed*100 < s.ULSpeed) || !(s.DLSpeed > s.ULSpeed*100)
425 | }
426 |
427 | func (s *Server) testDurationTotalCount() {
428 | total := s.getNotNullValue(s.TestDuration.Ping) +
429 | s.getNotNullValue(s.TestDuration.Download) +
430 | s.getNotNullValue(s.TestDuration.Upload)
431 |
432 | s.TestDuration.Total = &total
433 | }
434 |
435 | func (s *Server) getNotNullValue(time *time.Duration) time.Duration {
436 | if time == nil {
437 | return 0
438 | }
439 | return *time
440 | }
441 |
--------------------------------------------------------------------------------
/speedtest/request.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "github.com/showwin/speedtest-go/speedtest/transport"
8 | "io"
9 | "math"
10 | "net/http"
11 | "net/url"
12 | "path"
13 | "strings"
14 | "sync/atomic"
15 | "time"
16 | )
17 |
18 | type (
19 | downloadFunc func(context.Context, *Server, int) error
20 | uploadFunc func(context.Context, *Server, int) error
21 | )
22 |
23 | var (
24 | dlSizes = [...]int{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000}
25 | ulSizes = [...]int{100, 300, 500, 800, 1000, 1500, 2500, 3000, 3500, 4000} // kB
26 | )
27 |
28 | var (
29 | ErrConnectTimeout = errors.New("server connect timeout")
30 | )
31 |
32 | func (s *Server) MultiDownloadTestContext(ctx context.Context, servers Servers) error {
33 | ss := servers.Available()
34 | if ss.Len() == 0 {
35 | return errors.New("not found available servers")
36 | }
37 | mainIDIndex := 0
38 | var td *TestDirection
39 | _context, cancel := context.WithCancel(ctx)
40 | defer cancel()
41 | var errorTimes int64 = 0
42 | var requestTimes int64 = 0
43 | for i, server := range *ss {
44 | if server.ID == s.ID {
45 | mainIDIndex = i
46 | }
47 | sp := server
48 | dbg.Printf("Register Download Handler: %s\n", sp.URL)
49 | td = server.Context.RegisterDownloadHandler(func() {
50 | atomic.AddInt64(&requestTimes, 1)
51 | if err := downloadRequest(_context, sp, 3); err != nil {
52 | atomic.AddInt64(&errorTimes, 1)
53 | }
54 | })
55 | }
56 | if td == nil {
57 | return ErrorUninitializedManager
58 | }
59 | td.Start(cancel, mainIDIndex) // block here
60 | s.DLSpeed = ByteRate(td.manager.GetEWMADownloadRate())
61 | if s.DLSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 {
62 | s.DLSpeed = -1 // N/A
63 | }
64 | return nil
65 | }
66 |
67 | func (s *Server) MultiUploadTestContext(ctx context.Context, servers Servers) error {
68 | ss := servers.Available()
69 | if ss.Len() == 0 {
70 | return errors.New("not found available servers")
71 | }
72 | mainIDIndex := 0
73 | var td *TestDirection
74 | _context, cancel := context.WithCancel(ctx)
75 | defer cancel()
76 | var errorTimes int64 = 0
77 | var requestTimes int64 = 0
78 | for i, server := range *ss {
79 | if server.ID == s.ID {
80 | mainIDIndex = i
81 | }
82 | sp := server
83 | dbg.Printf("Register Upload Handler: %s\n", sp.URL)
84 | td = server.Context.RegisterUploadHandler(func() {
85 | atomic.AddInt64(&requestTimes, 1)
86 | if err := uploadRequest(_context, sp, 3); err != nil {
87 | atomic.AddInt64(&errorTimes, 1)
88 | }
89 | })
90 | }
91 | if td == nil {
92 | return ErrorUninitializedManager
93 | }
94 | td.Start(cancel, mainIDIndex) // block here
95 | s.ULSpeed = ByteRate(td.manager.GetEWMAUploadRate())
96 | if s.ULSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 {
97 | s.ULSpeed = -1 // N/A
98 | }
99 | return nil
100 | }
101 |
102 | // DownloadTest executes the test to measure download speed
103 | func (s *Server) DownloadTest() error {
104 | return s.downloadTestContext(context.Background(), downloadRequest)
105 | }
106 |
107 | // DownloadTestContext executes the test to measure download speed, observing the given context.
108 | func (s *Server) DownloadTestContext(ctx context.Context) error {
109 | return s.downloadTestContext(ctx, downloadRequest)
110 | }
111 |
112 | func (s *Server) downloadTestContext(ctx context.Context, downloadRequest downloadFunc) error {
113 | var errorTimes int64 = 0
114 | var requestTimes int64 = 0
115 | start := time.Now()
116 | _context, cancel := context.WithCancel(ctx)
117 | s.Context.RegisterDownloadHandler(func() {
118 | atomic.AddInt64(&requestTimes, 1)
119 | if err := downloadRequest(_context, s, 3); err != nil {
120 | atomic.AddInt64(&errorTimes, 1)
121 | }
122 | }).Start(cancel, 0)
123 | duration := time.Since(start)
124 | s.DLSpeed = ByteRate(s.Context.GetEWMADownloadRate())
125 | if s.DLSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 {
126 | s.DLSpeed = -1 // N/A
127 | }
128 | s.TestDuration.Download = &duration
129 | s.testDurationTotalCount()
130 | return nil
131 | }
132 |
133 | // UploadTest executes the test to measure upload speed
134 | func (s *Server) UploadTest() error {
135 | return s.uploadTestContext(context.Background(), uploadRequest)
136 | }
137 |
138 | // UploadTestContext executes the test to measure upload speed, observing the given context.
139 | func (s *Server) UploadTestContext(ctx context.Context) error {
140 | return s.uploadTestContext(ctx, uploadRequest)
141 | }
142 |
143 | func (s *Server) uploadTestContext(ctx context.Context, uploadRequest uploadFunc) error {
144 | var errorTimes int64 = 0
145 | var requestTimes int64 = 0
146 | start := time.Now()
147 | _context, cancel := context.WithCancel(ctx)
148 | s.Context.RegisterUploadHandler(func() {
149 | atomic.AddInt64(&requestTimes, 1)
150 | if err := uploadRequest(_context, s, 4); err != nil {
151 | atomic.AddInt64(&errorTimes, 1)
152 | }
153 | }).Start(cancel, 0)
154 | duration := time.Since(start)
155 | s.ULSpeed = ByteRate(s.Context.GetEWMAUploadRate())
156 | if s.ULSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 {
157 | s.ULSpeed = -1 // N/A
158 | }
159 | s.TestDuration.Upload = &duration
160 | s.testDurationTotalCount()
161 | return nil
162 | }
163 |
164 | func downloadRequest(ctx context.Context, s *Server, w int) error {
165 | size := dlSizes[w]
166 | u, err := url.Parse(s.URL)
167 | if err != nil {
168 | return err
169 | }
170 | u.Path = path.Dir(u.Path)
171 | xdlURL := u.JoinPath(fmt.Sprintf("random%dx%d.jpg", size, size)).String()
172 | dbg.Printf("XdlURL: %s\n", xdlURL)
173 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, xdlURL, nil)
174 | if err != nil {
175 | return err
176 | }
177 |
178 | resp, err := s.Context.doer.Do(req)
179 | if err != nil {
180 | return err
181 | }
182 | defer resp.Body.Close()
183 | return s.Context.NewChunk().DownloadHandler(resp.Body)
184 | }
185 |
186 | func uploadRequest(ctx context.Context, s *Server, w int) error {
187 | size := ulSizes[w]
188 | chunkSize := int64(size*100-51) * 10
189 | dc := s.Context.NewChunk().UploadHandler(chunkSize)
190 | req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.URL, io.NopCloser(dc))
191 | if err != nil {
192 | return err
193 | }
194 | req.ContentLength = chunkSize
195 | dbg.Printf("Len=%d, XulURL: %s\n", req.ContentLength, s.URL)
196 | req.Header.Set("Content-Type", "application/octet-stream")
197 | resp, err := s.Context.doer.Do(req)
198 | if err != nil {
199 | return err
200 | }
201 | _, _ = io.Copy(io.Discard, resp.Body)
202 | defer resp.Body.Close()
203 | return err
204 | }
205 |
206 | // PingTest executes test to measure latency
207 | func (s *Server) PingTest(callback func(latency time.Duration)) error {
208 | return s.PingTestContext(context.Background(), callback)
209 | }
210 |
211 | // PingTestContext executes test to measure latency, observing the given context.
212 | func (s *Server) PingTestContext(ctx context.Context, callback func(latency time.Duration)) (err error) {
213 | start := time.Now()
214 | var vectorPingResult []int64
215 | if s.Context.config.PingMode == TCP {
216 | vectorPingResult, err = s.TCPPing(ctx, 10, time.Millisecond*200, callback)
217 | } else if s.Context.config.PingMode == ICMP {
218 | vectorPingResult, err = s.ICMPPing(ctx, time.Second*4, 10, time.Millisecond*200, callback)
219 | } else {
220 | vectorPingResult, err = s.HTTPPing(ctx, 10, time.Millisecond*200, callback)
221 | }
222 | if err != nil || len(vectorPingResult) == 0 {
223 | return err
224 | }
225 | dbg.Printf("Before StandardDeviation: %v\n", vectorPingResult)
226 | mean, _, std, minLatency, maxLatency := StandardDeviation(vectorPingResult)
227 | duration := time.Since(start)
228 | s.Latency = time.Duration(mean) * time.Nanosecond
229 | s.Jitter = time.Duration(std) * time.Nanosecond
230 | s.MinLatency = time.Duration(minLatency) * time.Nanosecond
231 | s.MaxLatency = time.Duration(maxLatency) * time.Nanosecond
232 | s.TestDuration.Ping = &duration
233 | s.testDurationTotalCount()
234 | return nil
235 | }
236 |
237 | // TestAll executes ping, download and upload tests one by one
238 | func (s *Server) TestAll() error {
239 | err := s.PingTest(nil)
240 | if err != nil {
241 | return err
242 | }
243 | err = s.DownloadTest()
244 | if err != nil {
245 | return err
246 | }
247 | return s.UploadTest()
248 | }
249 |
250 | func (s *Server) TCPPing(
251 | ctx context.Context,
252 | echoTimes int,
253 | echoFreq time.Duration,
254 | callback func(latency time.Duration),
255 | ) (latencies []int64, err error) {
256 | var pingDst string
257 | if len(s.Host) == 0 {
258 | u, err := url.Parse(s.URL)
259 | if err != nil || len(u.Host) == 0 {
260 | return nil, err
261 | }
262 | pingDst = u.Host
263 | } else {
264 | pingDst = s.Host
265 | }
266 | failTimes := 0
267 | client, err := transport.NewClient(s.Context.tcpDialer)
268 | if err != nil {
269 | return nil, err
270 | }
271 | err = client.Connect(ctx, pingDst)
272 | if err != nil {
273 | return nil, err
274 | }
275 | for i := 0; i < echoTimes; i++ {
276 | latency, err := client.PingContext(ctx)
277 | if err != nil {
278 | failTimes++
279 | continue
280 | }
281 | latencies = append(latencies, latency)
282 | if callback != nil {
283 | callback(time.Duration(latency))
284 | }
285 | time.Sleep(echoFreq)
286 | }
287 | if failTimes == echoTimes {
288 | return nil, ErrConnectTimeout
289 | }
290 | return
291 | }
292 |
293 | func (s *Server) HTTPPing(
294 | ctx context.Context,
295 | echoTimes int,
296 | echoFreq time.Duration,
297 | callback func(latency time.Duration),
298 | ) (latencies []int64, err error) {
299 | var contextErr error
300 | u, err := url.Parse(s.URL)
301 | if err != nil || len(u.Host) == 0 {
302 | return nil, err
303 | }
304 | u.Path = path.Dir(u.Path)
305 | pingDst := u.JoinPath("latency.txt").String()
306 | dbg.Printf("Echo: %s\n", pingDst)
307 | failTimes := 0
308 | req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingDst, nil)
309 | if err != nil {
310 | return nil, err
311 | }
312 | // carry out an extra request to warm up the connection and ensure the first request is not going to affect the
313 | // overall estimation
314 | echoTimes++
315 | for i := 0; i < echoTimes; i++ {
316 | sTime := time.Now()
317 | resp, err := s.Context.doer.Do(req)
318 | endTime := time.Since(sTime)
319 | if err != nil {
320 | if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
321 | contextErr = err
322 | break
323 | }
324 |
325 | failTimes++
326 | continue
327 | }
328 | _, _ = io.Copy(io.Discard, resp.Body)
329 | _ = resp.Body.Close()
330 | if i > 0 {
331 | latency := endTime.Nanoseconds()
332 | latencies = append(latencies, latency)
333 | dbg.Printf("RTT: %d\n", latency)
334 | if callback != nil {
335 | callback(endTime)
336 | }
337 | }
338 | time.Sleep(echoFreq)
339 | }
340 |
341 | if contextErr != nil {
342 | return latencies, contextErr
343 | }
344 |
345 | if failTimes == echoTimes {
346 | return nil, ErrConnectTimeout
347 | }
348 |
349 | return
350 | }
351 |
352 | const PingTimeout = -1
353 | const echoOptionDataSize = 32 // `echoMessage` need to change at same time
354 |
355 | // ICMPPing privileged method
356 | func (s *Server) ICMPPing(
357 | ctx context.Context,
358 | readTimeout time.Duration,
359 | echoTimes int,
360 | echoFreq time.Duration,
361 | callback func(latency time.Duration),
362 | ) (latencies []int64, err error) {
363 | u, err := url.ParseRequestURI(s.URL)
364 | if err != nil || len(u.Host) == 0 {
365 | return nil, err
366 | }
367 | dbg.Printf("Echo: %s\n", strings.Split(u.Host, ":")[0])
368 | dialContext, err := s.Context.ipDialer.DialContext(ctx, "ip:icmp", strings.Split(u.Host, ":")[0])
369 | if err != nil {
370 | return nil, err
371 | }
372 | defer dialContext.Close()
373 |
374 | ICMPData := make([]byte, 8+echoOptionDataSize) // header + data
375 | ICMPData[0] = 8 // echo
376 | ICMPData[1] = 0 // code
377 | ICMPData[2] = 0 // checksum
378 | ICMPData[3] = 0 // checksum
379 | ICMPData[4] = 0 // id
380 | ICMPData[5] = 1 // id
381 | ICMPData[6] = 0 // seq
382 | ICMPData[7] = 1 // seq
383 |
384 | var echoMessage = "Hi! SpeedTest-Go \\(●'◡'●)/"
385 |
386 | for i := 0; i < len(echoMessage); i++ {
387 | ICMPData[7+i] = echoMessage[i]
388 | }
389 |
390 | failTimes := 0
391 | for i := 0; i < echoTimes; i++ {
392 | ICMPData[2] = byte(0)
393 | ICMPData[3] = byte(0)
394 |
395 | ICMPData[6] = byte(1 >> 8)
396 | ICMPData[7] = byte(1)
397 | ICMPData[8+echoOptionDataSize-1] = 6
398 | cs := checkSum(ICMPData)
399 | ICMPData[2] = byte(cs >> 8)
400 | ICMPData[3] = byte(cs)
401 |
402 | sTime := time.Now()
403 | _ = dialContext.SetDeadline(sTime.Add(readTimeout))
404 | _, err = dialContext.Write(ICMPData)
405 | if err != nil {
406 | failTimes += echoTimes - i
407 | break
408 | }
409 | buf := make([]byte, 20+echoOptionDataSize+8)
410 | _, err = dialContext.Read(buf)
411 | if err != nil || buf[20] != 0x00 {
412 | failTimes++
413 | continue
414 | }
415 | endTime := time.Since(sTime)
416 | latencies = append(latencies, endTime.Nanoseconds())
417 | dbg.Printf("1RTT: %s\n", endTime)
418 | if callback != nil {
419 | callback(endTime)
420 | }
421 | time.Sleep(echoFreq)
422 | }
423 | if failTimes == echoTimes {
424 | return nil, ErrConnectTimeout
425 | }
426 | return
427 | }
428 |
429 | func checkSum(data []byte) uint16 {
430 | var sum uint32
431 | var length = len(data)
432 | var index int
433 | for length > 1 {
434 | sum += uint32(data[index])<<8 + uint32(data[index+1])
435 | index += 2
436 | length -= 2
437 | }
438 | if length > 0 {
439 | sum += uint32(data[index])
440 | }
441 | sum += sum >> 16
442 | return uint16(^sum)
443 | }
444 |
445 | func StandardDeviation(vector []int64) (mean, variance, stdDev, min, max int64) {
446 | if len(vector) == 0 {
447 | return
448 | }
449 | var sumNum, accumulate int64
450 | min = math.MaxInt64
451 | max = math.MinInt64
452 | for _, value := range vector {
453 | sumNum += value
454 | if min > value {
455 | min = value
456 | }
457 | if max < value {
458 | max = value
459 | }
460 | }
461 | mean = sumNum / int64(len(vector))
462 | for _, value := range vector {
463 | accumulate += (value - mean) * (value - mean)
464 | }
465 | variance = accumulate / int64(len(vector))
466 | stdDev = int64(math.Sqrt(float64(variance)))
467 | return
468 | }
469 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # speedtest-go
2 | **Full-featured Command Line Interface and pure [Go API](#go-api) to Test Internet Speed using [speedtest.net](http://www.speedtest.net/)**.
3 |
4 | You can speedtest 2x faster than [speedtest.net](http://www.speedtest.net/) with almost the same result. [See the experimental results](https://github.com/showwin/speedtest-go#summary-of-experimental-results).
5 | Inspired by [sivel/speedtest-cli](https://github.com/sivel/speedtest-cli)
6 |
7 | ## CLI
8 | ### Installation
9 | #### macOS (homebrew)
10 |
11 | ```bash
12 | $ brew tap showwin/speedtest
13 | $ brew install speedtest
14 |
15 | ### How to Update ###
16 | $ brew update
17 | $ brew upgrade speedtest
18 | ```
19 |
20 | #### [Nix](https://nixos.org) (package manager)
21 | ```bash
22 | # Enter the latest speedtest-go environment
23 | $ nix-shell -p speedtest-go
24 | ```
25 |
26 | #### Other Platforms (Linux, Windows, etc.)
27 |
28 | Please download the compatible package from [Releases](https://github.com/showwin/speedtest-go/releases).
29 | If there are no compatible packages you want, please let me know on [Issue Tracker](https://github.com/showwin/speedtest-go/issues).
30 |
31 | #### Docker Build
32 |
33 | To build a multi-architecture Docker image:
34 |
35 | ```bash
36 | # Check if you already have a builder instance
37 | docker buildx ls
38 |
39 | # Only create a new builder if you don't have one
40 | # If the above command shows no builders or none are in use, run:
41 | docker buildx create --name mybuilder --use
42 |
43 | # Build and push for multiple platforms
44 | docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t yourusername/speedtest-go:latest --push .
45 | ```
46 |
47 | #### Running the Container
48 |
49 | ##### Docker
50 | Run the container with default settings (interactive shell):
51 | ```bash
52 | docker run -it yourusername/speedtest-go:latest
53 | ```
54 |
55 | Run a speedtest with specific arguments:
56 | ```bash
57 | # Run a basic speedtest
58 | docker run yourusername/speedtest-go:latest speedtest-go
59 |
60 | # Run with specific server
61 | docker run yourusername/speedtest-go:latest speedtest-go --server 6691
62 |
63 | # Run with multiple servers and JSON output
64 | docker run yourusername/speedtest-go:latest speedtest-go --server 6691 --server 6087 --json
65 |
66 | # Run with custom location
67 | docker run yourusername/speedtest-go:latest speedtest-go --location=60,-110
68 | ```
69 |
70 | ##### Kubernetes
71 | Here's an example Kubernetes pod specification that runs a speedtest:
72 |
73 | ```yaml
74 | apiVersion: v1
75 | kind: Pod
76 | metadata:
77 | name: speedtest
78 | spec:
79 | containers:
80 | - name: speedtest
81 | image: yourusername/speedtest-go:latest
82 | # Base command to run bash
83 | command: ["speedtest-go"]
84 | # Or run with specific arguments
85 | # args: ["--server", "6691", "--json"]
86 | restartPolicy: Never
87 | ```
88 |
89 | For a more complete deployment, you might want to use a CronJob to run periodic speedtests:
90 |
91 | ```yaml
92 | apiVersion: batch/v1
93 | kind: CronJob
94 | metadata:
95 | name: speedtest
96 | spec:
97 | schedule: "0 */6 * * *" # Run every 6 hours
98 | jobTemplate:
99 | spec:
100 | template:
101 | spec:
102 | containers:
103 | - name: speedtest
104 | image: yourusername/speedtest-go:latest
105 | command: ["speedtest-go"]
106 | args: ["--json"]
107 | restartPolicy: OnFailure
108 | ```
109 |
110 | ### Usage
111 |
112 | ```bash
113 | $ speedtest --help
114 | usage: speedtest-go []
115 |
116 | Flags:
117 | --help Show context-sensitive help (also try --help-long and --help-man).
118 | -l, --list Show available speedtest.net servers.
119 | -s, --server=SERVER ... Select server id to speedtest.
120 | --custom-url=CUSTOM-URL Specify the url of the server instead of fetching from speedtest.net.
121 | --saving-mode Test with few resources, though low accuracy (especially > 30Mbps).
122 | --json Output results in json format.
123 | --jsonl Output results in jsonl format (one json object per line).
124 | --unix Output results in unix like format.
125 | --location=LOCATION Change the location with a precise coordinate (format: lat,lon).
126 | --city=CITY Change the location with a predefined city label.
127 | --city-list List all predefined city labels.
128 | --proxy=PROXY Set a proxy(http[s] or socks) for the speedtest.
129 | eg: --proxy=socks://10.20.0.101:7890
130 | eg: --proxy=http://10.20.0.101:7890
131 | --source=SOURCE Bind a source interface for the speedtest.
132 | --dns-bind-source DNS request binding source (experimental).
133 | eg: --source=10.20.0.101
134 | -m --multi Enable multi-server mode.
135 | -t --thread=THREAD Set the number of concurrent connections.
136 | --search=SEARCH Fuzzy search servers by a keyword.
137 | --ua Set the user-agent header for the speedtest.
138 | --no-download Disable download test.
139 | --no-upload Disable upload test.
140 | --ping-mode Select a method for Ping (support icmp/tcp/http).
141 | -u --unit Set human-readable and auto-scaled rate units for output
142 | (options: decimal-bits/decimal-bytes/binary-bits/binary-bytes).
143 | -d --debug Enable debug mode.
144 | --version Show application version.
145 | ```
146 |
147 | #### Test Internet Speed
148 |
149 | Simply use `speedtest` command. The closest server is selected by default. Use the `-m` flag to enable multi-measurement mode (recommended)
150 |
151 | ```bash
152 | ## unix like format output
153 | # speedtest --unix
154 | $ speedtest
155 |
156 | speedtest-go v1.7.10 @showwin
157 |
158 | ✓ ISP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831]
159 | ✓ Found 20 Public Servers
160 |
161 | ✓ Test Server: [6691] 9.03km Shizuoka (Japan) by sudosan
162 | ✓ Latency: 4.452963ms Jitter: 41.271µs Min: 4.395179ms Max: 4.517576ms
163 | ✓ Packet Loss Analyzer: Running in background (<= 30 Secs)
164 | ✓ Download: 115.52 Mbps (Used: 135.75MB) (Latency: 4ms Jitter: 0ms Min: 4ms Max: 4ms)
165 | ✓ Upload: 4.02 Mbps (Used: 6.85MB) (Latency: 4ms Jitter: 1ms Min: 3ms Max: 8ms)
166 | ✓ Packet Loss: 8.82% (Sent: 217/Dup: 0/Max: 237)
167 | ```
168 |
169 | #### Test with Other Servers
170 |
171 | If you want to select other servers to test, you can see the available server list.
172 |
173 | ```bash
174 | $ speedtest --list
175 | Testing From IP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831]
176 | [6691] 9.03km 32.3365ms Shizuoka (Japan) by sudosan
177 | [6087] 120.55km 51.7453ms Fussa-shi (Japan) by Allied Telesis Capital Corporation
178 | [6508] 125.44km 54.6683ms Yokohama (Japan) by at2wn
179 | [6424] 148.23km 61.4724ms Tokyo (Japan) by Cordeos Corp.
180 | ...
181 | ```
182 |
183 | and select them by id.
184 |
185 | ```bash
186 | $ speedtest --server 6691 --server 6087
187 |
188 | speedtest-go v1.7.10 @showwin
189 |
190 | ✓ ISP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831]
191 | ✓ Found 2 Specified Public Server(s)
192 |
193 | ✓ Test Server: [6691] 9.03km Shizuoka (Japan) by sudosan
194 | ✓ Latency: 21.424ms Jitter: 1.644ms Min: 19.142ms Max: 23.926ms
195 | ✓ Packet Loss Analyzer: Running in background (<= 30 Sec)
196 | ✓ Download: 65.82Mbps (Used: 75.48MB) (Latency: 22ms Jitter: 2ms Min: 17ms Max: 24ms)
197 | ✓ Upload: 27.00Mbps (Used: 36.33MB) (Latency: 23ms Jitter: 2ms Min: 18ms Max: 25ms)
198 | ✓ Packet Loss: 0.00% (Sent: 321/Dup: 0/Max: 320)
199 |
200 | ✓ Test Server: [6087] 120.55km Fussa-shi (Japan) by Allied Telesis Capital Corporation
201 | ✓ Latency: 38.694699ms Jitter: 2.724ms Min: 36.443ms Max: 39.953ms
202 | ✓ Packet Loss Analyzer: Running in background (<= 30 Sec)
203 | ✓ Download: 72.24Mbps (Used: 83.72MB) (Latency: 37ms Jitter: 3ms Min: 36ms Max: 40ms)
204 | ✓ Upload: 29.56Mbps (Used: 47.64MB) (Latency: 38ms Jitter: 3ms Min: 37ms Max: 41ms)
205 | ✓ Packet Loss: 0.00% (Sent: 343/Dup: 0/Max: 342)
206 | ```
207 |
208 | #### Test with a virtual location
209 |
210 | With `--city` or `--location` option, the closest servers of the location will be picked.
211 | You can measure the speed between your location and the target location.
212 |
213 | ```bash
214 | $ speedtest --city-list
215 | Available city labels (case insensitive):
216 | CC CityLabel Location
217 | (za) capetown [-33.9391993, 18.4316716]
218 | (pl) warsaw [52.2396659, 21.0129345]
219 | (sg) yishun [1.4230218, 103.8404728]
220 | ...
221 |
222 | $ speedtest --city=capetown
223 | $ speedtest --location=60,-110
224 | ```
225 |
226 | #### Memory Saving Mode
227 |
228 | With `--saving-mode` option, it can be executed even in an insufficient memory environment like IoT devices.
229 | The memory usage can be reduced to 1/10, about 10MB of memory is used.
230 |
231 | However, please be careful that the accuracy is particularly low, especially in an environment of 30 Mbps or higher.
232 | To get more accurate results, run multiple times and average.
233 |
234 | For more details, please see [saving mode experimental result](https://github.com/showwin/speedtest-go/blob/master/docs/saving_mode_experimental_result.md).
235 |
236 | ⚠️This feature has been deprecated > v1.4.0, because speedtest-go can always run with less than 10MBytes of memory now. Even so, `--saving-mode` is still a good way to reduce computation.
237 |
238 | ## Go API
239 |
240 | ```bash
241 | go get github.com/showwin/speedtest-go
242 | ```
243 |
244 | ### API Usage
245 |
246 | The [code](https://github.com/showwin/speedtest-go/blob/master/example/naive/main.go) below finds the closest available speedtest server and tests the latency, download, and upload speeds.
247 | ```go
248 | package main
249 |
250 | import (
251 | "fmt"
252 | "github.com/showwin/speedtest-go/speedtest"
253 | )
254 |
255 | func main() {
256 | var speedtestClient = speedtest.New()
257 |
258 | // Use a proxy for the speedtest. eg: socks://127.0.0.1:7890
259 | // speedtest.WithUserConfig(&speedtest.UserConfig{Proxy: "socks://127.0.0.1:7890"})(speedtestClient)
260 |
261 | // Select a network card as the data interface.
262 | // speedtest.WithUserConfig(&speedtest.UserConfig{Source: "192.168.1.101"})(speedtestClient)
263 |
264 | // Get user's network information
265 | // user, _ := speedtestClient.FetchUserInfo()
266 |
267 | // Get a list of servers near a specified location
268 | // user.SetLocationByCity("Tokyo")
269 | // user.SetLocation("Osaka", 34.6952, 135.5006)
270 |
271 | // Search server using serverID.
272 | // eg: fetch server with ID 28910.
273 | // speedtest.ErrServerNotFound will be returned if the server cannot be found.
274 | // server, err := speedtest.FetchServerByID("28910")
275 |
276 | serverList, _ := speedtestClient.FetchServers()
277 | targets, _ := serverList.FindServer([]int{})
278 |
279 | for _, s := range targets {
280 | // Please make sure your host can access this test server,
281 | // otherwise you will get an error.
282 | // It is recommended to replace a server at this time
283 | s.PingTest(nil)
284 | s.DownloadTest()
285 | s.UploadTest()
286 | // Note: The unit of s.DLSpeed, s.ULSpeed is bytes per second, this is a float64.
287 | fmt.Printf("Latency: %s, Download: %s, Upload: %s\n", s.Latency, s.DLSpeed, s.ULSpeed)
288 | s.Context.Reset() // reset counter
289 | }
290 | }
291 | ```
292 |
293 | The [code](https://github.com/showwin/speedtest-go/blob/master/example/packet_loss/main.go) will find the closest available speedtest server and analyze packet loss.
294 | ```go
295 | package main
296 |
297 | import (
298 | "fmt"
299 | "github.com/showwin/speedtest-go/speedtest"
300 | "github.com/showwin/speedtest-go/speedtest/transport"
301 | "log"
302 | )
303 |
304 | func checkError(err error) {
305 | if err != nil {
306 | log.Fatal(err)
307 | }
308 | }
309 |
310 | // Note: The current packet loss analyzer does not support udp over http.
311 | // This means we cannot get packet loss through a proxy.
312 | func main() {
313 | // Retrieve available servers
314 | var speedtestClient = speedtest.New()
315 | serverList, _ := speedtestClient.FetchServers()
316 | targets, _ := serverList.FindServer([]int{})
317 |
318 | // Create a packet loss analyzer, use default options
319 | analyzer := speedtest.NewPacketLossAnalyzer(nil)
320 |
321 | // Perform packet loss analysis on all available servers
322 | for _, server := range targets {
323 | err := analyzer.Run(server.Host, func(packetLoss *transport.PLoss) {
324 | fmt.Println(packetLoss, server.Host, server.Name)
325 | // fmt.Println(packetLoss.Loss())
326 | })
327 | checkError(err)
328 | }
329 |
330 | // or test all at the same time.
331 | packetLoss, err := analyzer.RunMulti(targets.Hosts())
332 | checkError(err)
333 | fmt.Println(packetLoss)
334 | }
335 | ```
336 |
337 | ## Summary of Experimental Results
338 |
339 | Speedtest-go is a great tool because of the following five reasons:
340 | * Cross-platform available.
341 | * Low memory environment.
342 | * We are the first **FULL-FEATURED** open source speed testing project based on speedtest.net, including down/up rates, jitter and packet loss, etc.
343 | * Testing time is the **SHORTEST** compare to [speedtest.net](http://www.speedtest.net/) and [sivel/speedtest-cli](https://github.com/sivel/speedtest-cli), especially about 2x faster than [speedtest.net](http://www.speedtest.net/).
344 | * Result is **MORE CLOSE** to [speedtest.net](http://www.speedtest.net/) than [speedtest-cli](https://github.com/sivel/speedtest-cli).
345 |
346 | The following data is summarized. If you got interested, please see [more details](https://github.com/showwin/speedtest-go/blob/master/docs/experimental_result.md).
347 |
348 | ### Download (Mbps)
349 |
350 | distance = distance to testing server
351 | * 0 - 1000(km) ≒ domestic
352 | * 1000 - 8000(km) ≒ same region
353 | * 8000 - 20000(km) ≒ really far!
354 | * 20000km is half of the circumference of our planet.
355 |
356 | | distance (km) | speedtest.net | speedtest-go | speedtest-cli |
357 | |:-------------:|:-------------:|:------------:|:-------------:|
358 | | 0 - 1000 | 92.12 | **91.21** | 70.27 |
359 | | 1000 - 8000 | 66.45 | **65.51** | 56.56 |
360 | | 8000 - 20000 | 11.84 | 9.43 | **11.87** |
361 |
362 | ### Upload (Mbps)
363 |
364 | | distance (km) | speedtest.net | speedtest-go | speedtest-cli |
365 | |:-------------:|:-------------:|:------------:|:-------------:|
366 | | 0 - 1000 | 65.56 | **47.58** | 36.16 |
367 | | 1000 - 8000 | 58.02 | **54.74** | 26.78 |
368 | | 8000 - 20000 | 5.20 | **8.32** | 2.58 |
369 |
370 | ### Testing Time (sec)
371 |
372 | | distance (km) | speedtest.net | speedtest-go | speedtest-cli |
373 | |:-------------:|:-------------:|:------------:|:-------------:|
374 | | 0 - 1000 | 45.03 | **22.84** | 24.46 |
375 | | 1000 - 8000 | 44.89 | **24.45** | 28.52 |
376 | | 8000 - 20000 | 49.64 | **34.08** | 41.26 |
377 |
378 | ## Contributors
379 |
380 | See [Contributors](https://github.com/showwin/speedtest-go/graphs/contributors), PRs are welcome!
381 |
382 | ## Issues
383 |
384 | You can find or report issues in the [Issue Tracker](https://github.com/showwin/speedtest-go/issues).
385 |
386 | ## LICENSE
387 |
388 | [MIT](https://github.com/showwin/speedtest-go/blob/master/LICENSE)
389 |
--------------------------------------------------------------------------------
/speedtest/data_manager.go:
--------------------------------------------------------------------------------
1 | package speedtest
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "errors"
7 | "github.com/showwin/speedtest-go/speedtest/internal"
8 | "io"
9 | "math"
10 | "runtime"
11 | "sync"
12 | "sync/atomic"
13 | "time"
14 | )
15 |
16 | type Manager interface {
17 | SetRateCaptureFrequency(duration time.Duration) Manager
18 | SetCaptureTime(duration time.Duration) Manager
19 |
20 | NewChunk() Chunk
21 |
22 | GetTotalDownload() int64
23 | GetTotalUpload() int64
24 | AddTotalDownload(value int64)
25 | AddTotalUpload(value int64)
26 |
27 | GetAvgDownloadRate() float64
28 | GetAvgUploadRate() float64
29 |
30 | GetEWMADownloadRate() float64
31 | GetEWMAUploadRate() float64
32 |
33 | SetCallbackDownload(callback func(downRate ByteRate))
34 | SetCallbackUpload(callback func(upRate ByteRate))
35 |
36 | RegisterDownloadHandler(fn func()) *TestDirection
37 | RegisterUploadHandler(fn func()) *TestDirection
38 |
39 | // Wait for the upload or download task to end to avoid errors caused by core occupation
40 | Wait()
41 | Reset()
42 | Snapshots() *Snapshots
43 |
44 | SetNThread(n int) Manager
45 | }
46 |
47 | type Chunk interface {
48 | UploadHandler(size int64) Chunk
49 | DownloadHandler(r io.Reader) error
50 |
51 | GetRate() float64
52 | GetDuration() time.Duration
53 | GetParent() Manager
54 |
55 | Read(b []byte) (n int, err error)
56 | }
57 |
58 | const readChunkSize = 1024 // 1 KBytes with higher frequency rate feedback
59 |
60 | type DataType int32
61 |
62 | const (
63 | typeEmptyChunk = iota
64 | typeDownload
65 | typeUpload
66 | )
67 |
68 | var (
69 | ErrorUninitializedManager = errors.New("uninitialized manager")
70 | )
71 |
72 | type funcGroup struct {
73 | fns []func()
74 | }
75 |
76 | func (f *funcGroup) Add(fn func()) {
77 | f.fns = append(f.fns, fn)
78 | }
79 |
80 | type DataManager struct {
81 | SnapshotStore *Snapshots
82 | Snapshot *Snapshot
83 | sync.Mutex
84 |
85 | repeatByte *[]byte
86 |
87 | captureTime time.Duration
88 | rateCaptureFrequency time.Duration
89 | nThread int
90 |
91 | running bool
92 | runningRW sync.RWMutex
93 |
94 | download *TestDirection
95 | upload *TestDirection
96 | }
97 |
98 | type TestDirection struct {
99 | TestType int // test type
100 | manager *DataManager // manager
101 | totalDataVolume int64 // total send/receive data volume
102 | RateSequence []int64 // rate history sequence
103 | welford *internal.Welford // std/EWMA/mean
104 | captureCallback func(realTimeRate ByteRate) // user callback
105 | closeFunc func() // close func
106 | *funcGroup // actually exec function
107 | }
108 |
109 | func (dm *DataManager) NewDataDirection(testType int) *TestDirection {
110 | return &TestDirection{
111 | TestType: testType,
112 | manager: dm,
113 | funcGroup: &funcGroup{},
114 | }
115 | }
116 |
117 | func NewDataManager() *DataManager {
118 | r := bytes.Repeat([]byte{0xAA}, readChunkSize) // uniformly distributed sequence of bits
119 | ret := &DataManager{
120 | nThread: runtime.NumCPU(),
121 | captureTime: time.Second * 15,
122 | rateCaptureFrequency: time.Millisecond * 50,
123 | Snapshot: &Snapshot{},
124 | repeatByte: &r,
125 | }
126 | ret.download = ret.NewDataDirection(typeDownload)
127 | ret.upload = ret.NewDataDirection(typeUpload)
128 | ret.SnapshotStore = newHistorySnapshots(maxSnapshotSize)
129 | return ret
130 | }
131 |
132 | func (dm *DataManager) SetCallbackDownload(callback func(downRate ByteRate)) {
133 | if dm.download != nil {
134 | dm.download.captureCallback = callback
135 | }
136 | }
137 |
138 | func (dm *DataManager) SetCallbackUpload(callback func(upRate ByteRate)) {
139 | if dm.upload != nil {
140 | dm.upload.captureCallback = callback
141 | }
142 | }
143 |
144 | func (dm *DataManager) Wait() {
145 | oldDownTotal := dm.GetTotalDownload()
146 | oldUpTotal := dm.GetTotalUpload()
147 | for {
148 | time.Sleep(dm.rateCaptureFrequency)
149 | newDownTotal := dm.GetTotalDownload()
150 | newUpTotal := dm.GetTotalUpload()
151 | deltaDown := newDownTotal - oldDownTotal
152 | deltaUp := newUpTotal - oldUpTotal
153 | oldDownTotal = newDownTotal
154 | oldUpTotal = newUpTotal
155 | if deltaDown == 0 && deltaUp == 0 {
156 | return
157 | }
158 | }
159 | }
160 |
161 | func (dm *DataManager) RegisterUploadHandler(fn func()) *TestDirection {
162 | if len(dm.upload.fns) < dm.nThread {
163 | dm.upload.Add(fn)
164 | }
165 | return dm.upload
166 | }
167 |
168 | func (dm *DataManager) RegisterDownloadHandler(fn func()) *TestDirection {
169 | if len(dm.download.fns) < dm.nThread {
170 | dm.download.Add(fn)
171 | }
172 | return dm.download
173 | }
174 |
175 | func (td *TestDirection) GetTotalDataVolume() int64 {
176 | return atomic.LoadInt64(&td.totalDataVolume)
177 | }
178 |
179 | func (td *TestDirection) AddTotalDataVolume(delta int64) int64 {
180 | return atomic.AddInt64(&td.totalDataVolume, delta)
181 | }
182 |
183 | func (td *TestDirection) Start(cancel context.CancelFunc, mainRequestHandlerIndex int) {
184 | if len(td.fns) == 0 {
185 | panic("empty task stack")
186 | }
187 | if mainRequestHandlerIndex > len(td.fns)-1 {
188 | mainRequestHandlerIndex = 0
189 | }
190 | mainLoadFactor := 0.1
191 | // When the number of processor cores is equivalent to the processing program,
192 | // the processing efficiency reaches the highest level (VT is not considered).
193 | mainN := int(mainLoadFactor * float64(len(td.fns)))
194 | if mainN == 0 {
195 | mainN = 1
196 | }
197 | if len(td.fns) == 1 {
198 | mainN = td.manager.nThread
199 | }
200 | auxN := td.manager.nThread - mainN
201 | dbg.Printf("Available fns: %d\n", len(td.fns))
202 | dbg.Printf("mainN: %d\n", mainN)
203 | dbg.Printf("auxN: %d\n", auxN)
204 | wg := sync.WaitGroup{}
205 | td.manager.running = true
206 | stopCapture := td.rateCapture()
207 |
208 | // refresh once function
209 | once := sync.Once{}
210 | td.closeFunc = func() {
211 | once.Do(func() {
212 | stopCapture <- true
213 | close(stopCapture)
214 | td.manager.runningRW.Lock()
215 | td.manager.running = false
216 | td.manager.runningRW.Unlock()
217 | cancel()
218 | dbg.Println("FuncGroup: Stop")
219 | })
220 | }
221 |
222 | time.AfterFunc(td.manager.captureTime, td.closeFunc)
223 | for i := 0; i < mainN; i++ {
224 | wg.Add(1)
225 | go func() {
226 | defer wg.Done()
227 | for {
228 | td.manager.runningRW.RLock()
229 | running := td.manager.running
230 | td.manager.runningRW.RUnlock()
231 | if !running {
232 | return
233 | }
234 | td.fns[mainRequestHandlerIndex]()
235 | }
236 | }()
237 | }
238 | for j := 0; j < auxN; {
239 | for i := range td.fns {
240 | if j == auxN {
241 | break
242 | }
243 | if i == mainRequestHandlerIndex {
244 | continue
245 | }
246 | wg.Add(1)
247 | t := i
248 | go func() {
249 | defer wg.Done()
250 | for {
251 | td.manager.runningRW.RLock()
252 | running := td.manager.running
253 | td.manager.runningRW.RUnlock()
254 | if !running {
255 | return
256 | }
257 | td.fns[t]()
258 | }
259 | }()
260 | j++
261 | }
262 | }
263 | wg.Wait()
264 | }
265 |
266 | func (td *TestDirection) rateCapture() chan bool {
267 | ticker := time.NewTicker(td.manager.rateCaptureFrequency)
268 | var prevTotalDataVolume int64 = 0
269 | stopCapture := make(chan bool)
270 | td.welford = internal.NewWelford(5*time.Second, td.manager.rateCaptureFrequency)
271 | sTime := time.Now()
272 | go func(t *time.Ticker) {
273 | defer t.Stop()
274 | for {
275 | select {
276 | case <-t.C:
277 | newTotalDataVolume := td.GetTotalDataVolume()
278 | deltaDataVolume := newTotalDataVolume - prevTotalDataVolume
279 | prevTotalDataVolume = newTotalDataVolume
280 | if deltaDataVolume != 0 {
281 | td.RateSequence = append(td.RateSequence, deltaDataVolume)
282 | }
283 | // anyway we update the measuring instrument
284 | globalAvg := (float64(td.GetTotalDataVolume())) / float64(time.Since(sTime).Milliseconds()) * 1000
285 | if td.welford.Update(globalAvg, float64(deltaDataVolume)) {
286 | go td.closeFunc()
287 | }
288 | // reports the current rate at the given rate
289 | if td.captureCallback != nil {
290 | td.captureCallback(ByteRate(td.welford.EWMA()))
291 | }
292 | case stop := <-stopCapture:
293 | if stop {
294 | return
295 | }
296 | }
297 | }
298 | }(ticker)
299 | return stopCapture
300 | }
301 |
302 | func (dm *DataManager) NewChunk() Chunk {
303 | var dc DataChunk
304 | dc.manager = dm
305 | dm.Lock()
306 | *dm.Snapshot = append(*dm.Snapshot, &dc)
307 | dm.Unlock()
308 | return &dc
309 | }
310 |
311 | func (dm *DataManager) AddTotalDownload(value int64) {
312 | dm.download.AddTotalDataVolume(value)
313 | }
314 |
315 | func (dm *DataManager) AddTotalUpload(value int64) {
316 | dm.upload.AddTotalDataVolume(value)
317 | }
318 |
319 | func (dm *DataManager) GetTotalDownload() int64 {
320 | return dm.download.GetTotalDataVolume()
321 | }
322 |
323 | func (dm *DataManager) GetTotalUpload() int64 {
324 | return dm.upload.GetTotalDataVolume()
325 | }
326 |
327 | func (dm *DataManager) SetRateCaptureFrequency(duration time.Duration) Manager {
328 | dm.rateCaptureFrequency = duration
329 | return dm
330 | }
331 |
332 | func (dm *DataManager) SetCaptureTime(duration time.Duration) Manager {
333 | dm.captureTime = duration
334 | return dm
335 | }
336 |
337 | func (dm *DataManager) SetNThread(n int) Manager {
338 | if n < 1 {
339 | dm.nThread = runtime.NumCPU()
340 | } else {
341 | dm.nThread = n
342 | }
343 | return dm
344 | }
345 |
346 | func (dm *DataManager) Snapshots() *Snapshots {
347 | return dm.SnapshotStore
348 | }
349 |
350 | func (dm *DataManager) Reset() {
351 | dm.SnapshotStore.push(dm.Snapshot)
352 | dm.Snapshot = &Snapshot{}
353 | dm.download = dm.NewDataDirection(typeDownload)
354 | dm.upload = dm.NewDataDirection(typeUpload)
355 | }
356 |
357 | func (dm *DataManager) GetAvgDownloadRate() float64 {
358 | unit := float64(dm.captureTime / time.Millisecond)
359 | return float64(dm.download.GetTotalDataVolume()*8/1000) / unit
360 | }
361 |
362 | func (dm *DataManager) GetEWMADownloadRate() float64 {
363 | if dm.download.welford != nil {
364 | return dm.download.welford.EWMA()
365 | }
366 | return 0
367 | }
368 |
369 | func (dm *DataManager) GetAvgUploadRate() float64 {
370 | unit := float64(dm.captureTime / time.Millisecond)
371 | return float64(dm.upload.GetTotalDataVolume()*8/1000) / unit
372 | }
373 |
374 | func (dm *DataManager) GetEWMAUploadRate() float64 {
375 | if dm.upload.welford != nil {
376 | return dm.upload.welford.EWMA()
377 | }
378 | return 0
379 | }
380 |
381 | type DataChunk struct {
382 | manager *DataManager
383 | dateType DataType
384 | startTime time.Time
385 | endTime time.Time
386 | err error
387 | ContentLength int64
388 | remainOrDiscardSize int64
389 | }
390 |
391 | var blackHolePool = sync.Pool{
392 | New: func() any {
393 | b := make([]byte, 8192)
394 | return &b
395 | },
396 | }
397 |
398 | func (dc *DataChunk) GetDuration() time.Duration {
399 | return dc.endTime.Sub(dc.startTime)
400 | }
401 |
402 | func (dc *DataChunk) GetRate() float64 {
403 | if dc.dateType == typeDownload {
404 | return float64(dc.remainOrDiscardSize) / dc.GetDuration().Seconds()
405 | } else if dc.dateType == typeUpload {
406 | return float64(dc.ContentLength-dc.remainOrDiscardSize) * 8 / 1000 / 1000 / dc.GetDuration().Seconds()
407 | }
408 | return 0
409 | }
410 |
411 | // DownloadHandler No value will be returned here, because the error will interrupt the test.
412 | // The error chunk is generally caused by the remote server actively closing the connection.
413 | func (dc *DataChunk) DownloadHandler(r io.Reader) error {
414 | if dc.dateType != typeEmptyChunk {
415 | dc.err = errors.New("multiple calls to the same chunk handler are not allowed")
416 | return dc.err
417 | }
418 | dc.dateType = typeDownload
419 | dc.startTime = time.Now()
420 | defer func() {
421 | dc.endTime = time.Now()
422 | }()
423 | bufP := blackHolePool.Get().(*[]byte)
424 | defer blackHolePool.Put(bufP)
425 | readSize := 0
426 | for {
427 | dc.manager.runningRW.RLock()
428 | running := dc.manager.running
429 | dc.manager.runningRW.RUnlock()
430 | if !running {
431 | return nil
432 | }
433 | readSize, dc.err = r.Read(*bufP)
434 | rs := int64(readSize)
435 |
436 | dc.remainOrDiscardSize += rs
437 | dc.manager.download.AddTotalDataVolume(rs)
438 | if dc.err != nil {
439 | if dc.err == io.EOF {
440 | return nil
441 | }
442 | return dc.err
443 | }
444 | }
445 | }
446 |
447 | func (dc *DataChunk) UploadHandler(size int64) Chunk {
448 | if dc.dateType != typeEmptyChunk {
449 | dc.err = errors.New("multiple calls to the same chunk handler are not allowed")
450 | }
451 |
452 | if size <= 0 {
453 | panic("the size of repeated bytes should be > 0")
454 | }
455 |
456 | dc.ContentLength = size
457 | dc.remainOrDiscardSize = size
458 | dc.dateType = typeUpload
459 | dc.startTime = time.Now()
460 | return dc
461 | }
462 |
463 | func (dc *DataChunk) GetParent() Manager {
464 | return dc.manager
465 | }
466 |
467 | func (dc *DataChunk) Read(b []byte) (n int, err error) {
468 | if dc.remainOrDiscardSize < readChunkSize {
469 | if dc.remainOrDiscardSize <= 0 {
470 | dc.endTime = time.Now()
471 | return n, io.EOF
472 | }
473 | n = copy(b, (*dc.manager.repeatByte)[:dc.remainOrDiscardSize])
474 | } else {
475 | n = copy(b, *dc.manager.repeatByte)
476 | }
477 | n64 := int64(n)
478 | dc.remainOrDiscardSize -= n64
479 | dc.manager.AddTotalUpload(n64)
480 | return
481 | }
482 |
483 | // calcMAFilter Median-Averaging Filter
484 | func _(list []int64) float64 {
485 | if len(list) == 0 {
486 | return 0
487 | }
488 | var sum int64 = 0
489 | n := len(list)
490 | if n == 0 {
491 | return 0
492 | }
493 | length := len(list)
494 | for i := 0; i < length-1; i++ {
495 | for j := i + 1; j < length; j++ {
496 | if list[i] > list[j] {
497 | list[i], list[j] = list[j], list[i]
498 | }
499 | }
500 | }
501 | for i := 1; i < n-1; i++ {
502 | sum += list[i]
503 | }
504 | return float64(sum) / float64(n-2)
505 | }
506 |
507 | func pautaFilter(vector []int64) []int64 {
508 | dbg.Println("Per capture unit")
509 | dbg.Printf("Raw Sequence len: %d\n", len(vector))
510 | dbg.Printf("Raw Sequence: %v\n", vector)
511 | if len(vector) == 0 {
512 | return vector
513 | }
514 | mean, _, std, _, _ := sampleVariance(vector)
515 | var retVec []int64
516 | for _, value := range vector {
517 | if math.Abs(float64(value-mean)) < float64(3*std) {
518 | retVec = append(retVec, value)
519 | }
520 | }
521 | dbg.Printf("Raw average: %dByte\n", mean)
522 | dbg.Printf("Pauta Sequence len: %d\n", len(retVec))
523 | dbg.Printf("Pauta Sequence: %v\n", retVec)
524 | return retVec
525 | }
526 |
527 | // sampleVariance sample Variance
528 | func sampleVariance(vector []int64) (mean, variance, stdDev, min, max int64) {
529 | if len(vector) == 0 {
530 | return 0, 0, 0, 0, 0
531 | }
532 | var sumNum, accumulate int64
533 | min = math.MaxInt64
534 | max = math.MinInt64
535 | for _, value := range vector {
536 | sumNum += value
537 | if min > value {
538 | min = value
539 | }
540 | if max < value {
541 | max = value
542 | }
543 | }
544 | mean = sumNum / int64(len(vector))
545 | for _, value := range vector {
546 | accumulate += (value - mean) * (value - mean)
547 | }
548 | variance = accumulate / int64(len(vector)-1) // Bessel's correction
549 | stdDev = int64(math.Sqrt(float64(variance)))
550 | return
551 | }
552 |
553 | const maxSnapshotSize = 10
554 |
555 | type Snapshot []*DataChunk
556 |
557 | type Snapshots struct {
558 | sp []*Snapshot
559 | maxSize int
560 | }
561 |
562 | func newHistorySnapshots(size int) *Snapshots {
563 | return &Snapshots{
564 | sp: make([]*Snapshot, 0, size),
565 | maxSize: size,
566 | }
567 | }
568 |
569 | func (rs *Snapshots) push(value *Snapshot) {
570 | if len(rs.sp) == rs.maxSize {
571 | rs.sp = rs.sp[1:]
572 | }
573 | rs.sp = append(rs.sp, value)
574 | }
575 |
576 | func (rs *Snapshots) Latest() *Snapshot {
577 | if len(rs.sp) > 0 {
578 | return rs.sp[len(rs.sp)-1]
579 | }
580 | return nil
581 | }
582 |
583 | func (rs *Snapshots) All() []*Snapshot {
584 | return rs.sp
585 | }
586 |
587 | func (rs *Snapshots) Clean() {
588 | rs.sp = make([]*Snapshot, 0, rs.maxSize)
589 | }
590 |
--------------------------------------------------------------------------------