├── .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 | SpeedTest-Go (1) 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 | --------------------------------------------------------------------------------