pax_global_header00006660000000000000000000000064152277042320014516gustar00rootroot0000000000000052 comment=cc92f037f2612abea33a3a07b0a70345138e5875 speedtest-go-1.7.10/000077500000000000000000000000001522770423200142075ustar00rootroot00000000000000speedtest-go-1.7.10/.github/000077500000000000000000000000001522770423200155475ustar00rootroot00000000000000speedtest-go-1.7.10/.github/workflows/000077500000000000000000000000001522770423200176045ustar00rootroot00000000000000speedtest-go-1.7.10/.github/workflows/ci.yaml000066400000000000000000000020431522770423200210620ustar00rootroot00000000000000name: go-ci on: [push] jobs: setup: runs-on: ubuntu-latest steps: - name: set up uses: actions/setup-go@v3 with: go-version: ^1.19 id: go - name: check out uses: actions/checkout@v3 - name: Cache uses: actions/cache@v2.1.0 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- build: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: build run: go build ./... test: needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: test run: go test ./speedtest -v lint: needs: setup runs-on: ubuntu-latest steps: - uses: actions/setup-go@v3 with: go-version: 1.19 - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: version: v1.53.2 speedtest-go-1.7.10/.github/workflows/release.yml000066400000000000000000000011101522770423200217400ustar00rootroot00000000000000name: goreleaser on: push: tags: - "v*.*.*" jobs: goreleaser: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 with: fetch-depth: 0 - name: Set up Go uses: actions/setup-go@v3 with: go-version: 1.23.4 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 with: distribution: goreleaser version: latest args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} speedtest-go-1.7.10/.gitignore000066400000000000000000000004741522770423200162040ustar00rootroot00000000000000# Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof .goxc.local.json dist/* speedtest-go .idea/ .run/speedtest-go-1.7.10/.go-version000066400000000000000000000000071522770423200162750ustar00rootroot000000000000001.16.5 speedtest-go-1.7.10/.goreleaser.yml000066400000000000000000000025151522770423200171430ustar00rootroot00000000000000# This is an example goreleaser.yaml file with some sane defaults. # Make sure to check the documentation at http://goreleaser.com before: hooks: # You may remove this if you don't use go modules. - go mod download # you may remove this if you don't need go generate - go generate ./... builds: - env: - CGO_ENABLED=0 goos: - linux - windows - darwin - openbsd - freebsd goarch: - amd64 - arm - arm64 - 386 - s390x - ppc64 - ppc64le - riscv64 - mips - mips64 - mipsle - mips64le - loong64 goarm: - 5 - 6 - 7 gomips: - hardfloat - softfloat ldflags: - -s -w -X main.version={{.Version}} -X main.commit={{.ShortCommit}} -X main.date={{ .CommitDate }} archives: - name_template: >- {{ .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 }} checksum: name_template: 'checksums.txt' snapshot: name_template: "{{ .Tag }}-next" changelog: sort: asc filters: exclude: - '^docs:' - '^test:' speedtest-go-1.7.10/LICENSE000066400000000000000000000020651522770423200152170ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 ITO Shogo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. speedtest-go-1.7.10/README.md000066400000000000000000000273261522770423200155000ustar00rootroot00000000000000# speedtest-go **Full-featured Command Line Interface and pure [Go API](#go-api) to Test Internet Speed using [speedtest.net](http://www.speedtest.net/)**. 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). Inspired by [sivel/speedtest-cli](https://github.com/sivel/speedtest-cli) ## CLI ### Installation #### macOS (homebrew) ```bash $ brew tap showwin/speedtest $ brew install speedtest ### How to Update ### $ brew update $ brew upgrade speedtest ``` #### [Nix](https://nixos.org) (package manager) ```bash # Enter the latest speedtest-go environment $ nix-shell -p speedtest-go ``` #### Other Platforms (Linux, Windows, etc.) Please download the compatible package from [Releases](https://github.com/showwin/speedtest-go/releases). If there are no compatible packages you want, please let me know on [Issue Tracker](https://github.com/showwin/speedtest-go/issues). ### Usage ```bash $ speedtest --help usage: speedtest-go [] Flags: --help Show context-sensitive help (also try --help-long and --help-man). -l, --list Show available speedtest.net servers. -s, --server=SERVER ... Select server id to speedtest. --custom-url=CUSTOM-URL Specify the url of the server instead of fetching from speedtest.net. --saving-mode Test with few resources, though low accuracy (especially > 30Mbps). --json Output results in json format. --unix Output results in unix like format. --location=LOCATION Change the location with a precise coordinate (format: lat,lon). --city=CITY Change the location with a predefined city label. --city-list List all predefined city labels. --proxy=PROXY Set a proxy(http[s] or socks) for the speedtest. eg: --proxy=socks://10.20.0.101:7890 eg: --proxy=http://10.20.0.101:7890 --source=SOURCE Bind a source interface for the speedtest. --dns-bind-source DNS request binding source (experimental). eg: --source=10.20.0.101 -m --multi Enable multi-server mode. -t --thread=THREAD Set the number of concurrent connections. --search=SEARCH Fuzzy search servers by a keyword. --ua Set the user-agent header for the speedtest. --no-download Disable download test. --no-upload Disable upload test. --ping-mode Select a method for Ping (support icmp/tcp/http). -u --unit Set human-readable and auto-scaled rate units for output (options: decimal-bits/decimal-bytes/binary-bits/binary-bytes). -d --debug Enable debug mode. --version Show application version. ``` #### Test Internet Speed Simply use `speedtest` command. The closest server is selected by default. Use the `-m` flag to enable multi-measurement mode (recommended) ```bash ## unix like format output # speedtest --unix $ speedtest speedtest-go v1.7.10 @showwin ✓ ISP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831] ✓ Found 20 Public Servers ✓ Test Server: [6691] 9.03km Shizuoka (Japan) by sudosan ✓ Latency: 4.452963ms Jitter: 41.271µs Min: 4.395179ms Max: 4.517576ms ✓ Packet Loss Analyzer: Running in background (<= 30 Secs) ✓ Download: 115.52 Mbps (Used: 135.75MB) (Latency: 4ms Jitter: 0ms Min: 4ms Max: 4ms) ✓ Upload: 4.02 Mbps (Used: 6.85MB) (Latency: 4ms Jitter: 1ms Min: 3ms Max: 8ms) ✓ Packet Loss: 8.82% (Sent: 217/Dup: 0/Max: 237) ``` #### Test with Other Servers If you want to select other servers to test, you can see the available server list. ```bash $ speedtest --list Testing From IP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831] [6691] 9.03km 32.3365ms Shizuoka (Japan) by sudosan [6087] 120.55km 51.7453ms Fussa-shi (Japan) by Allied Telesis Capital Corporation [6508] 125.44km 54.6683ms Yokohama (Japan) by at2wn [6424] 148.23km 61.4724ms Tokyo (Japan) by Cordeos Corp. ... ``` and select them by id. ```bash $ speedtest --server 6691 --server 6087 speedtest-go v1.7.10 @showwin ✓ ISP: 124.27.199.165 (Fujitsu) [34.9769, 138.3831] ✓ Found 2 Specified Public Server(s) ✓ Test Server: [6691] 9.03km Shizuoka (Japan) by sudosan ✓ Latency: 21.424ms Jitter: 1.644ms Min: 19.142ms Max: 23.926ms ✓ Packet Loss Analyzer: Running in background (<= 30 Sec) ✓ Download: 65.82Mbps (Used: 75.48MB) (Latency: 22ms Jitter: 2ms Min: 17ms Max: 24ms) ✓ Upload: 27.00Mbps (Used: 36.33MB) (Latency: 23ms Jitter: 2ms Min: 18ms Max: 25ms) ✓ Packet Loss: 0.00% (Sent: 321/Dup: 0/Max: 320) ✓ Test Server: [6087] 120.55km Fussa-shi (Japan) by Allied Telesis Capital Corporation ✓ Latency: 38.694699ms Jitter: 2.724ms Min: 36.443ms Max: 39.953ms ✓ Packet Loss Analyzer: Running in background (<= 30 Sec) ✓ Download: 72.24Mbps (Used: 83.72MB) (Latency: 37ms Jitter: 3ms Min: 36ms Max: 40ms) ✓ Upload: 29.56Mbps (Used: 47.64MB) (Latency: 38ms Jitter: 3ms Min: 37ms Max: 41ms) ✓ Packet Loss: 0.00% (Sent: 343/Dup: 0/Max: 342) ``` #### Test with a virtual location With `--city` or `--location` option, the closest servers of the location will be picked. You can measure the speed between your location and the target location. ```bash $ speedtest --city-list Available city labels (case insensitive): CC CityLabel Location (za) capetown [-33.9391993, 18.4316716] (pl) warsaw [52.2396659, 21.0129345] (sg) yishun [1.4230218, 103.8404728] ... $ speedtest --city=capetown $ speedtest --location=60,-110 ``` #### Memory Saving Mode With `--saving-mode` option, it can be executed even in an insufficient memory environment like IoT devices. The memory usage can be reduced to 1/10, about 10MB of memory is used. However, please be careful that the accuracy is particularly low, especially in an environment of 30 Mbps or higher. To get more accurate results, run multiple times and average. For more details, please see [saving mode experimental result](https://github.com/showwin/speedtest-go/blob/master/docs/saving_mode_experimental_result.md). ⚠️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. ## Go API ```bash go get github.com/showwin/speedtest-go ``` ### API Usage 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. ```go package main import ( "fmt" "github.com/showwin/speedtest-go/speedtest" ) func main() { var speedtestClient = speedtest.New() // Use a proxy for the speedtest. eg: socks://127.0.0.1:7890 // speedtest.WithUserConfig(&speedtest.UserConfig{Proxy: "socks://127.0.0.1:7890"})(speedtestClient) // Select a network card as the data interface. // speedtest.WithUserConfig(&speedtest.UserConfig{Source: "192.168.1.101"})(speedtestClient) // Get user's network information // user, _ := speedtestClient.FetchUserInfo() // Get a list of servers near a specified location // user.SetLocationByCity("Tokyo") // user.SetLocation("Osaka", 34.6952, 135.5006) // Search server using serverID. // eg: fetch server with ID 28910. // speedtest.ErrServerNotFound will be returned if the server cannot be found. // server, err := speedtest.FetchServerByID("28910") serverList, _ := speedtestClient.FetchServers() targets, _ := serverList.FindServer([]int{}) for _, s := range targets { // Please make sure your host can access this test server, // otherwise you will get an error. // It is recommended to replace a server at this time s.PingTest(nil) s.DownloadTest() s.UploadTest() // Note: The unit of s.DLSpeed, s.ULSpeed is bytes per second, this is a float64. fmt.Printf("Latency: %s, Download: %s, Upload: %s\n", s.Latency, s.DLSpeed, s.ULSpeed) s.Context.Reset() // reset counter } } ``` 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. ```go package main import ( "fmt" "github.com/showwin/speedtest-go/speedtest" "github.com/showwin/speedtest-go/speedtest/transport" "log" ) func checkError(err error) { if err != nil { log.Fatal(err) } } // Note: The current packet loss analyzer does not support udp over http. // This means we cannot get packet loss through a proxy. func main() { // Retrieve available servers var speedtestClient = speedtest.New() serverList, _ := speedtestClient.FetchServers() targets, _ := serverList.FindServer([]int{}) // Create a packet loss analyzer, use default options analyzer := speedtest.NewPacketLossAnalyzer(nil) // Perform packet loss analysis on all available servers for _, server := range targets { err := analyzer.Run(server.Host, func(packetLoss *transport.PLoss) { fmt.Println(packetLoss, server.Host, server.Name) // fmt.Println(packetLoss.Loss()) }) checkError(err) } // or test all at the same time. packetLoss, err := analyzer.RunMulti(targets.Hosts()) checkError(err) fmt.Println(packetLoss) } ``` ## Summary of Experimental Results Speedtest-go is a great tool because of the following five reasons: * Cross-platform available. * Low memory environment. * We are the first **FULL-FEATURED** open source speed testing project based on speedtest.net, including down/up rates, jitter and packet loss, etc. * 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/). * Result is **MORE CLOSE** to [speedtest.net](http://www.speedtest.net/) than [speedtest-cli](https://github.com/sivel/speedtest-cli). 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). ### Download (Mbps) distance = distance to testing server * 0 - 1000(km) ≒ domestic * 1000 - 8000(km) ≒ same region * 8000 - 20000(km) ≒ really far! * 20000km is half of the circumference of our planet. | distance (km) | speedtest.net | speedtest-go | speedtest-cli | |:-------------:|:-------------:|:------------:|:-------------:| | 0 - 1000 | 92.12 | **91.21** | 70.27 | | 1000 - 8000 | 66.45 | **65.51** | 56.56 | | 8000 - 20000 | 11.84 | 9.43 | **11.87** | ### Upload (Mbps) | distance (km) | speedtest.net | speedtest-go | speedtest-cli | |:-------------:|:-------------:|:------------:|:-------------:| | 0 - 1000 | 65.56 | **47.58** | 36.16 | | 1000 - 8000 | 58.02 | **54.74** | 26.78 | | 8000 - 20000 | 5.20 | **8.32** | 2.58 | ### Testing Time (sec) | distance (km) | speedtest.net | speedtest-go | speedtest-cli | |:-------------:|:-------------:|:------------:|:-------------:| | 0 - 1000 | 45.03 | **22.84** | 24.46 | | 1000 - 8000 | 44.89 | **24.45** | 28.52 | | 8000 - 20000 | 49.64 | **34.08** | 41.26 | ## Contributors See [Contributors](https://github.com/showwin/speedtest-go/graphs/contributors), PRs are welcome! ## Issues You can find or report issues in the [Issue Tracker](https://github.com/showwin/speedtest-go/issues). ## LICENSE [MIT](https://github.com/showwin/speedtest-go/blob/master/LICENSE) speedtest-go-1.7.10/docs/000077500000000000000000000000001522770423200151375ustar00rootroot00000000000000speedtest-go-1.7.10/docs/experimental_result.md000066400000000000000000000062711522770423200215620ustar00rootroot00000000000000# Experimental Result 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]. ## Downlaod (Mbps) | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | | 0 - 1000 (9km) | 6691 | 93.34 | 93.51 | 89.13 | 88.40 | 88.21 | 83.34 | | 0 - 1000 (194km) | 6368 | 93.45 | 94.20 | 92.93 | 92.26 | 88.45 | 77.07 | | 0 - 1000 (865km) | 6842 | 88.31 | 89.91 | 91.26 | 93.30 | 67.33 | 87.52 | | 1000 - 8000 (1982km) | 2589 | 90.24 | 94.05 | 79.61 | 79.52 | 79.16 | 70.36 | | 1000 - 8000 (4801km) | 3296 | 75.61 | 52.83 | 69.89 | 69.86 | 58.19 | 55.93 | | 1000 - 8000 (6627km) | 1718 | 42.04 | 43.92 | 39.81 | 54.35 | 44.47 | 31.27 | | 8000 - 20000 (8972km) | 3409 | 22.11 | 22.74 | 28.21 | 21.82 | 24.78 | 30.59 | | 8000 - 20000 (13781km) | 3162 | 15.78 | 7.73 | 1.94 | 1.02 | 3.32 | 2.40 | | 8000 - 20000 (17805km) | 4256 | 1.15 | 1.50 | 1.79 | 1.79 | 6.41 | 3.69 | ## Upload (Mbps) | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | | 0 - 1000 (9km) | 6691 | 52.03 | 48.82 | 40.72 | 43.84 | 36.03 | 36.21 | 0 - 1000 (194km) | 6368 | 78.30 | 66.92 | 53.98 | 44.07 | 37.26 | 46.98 | 0 - 1000 (865km) | 6842 | 65.34 | 81.96 | 50.27 | 52.57 | 29.42 | 31.08 | 1000 - 8000 (1982km) | 2589 | 89.29 | 58.69 | 80.68 | 56.59 | 42.66 | 44.34 | 1000 - 8000 (4801km) | 3296 | 50.16 | 50.78 | 54.94 | 54.18 | 20.59 | 20.23 | 1000 - 8000 (6627km) | 1718 | 48.48 | 50.71 | 39.81 | 42.21 | 16.16 | 16.72 | 8000 - 20000 (8972km) | 3409 | 18.07 | 20.30 | 20.35 | 24.92 | 5.87 | 3.37 | 8000 - 20000 (13781km) | 3162 | 1.45 | 0.33 | 1.78 | 0.77 | 1.08 | 1.34 | 8000 - 20000 (17805km) | 4256 | 1.16 | 0.30 | 1.00 | 1.11 | 2.37 | 1.42 ## Testing Time (sec) | distance(km) | server id | speediest.net (test1) | speediest.net (test2) | speedtest-go (test1) | speedtest-go (test2) | speedtest-cli (test1) | speedtest-cli (test2) | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | :--: | | 0 - 1000 (9km) | 6691 | 44.87 | 45.76 | 24.45 | 22.40 | 20.82 | 16.51 | 0 - 1000 (194km) | 6368 | 43.85 | 42.97 | 19.60 | 22.36 | 22.25 | 24.01 | 0 - 1000 (865km) | 6842 | 46.62 | 46.12 | 21.83 | 26.40 | 39.07 | 24.10 | 1000 - 8000 (1982km) | 2589 | 45.38 | 44.18 | 18.71 | 21.35 | 26.15 | 25.71 | 1000 - 8000 (4801km) | 3296 | 47.92 | 50.52 | 24.71 | 23.47 | 32.07 | 28.53 | 1000 - 8000 (6627km) | 1718 | 40.15 | 41.16 | 28.05 | 30.42 | 31.01 | 27.65 | 8000 - 20000 (8972km) | 3409 | 51.53 | 56.89 | 38.13 | 41.55 | 36.87 | 34.47 | 8000 - 20000 (13781km) | 3162 | 52.75 | 58.10 | 28.78 | 29.91 | 40.75 | 46.88 | 8000 - 20000 (17805km) | 4256 | 37.73 | 40.82 | 33.33 | 32.79 | 37.82 | 50.78 speedtest-go-1.7.10/docs/release.md000066400000000000000000000007261522770423200171060ustar00rootroot00000000000000# Release flow This is a note for repo owner. ```bach $ git checkout -b release/vX.Y.Z # edit `var version` at speedtest/speedtest.go $ git commit -am 'Release vX.Y.Z' $ git push origin release/vX.Y.Z ``` merge PR ```bach $ git checkout master $ git pull origin master $ git tag vX.Y.Z $ git push origin vX.Y.Z # run GitHub Action to build packages and make release. ``` update brew formula https://github.com/showwin/homebrew-speedtest/blob/master/speedtest.rb#L3 speedtest-go-1.7.10/docs/saving_mode_experimental_result.md000066400000000000000000000013451522770423200241320ustar00rootroot00000000000000# --saving-mode Experimental Result `--saving-mode` vs normal mode. `N - Download` means normal mode download speed results. `S - RAM` means `--saving-mode` memory usage. | exp No. | N - Download | N - Upload | N - RAM | S - Download | S - Upload | S- RAM | | :-- | :--: | :--: | :--: | :--: | :--: | :--: | | 1 | 54.13 Mbit/s | 61.07 Mbit/s | 85.82MB | 50.65 Mbit/s | 29.25 Mbit/s | 5.39MB | | 2 | 47.65 Mbit/s | 57.94 Mbit/s | 93.84MB | 48.39 Mbit/s | 44.04 Mbit/s | 9.39MB | | 3 | 52.58 Mbit/s | 56.43 Mbit/s | 109.86MB | 24.34 Mbit/s | 41.68 Mbit/s | 9.39MB | | 4 | 43.73 Mbit/s | 49.46 Mbit/s | 109.86MB | 32.10 Mbit/s | 36.51 Mbit/s | 9.39MB | | 5 | 54.66 Mbit/s | 42.81 Mbit/s | 101.84MB | 40.08 Mbit/s | 10.37 Mbit/s | 5.39MB | speedtest-go-1.7.10/example/000077500000000000000000000000001522770423200156425ustar00rootroot00000000000000speedtest-go-1.7.10/example/multi/000077500000000000000000000000001522770423200167745ustar00rootroot00000000000000speedtest-go-1.7.10/example/multi/main.go000066400000000000000000000012211522770423200202430ustar00rootroot00000000000000package main import ( "context" "fmt" "github.com/showwin/speedtest-go/speedtest" "log" ) func main() { serverList, _ := speedtest.FetchServers() targets, _ := serverList.FindServer([]int{}) if len(targets) > 0 { // Use s as main server and use targets as auxiliary servers. // The main server is loaded at a greater proportion than the auxiliary servers. s := targets[0] checkError(s.MultiDownloadTestContext(context.TODO(), targets)) checkError(s.MultiUploadTestContext(context.TODO(), targets)) fmt.Printf("Download: %s, Upload: %s\n", s.DLSpeed, s.ULSpeed) } } func checkError(err error) { if err != nil { log.Fatal(err) } } speedtest-go-1.7.10/example/naive/000077500000000000000000000000001522770423200167445ustar00rootroot00000000000000speedtest-go-1.7.10/example/naive/main.go000066400000000000000000000023451522770423200202230ustar00rootroot00000000000000package main import ( "fmt" "github.com/showwin/speedtest-go/speedtest" "log" ) func main() { // _, _ = speedtest.FetchUserInfo() // Get a list of servers near a specified location // user.SetLocationByCity("Tokyo") // user.SetLocation("Osaka", 34.6952, 135.5006) // Select a network card as the data interface. // speedtest.WithUserConfig(&speedtest.UserConfig{Source: "192.168.1.101"})(speedtestClient) // Search server using serverID. // eg: fetch server with ID 28910. // speedtest.ErrEmptyServers will be returned if the server cannot be found. // server, err := speedtest.FetchServerByID("28910") serverList, _ := speedtest.FetchServers() targets, _ := serverList.FindServer([]int{}) for _, s := range targets { // Please make sure your host can access this test server, // otherwise you will get an error. // It is recommended to replace a server at this time checkError(s.PingTest(nil)) checkError(s.DownloadTest()) checkError(s.UploadTest()) // Note: The unit of s.DLSpeed, s.ULSpeed is bytes per second, this is a float64. fmt.Printf("Latency: %s, Download: %s, Upload: %s\n", s.Latency, s.DLSpeed, s.ULSpeed) s.Context.Reset() } } func checkError(err error) { if err != nil { log.Fatal(err) } } speedtest-go-1.7.10/example/packet_loss/000077500000000000000000000000001522770423200201515ustar00rootroot00000000000000speedtest-go-1.7.10/example/packet_loss/main.go000066400000000000000000000037231522770423200214310ustar00rootroot00000000000000package main import ( "fmt" "github.com/showwin/speedtest-go/speedtest" "github.com/showwin/speedtest-go/speedtest/transport" "log" "sync" "time" ) // Note: The current packet loss analyzer does not support udp over http. // This means we cannot get packet loss through a proxy. func main() { // 0. Fetching servers serverList, err := speedtest.FetchServers() checkError(err) // 1. Retrieve available servers targets := serverList.Available() // 2. Create a packet loss analyzer, use default options analyzer := speedtest.NewPacketLossAnalyzer(&speedtest.PacketLossAnalyzerOptions{ PacketSendingInterval: time.Millisecond * 100, }) wg := &sync.WaitGroup{} // 3. Perform packet loss analysis on all available servers for _, server := range *targets { wg.Add(1) //ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) //go func(server *speedtest.Server, analyzer *speedtest.PacketLossAnalyzer, ctx context.Context, cancel context.CancelFunc) { go func(server *speedtest.Server, analyzer *speedtest.PacketLossAnalyzer) { //defer cancel() defer wg.Done() // Note: Please call ctx.cancel at the appropriate time to release resources if you use analyzer.RunWithContext // we using analyzer.Run() here. err = analyzer.Run(server.Host, func(packetLoss *transport.PLoss) { fmt.Println(packetLoss, server.Host, server.Name) }) //err = analyzer.RunWithContext(ctx, server.Host, func(packetLoss *transport.PLoss) { // fmt.Println(packetLoss, server.Host, server.Name) //}) if err != nil { fmt.Println(err) } //}(server, analyzer, ctx, cancel) }(server, analyzer) } wg.Wait() // use mixed PacketLoss mixed, err := analyzer.RunMulti(serverList.Hosts()) checkError(err) fmt.Printf("Mixed packets lossed: %.2f%%\n", mixed.LossPercent()) fmt.Printf("Mixed packets lossed: %.2f\n", mixed.Loss()) fmt.Printf("Mixed packets lossed: %s\n", mixed) } func checkError(err error) { if err != nil { log.Fatal(err) } } speedtest-go-1.7.10/go.mod000066400000000000000000000010111522770423200153060ustar00rootroot00000000000000module github.com/showwin/speedtest-go go 1.19 require ( github.com/chelnak/ysmrr v0.5.0 gopkg.in/alecthomas/kingpin.v2 v2.2.6 ) require ( github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect github.com/fatih/color v1.18.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect ) speedtest-go-1.7.10/go.sum000066400000000000000000000060161522770423200153450ustar00rootroot00000000000000github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/chelnak/ysmrr v0.5.0 h1:aCLTtiJbzJVhiRTL1zyTGnWSCdK3R44QeFklPZRt8tg= github.com/chelnak/ysmrr v0.5.0/go.mod h1:Eg/IrbWqE3hOD5itwl2GlekRD7um93ap4gHOsxe+KvQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= speedtest-go-1.7.10/speedtest.go000066400000000000000000000243721522770423200165460ustar00rootroot00000000000000package main import ( "context" "errors" "fmt" "github.com/showwin/speedtest-go/speedtest/transport" "gopkg.in/alecthomas/kingpin.v2" "io" "log" "os" "strconv" "strings" "sync" "sync/atomic" "time" "github.com/showwin/speedtest-go/speedtest" ) var ( showList = kingpin.Flag("list", "Show available speedtest.net servers.").Short('l').Bool() serverIds = kingpin.Flag("server", "Select server id to run speedtest.").Short('s').Ints() customURL = kingpin.Flag("custom-url", "Specify the url of the server instead of fetching from speedtest.net.").String() savingMode = kingpin.Flag("saving-mode", "Test with few resources, though low accuracy (especially > 30Mbps).").Bool() jsonOutput = kingpin.Flag("json", "Output results in json format.").Bool() unixOutput = kingpin.Flag("unix", "Output results in unix like format.").Bool() location = kingpin.Flag("location", "Change the location with a precise coordinate (format: lat,lon).").String() city = kingpin.Flag("city", "Change the location with a predefined city label.").String() showCityList = kingpin.Flag("city-list", "List all predefined city labels.").Bool() proxy = kingpin.Flag("proxy", "Set a proxy(http[s] or socks) for the speedtest.").String() source = kingpin.Flag("source", "Bind a source interface for the speedtest.").String() dnsBindSource = kingpin.Flag("dns-bind-source", "DNS request binding source (experimental).").Bool() multi = kingpin.Flag("multi", "Enable multi-server mode.").Short('m').Bool() thread = kingpin.Flag("thread", "Set the number of concurrent connections.").Short('t').Int() search = kingpin.Flag("search", "Fuzzy search servers by a keyword.").String() userAgent = kingpin.Flag("ua", "Set the user-agent header for the speedtest.").String() noDownload = kingpin.Flag("no-download", "Disable download test.").Bool() noUpload = kingpin.Flag("no-upload", "Disable upload test.").Bool() pingMode = kingpin.Flag("ping-mode", "Select a method for Ping (support icmp/tcp/http).").Default("http").String() 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() debug = kingpin.Flag("debug", "Enable debug mode.").Short('d').Bool() ) var ( commit = "dev" date = "unknown" ) func main() { kingpin.Version(fmt.Sprintf("speedtest-go v%s git-%s built at %s", speedtest.Version(), commit, date)) kingpin.Parse() AppInfo() speedtest.SetUnit(parseUnit(*unit)) // discard standard log. log.SetOutput(io.Discard) // start unix output for saving mode by default. if *savingMode && !*jsonOutput && !*unixOutput { *unixOutput = true } // 0. speed test setting var speedtestClient = speedtest.New(speedtest.WithUserConfig( &speedtest.UserConfig{ UserAgent: *userAgent, Proxy: *proxy, Source: *source, DnsBindSource: *dnsBindSource, Debug: *debug, PingMode: parseProto(*pingMode), // TCP as default SavingMode: *savingMode, MaxConnections: *thread, CityFlag: *city, LocationFlag: *location, Keyword: *search, })) if *showCityList { speedtest.PrintCityList() return } // 1. retrieving user information taskManager := InitTaskManager(*jsonOutput, *unixOutput) taskManager.AsyncRun("Retrieving User Information", func(task *Task) { u, err := speedtestClient.FetchUserInfo() task.CheckError(err) task.Printf("ISP: %s", u.String()) task.Complete() }) // 2. retrieving servers var err error var servers speedtest.Servers var targets speedtest.Servers taskManager.Run("Retrieving Servers", func(task *Task) { if len(*customURL) > 0 { var target *speedtest.Server target, err = speedtestClient.CustomServer(*customURL) task.CheckError(err) targets = []*speedtest.Server{target} task.Println("Skip: Using Custom Server") } else if len(*serverIds) > 0 { // TODO: need async fetch to speedup for _, id := range *serverIds { serverPtr, errFetch := speedtestClient.FetchServerByID(strconv.Itoa(id)) if errFetch != nil { continue // Silently Skip all ids that actually don't exist. } targets = append(targets, serverPtr) } task.CheckError(err) task.Printf("Found %d Specified Public Server(s)", len(targets)) } else { servers, err = speedtestClient.FetchServers() task.CheckError(err) task.Printf("Found %d Public Servers", len(servers)) if *showList { task.Complete() task.manager.Reset() showServerList(servers) os.Exit(0) } targets, err = servers.FindServer(*serverIds) task.CheckError(err) } task.Complete() }) taskManager.Reset() // 3. test each selected server with ping, download and upload. for _, server := range targets { if !*jsonOutput { fmt.Println() } taskManager.Println("Test Server: " + server.String()) taskManager.Run("Latency: --", func(task *Task) { task.CheckError(server.PingTest(func(latency time.Duration) { task.Updatef("Latency: %v", latency) })) task.Printf("Latency: %v Jitter: %v Min: %v Max: %v", server.Latency, server.Jitter, server.MinLatency, server.MaxLatency) task.Complete() }) // 3.0 create a packet loss analyzer, use default options analyzer := speedtest.NewPacketLossAnalyzer(&speedtest.PacketLossAnalyzerOptions{ SourceInterface: *source, }) blocker := sync.WaitGroup{} packetLossAnalyzerCtx, packetLossAnalyzerCancel := context.WithTimeout(context.Background(), time.Second*40) taskManager.Run("Packet Loss Analyzer", func(task *Task) { blocker.Add(1) go func() { defer blocker.Done() err = analyzer.RunWithContext(packetLossAnalyzerCtx, server.Host, func(packetLoss *transport.PLoss) { server.PacketLoss = *packetLoss }) if errors.Is(err, transport.ErrUnsupported) { packetLossAnalyzerCancel() // cancel early } }() task.Println("Packet Loss Analyzer: Running in background (<= 30 Secs)") task.Complete() }) // 3.1 create accompany Echo accEcho := newAccompanyEcho(server, time.Millisecond*500) taskManager.RunWithTrigger(!*noDownload, "Download", func(task *Task) { accEcho.Run() speedtestClient.SetCallbackDownload(func(downRate speedtest.ByteRate) { lc := accEcho.CurrentLatency() if lc == 0 { task.Updatef("Download: %s (Latency: --)", downRate) } else { task.Updatef("Download: %s (Latency: %dms)", downRate, lc/1000000) } }) if *multi { task.CheckError(server.MultiDownloadTestContext(context.Background(), servers)) } else { task.CheckError(server.DownloadTest()) } accEcho.Stop() mean, _, std, minL, maxL := speedtest.StandardDeviation(accEcho.Latencies()) 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) task.Complete() }) taskManager.RunWithTrigger(!*noUpload, "Upload", func(task *Task) { accEcho.Run() speedtestClient.SetCallbackUpload(func(upRate speedtest.ByteRate) { lc := accEcho.CurrentLatency() if lc == 0 { task.Updatef("Upload: %s (Latency: --)", upRate) } else { task.Updatef("Upload: %s (Latency: %dms)", upRate, lc/1000000) } }) if *multi { task.CheckError(server.MultiUploadTestContext(context.Background(), servers)) } else { task.CheckError(server.UploadTest()) } accEcho.Stop() mean, _, std, minL, maxL := speedtest.StandardDeviation(accEcho.Latencies()) 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) task.Complete() }) if *noUpload && *noDownload { time.Sleep(time.Second * 30) } packetLossAnalyzerCancel() blocker.Wait() if !*jsonOutput { taskManager.Println(server.PacketLoss.String()) } taskManager.Reset() speedtestClient.Manager.Reset() } taskManager.Stop() if *jsonOutput { json, errMarshal := speedtestClient.JSON(targets) if errMarshal != nil { panic(errMarshal) } fmt.Print(string(json)) } } type AccompanyEcho struct { stopEcho chan bool server *speedtest.Server currentLatency int64 interval time.Duration latencies []int64 } func newAccompanyEcho(server *speedtest.Server, interval time.Duration) *AccompanyEcho { return &AccompanyEcho{ server: server, interval: interval, stopEcho: make(chan bool), } } func (ae *AccompanyEcho) Run() { ae.latencies = make([]int64, 0) ctx, cancel := context.WithCancel(context.Background()) go func() { for { select { case <-ae.stopEcho: cancel() return default: latency, _ := ae.server.HTTPPing(ctx, 1, ae.interval, nil) if len(latency) > 0 { atomic.StoreInt64(&ae.currentLatency, latency[0]) ae.latencies = append(ae.latencies, latency[0]) } } } }() } func (ae *AccompanyEcho) Stop() { ae.stopEcho <- false } func (ae *AccompanyEcho) CurrentLatency() int64 { return atomic.LoadInt64(&ae.currentLatency) } func (ae *AccompanyEcho) Latencies() []int64 { return ae.latencies } func showServerList(servers speedtest.Servers) { for _, s := range servers { fmt.Printf("[%5s] %9.2fkm ", s.ID, s.Distance) if s.Latency == -1 { fmt.Printf("%v", "Timeout ") } else { fmt.Printf("%-dms ", s.Latency/time.Millisecond) } fmt.Printf("\t%s (%s) by %s \n", s.Name, s.Country, s.Sponsor) } } func parseUnit(str string) speedtest.UnitType { str = strings.ToLower(str) if str == "decimal-bits" { return speedtest.UnitTypeDecimalBits } else if str == "decimal-bytes" { return speedtest.UnitTypeDecimalBytes } else if str == "binary-bits" { return speedtest.UnitTypeBinaryBits } else if str == "binary-bytes" { return speedtest.UnitTypeBinaryBytes } else { return speedtest.UnitTypeDefaultMbps } } func parseProto(str string) speedtest.Proto { str = strings.ToLower(str) if str == "icmp" { return speedtest.ICMP } else if str == "tcp" { return speedtest.TCP } else { return speedtest.HTTP } } func AppInfo() { if !*jsonOutput { fmt.Println() fmt.Printf(" speedtest-go v%s (git-%s) @showwin\n", speedtest.Version(), commit) fmt.Println() } } speedtest-go-1.7.10/speedtest/000077500000000000000000000000001522770423200162075ustar00rootroot00000000000000speedtest-go-1.7.10/speedtest/data_manager.go000066400000000000000000000325571522770423200211550ustar00rootroot00000000000000package speedtest import ( "bytes" "context" "errors" "github.com/showwin/speedtest-go/speedtest/internal" "io" "math" "runtime" "sync" "sync/atomic" "time" ) type Manager interface { SetRateCaptureFrequency(duration time.Duration) Manager SetCaptureTime(duration time.Duration) Manager NewChunk() Chunk GetTotalDownload() int64 GetTotalUpload() int64 AddTotalDownload(value int64) AddTotalUpload(value int64) GetAvgDownloadRate() float64 GetAvgUploadRate() float64 GetEWMADownloadRate() float64 GetEWMAUploadRate() float64 SetCallbackDownload(callback func(downRate ByteRate)) SetCallbackUpload(callback func(upRate ByteRate)) RegisterDownloadHandler(fn func()) *TestDirection RegisterUploadHandler(fn func()) *TestDirection // Wait for the upload or download task to end to avoid errors caused by core occupation Wait() Reset() Snapshots() *Snapshots SetNThread(n int) Manager } type Chunk interface { UploadHandler(size int64) Chunk DownloadHandler(r io.Reader) error GetRate() float64 GetDuration() time.Duration GetParent() Manager Read(b []byte) (n int, err error) } const readChunkSize = 1024 // 1 KBytes with higher frequency rate feedback type DataType int32 const ( typeEmptyChunk = iota typeDownload typeUpload ) var ( ErrorUninitializedManager = errors.New("uninitialized manager") ) type funcGroup struct { fns []func() } func (f *funcGroup) Add(fn func()) { f.fns = append(f.fns, fn) } type DataManager struct { SnapshotStore *Snapshots Snapshot *Snapshot sync.Mutex repeatByte *[]byte captureTime time.Duration rateCaptureFrequency time.Duration nThread int running bool runningRW sync.RWMutex download *TestDirection upload *TestDirection } type TestDirection struct { TestType int // test type manager *DataManager // manager totalDataVolume int64 // total send/receive data volume RateSequence []int64 // rate history sequence welford *internal.Welford // std/EWMA/mean captureCallback func(realTimeRate ByteRate) // user callback closeFunc func() // close func *funcGroup // actually exec function } func (dm *DataManager) NewDataDirection(testType int) *TestDirection { return &TestDirection{ TestType: testType, manager: dm, funcGroup: &funcGroup{}, } } func NewDataManager() *DataManager { r := bytes.Repeat([]byte{0xAA}, readChunkSize) // uniformly distributed sequence of bits ret := &DataManager{ nThread: runtime.NumCPU(), captureTime: time.Second * 15, rateCaptureFrequency: time.Millisecond * 50, Snapshot: &Snapshot{}, repeatByte: &r, } ret.download = ret.NewDataDirection(typeDownload) ret.upload = ret.NewDataDirection(typeUpload) ret.SnapshotStore = newHistorySnapshots(maxSnapshotSize) return ret } func (dm *DataManager) SetCallbackDownload(callback func(downRate ByteRate)) { if dm.download != nil { dm.download.captureCallback = callback } } func (dm *DataManager) SetCallbackUpload(callback func(upRate ByteRate)) { if dm.upload != nil { dm.upload.captureCallback = callback } } func (dm *DataManager) Wait() { oldDownTotal := dm.GetTotalDownload() oldUpTotal := dm.GetTotalUpload() for { time.Sleep(dm.rateCaptureFrequency) newDownTotal := dm.GetTotalDownload() newUpTotal := dm.GetTotalUpload() deltaDown := newDownTotal - oldDownTotal deltaUp := newUpTotal - oldUpTotal oldDownTotal = newDownTotal oldUpTotal = newUpTotal if deltaDown == 0 && deltaUp == 0 { return } } } func (dm *DataManager) RegisterUploadHandler(fn func()) *TestDirection { if len(dm.upload.fns) < dm.nThread { dm.upload.Add(fn) } return dm.upload } func (dm *DataManager) RegisterDownloadHandler(fn func()) *TestDirection { if len(dm.download.fns) < dm.nThread { dm.download.Add(fn) } return dm.download } func (td *TestDirection) GetTotalDataVolume() int64 { return atomic.LoadInt64(&td.totalDataVolume) } func (td *TestDirection) AddTotalDataVolume(delta int64) int64 { return atomic.AddInt64(&td.totalDataVolume, delta) } func (td *TestDirection) Start(cancel context.CancelFunc, mainRequestHandlerIndex int) { if len(td.fns) == 0 { panic("empty task stack") } if mainRequestHandlerIndex > len(td.fns)-1 { mainRequestHandlerIndex = 0 } mainLoadFactor := 0.1 // When the number of processor cores is equivalent to the processing program, // the processing efficiency reaches the highest level (VT is not considered). mainN := int(mainLoadFactor * float64(len(td.fns))) if mainN == 0 { mainN = 1 } if len(td.fns) == 1 { mainN = td.manager.nThread } auxN := td.manager.nThread - mainN dbg.Printf("Available fns: %d\n", len(td.fns)) dbg.Printf("mainN: %d\n", mainN) dbg.Printf("auxN: %d\n", auxN) wg := sync.WaitGroup{} td.manager.running = true stopCapture := td.rateCapture() // refresh once function once := sync.Once{} td.closeFunc = func() { once.Do(func() { stopCapture <- true close(stopCapture) td.manager.runningRW.Lock() td.manager.running = false td.manager.runningRW.Unlock() cancel() dbg.Println("FuncGroup: Stop") }) } time.AfterFunc(td.manager.captureTime, td.closeFunc) for i := 0; i < mainN; i++ { wg.Add(1) go func() { defer wg.Done() for { td.manager.runningRW.RLock() running := td.manager.running td.manager.runningRW.RUnlock() if !running { return } td.fns[mainRequestHandlerIndex]() } }() } for j := 0; j < auxN; { for i := range td.fns { if j == auxN { break } if i == mainRequestHandlerIndex { continue } wg.Add(1) t := i go func() { defer wg.Done() for { td.manager.runningRW.RLock() running := td.manager.running td.manager.runningRW.RUnlock() if !running { return } td.fns[t]() } }() j++ } } wg.Wait() } func (td *TestDirection) rateCapture() chan bool { ticker := time.NewTicker(td.manager.rateCaptureFrequency) var prevTotalDataVolume int64 = 0 stopCapture := make(chan bool) td.welford = internal.NewWelford(5*time.Second, td.manager.rateCaptureFrequency) sTime := time.Now() go func(t *time.Ticker) { defer t.Stop() for { select { case <-t.C: newTotalDataVolume := td.GetTotalDataVolume() deltaDataVolume := newTotalDataVolume - prevTotalDataVolume prevTotalDataVolume = newTotalDataVolume if deltaDataVolume != 0 { td.RateSequence = append(td.RateSequence, deltaDataVolume) } // anyway we update the measuring instrument globalAvg := (float64(td.GetTotalDataVolume())) / float64(time.Since(sTime).Milliseconds()) * 1000 if td.welford.Update(globalAvg, float64(deltaDataVolume)) { go td.closeFunc() } // reports the current rate at the given rate if td.captureCallback != nil { td.captureCallback(ByteRate(td.welford.EWMA())) } case stop := <-stopCapture: if stop { return } } } }(ticker) return stopCapture } func (dm *DataManager) NewChunk() Chunk { var dc DataChunk dc.manager = dm dm.Lock() *dm.Snapshot = append(*dm.Snapshot, &dc) dm.Unlock() return &dc } func (dm *DataManager) AddTotalDownload(value int64) { dm.download.AddTotalDataVolume(value) } func (dm *DataManager) AddTotalUpload(value int64) { dm.upload.AddTotalDataVolume(value) } func (dm *DataManager) GetTotalDownload() int64 { return dm.download.GetTotalDataVolume() } func (dm *DataManager) GetTotalUpload() int64 { return dm.upload.GetTotalDataVolume() } func (dm *DataManager) SetRateCaptureFrequency(duration time.Duration) Manager { dm.rateCaptureFrequency = duration return dm } func (dm *DataManager) SetCaptureTime(duration time.Duration) Manager { dm.captureTime = duration return dm } func (dm *DataManager) SetNThread(n int) Manager { if n < 1 { dm.nThread = runtime.NumCPU() } else { dm.nThread = n } return dm } func (dm *DataManager) Snapshots() *Snapshots { return dm.SnapshotStore } func (dm *DataManager) Reset() { dm.SnapshotStore.push(dm.Snapshot) dm.Snapshot = &Snapshot{} dm.download = dm.NewDataDirection(typeDownload) dm.upload = dm.NewDataDirection(typeUpload) } func (dm *DataManager) GetAvgDownloadRate() float64 { unit := float64(dm.captureTime / time.Millisecond) return float64(dm.download.GetTotalDataVolume()*8/1000) / unit } func (dm *DataManager) GetEWMADownloadRate() float64 { if dm.download.welford != nil { return dm.download.welford.EWMA() } return 0 } func (dm *DataManager) GetAvgUploadRate() float64 { unit := float64(dm.captureTime / time.Millisecond) return float64(dm.upload.GetTotalDataVolume()*8/1000) / unit } func (dm *DataManager) GetEWMAUploadRate() float64 { if dm.upload.welford != nil { return dm.upload.welford.EWMA() } return 0 } type DataChunk struct { manager *DataManager dateType DataType startTime time.Time endTime time.Time err error ContentLength int64 remainOrDiscardSize int64 } var blackHolePool = sync.Pool{ New: func() any { b := make([]byte, 8192) return &b }, } func (dc *DataChunk) GetDuration() time.Duration { return dc.endTime.Sub(dc.startTime) } func (dc *DataChunk) GetRate() float64 { if dc.dateType == typeDownload { return float64(dc.remainOrDiscardSize) / dc.GetDuration().Seconds() } else if dc.dateType == typeUpload { return float64(dc.ContentLength-dc.remainOrDiscardSize) * 8 / 1000 / 1000 / dc.GetDuration().Seconds() } return 0 } // DownloadHandler No value will be returned here, because the error will interrupt the test. // The error chunk is generally caused by the remote server actively closing the connection. func (dc *DataChunk) DownloadHandler(r io.Reader) error { if dc.dateType != typeEmptyChunk { dc.err = errors.New("multiple calls to the same chunk handler are not allowed") return dc.err } dc.dateType = typeDownload dc.startTime = time.Now() defer func() { dc.endTime = time.Now() }() bufP := blackHolePool.Get().(*[]byte) defer blackHolePool.Put(bufP) readSize := 0 for { dc.manager.runningRW.RLock() running := dc.manager.running dc.manager.runningRW.RUnlock() if !running { return nil } readSize, dc.err = r.Read(*bufP) rs := int64(readSize) dc.remainOrDiscardSize += rs dc.manager.download.AddTotalDataVolume(rs) if dc.err != nil { if dc.err == io.EOF { return nil } return dc.err } } } func (dc *DataChunk) UploadHandler(size int64) Chunk { if dc.dateType != typeEmptyChunk { dc.err = errors.New("multiple calls to the same chunk handler are not allowed") } if size <= 0 { panic("the size of repeated bytes should be > 0") } dc.ContentLength = size dc.remainOrDiscardSize = size dc.dateType = typeUpload dc.startTime = time.Now() return dc } func (dc *DataChunk) GetParent() Manager { return dc.manager } func (dc *DataChunk) Read(b []byte) (n int, err error) { if dc.remainOrDiscardSize < readChunkSize { if dc.remainOrDiscardSize <= 0 { dc.endTime = time.Now() return n, io.EOF } n = copy(b, (*dc.manager.repeatByte)[:dc.remainOrDiscardSize]) } else { n = copy(b, *dc.manager.repeatByte) } n64 := int64(n) dc.remainOrDiscardSize -= n64 dc.manager.AddTotalUpload(n64) return } // calcMAFilter Median-Averaging Filter func _(list []int64) float64 { if len(list) == 0 { return 0 } var sum int64 = 0 n := len(list) if n == 0 { return 0 } length := len(list) for i := 0; i < length-1; i++ { for j := i + 1; j < length; j++ { if list[i] > list[j] { list[i], list[j] = list[j], list[i] } } } for i := 1; i < n-1; i++ { sum += list[i] } return float64(sum) / float64(n-2) } func pautaFilter(vector []int64) []int64 { dbg.Println("Per capture unit") dbg.Printf("Raw Sequence len: %d\n", len(vector)) dbg.Printf("Raw Sequence: %v\n", vector) if len(vector) == 0 { return vector } mean, _, std, _, _ := sampleVariance(vector) var retVec []int64 for _, value := range vector { if math.Abs(float64(value-mean)) < float64(3*std) { retVec = append(retVec, value) } } dbg.Printf("Raw average: %dByte\n", mean) dbg.Printf("Pauta Sequence len: %d\n", len(retVec)) dbg.Printf("Pauta Sequence: %v\n", retVec) return retVec } // sampleVariance sample Variance func sampleVariance(vector []int64) (mean, variance, stdDev, min, max int64) { if len(vector) == 0 { return 0, 0, 0, 0, 0 } var sumNum, accumulate int64 min = math.MaxInt64 max = math.MinInt64 for _, value := range vector { sumNum += value if min > value { min = value } if max < value { max = value } } mean = sumNum / int64(len(vector)) for _, value := range vector { accumulate += (value - mean) * (value - mean) } variance = accumulate / int64(len(vector)-1) // Bessel's correction stdDev = int64(math.Sqrt(float64(variance))) return } const maxSnapshotSize = 10 type Snapshot []*DataChunk type Snapshots struct { sp []*Snapshot maxSize int } func newHistorySnapshots(size int) *Snapshots { return &Snapshots{ sp: make([]*Snapshot, 0, size), maxSize: size, } } func (rs *Snapshots) push(value *Snapshot) { if len(rs.sp) == rs.maxSize { rs.sp = rs.sp[1:] } rs.sp = append(rs.sp, value) } func (rs *Snapshots) Latest() *Snapshot { if len(rs.sp) > 0 { return rs.sp[len(rs.sp)-1] } return nil } func (rs *Snapshots) All() []*Snapshot { return rs.sp } func (rs *Snapshots) Clean() { rs.sp = make([]*Snapshot, 0, rs.maxSize) } speedtest-go-1.7.10/speedtest/data_manager_test.go000066400000000000000000000044471522770423200222110ustar00rootroot00000000000000package speedtest import ( "fmt" "sync" "testing" "time" ) func BenchmarkDataManager_NewDataChunk(b *testing.B) { dmp := NewDataManager() dmp.Reset() for i := 0; i < b.N; i++ { dmp.NewChunk() } } func BenchmarkDataManager_AddTotalDownload(b *testing.B) { dmp := NewDataManager() for i := 0; i < b.N; i++ { dmp.AddTotalDownload(43521) } } func TestDataManager_AddTotalDownload(t *testing.T) { dmp := NewDataManager() wg := sync.WaitGroup{} for i := 0; i < 1000; i++ { wg.Add(1) go func() { for j := 0; j < 1000; j++ { dmp.AddTotalDownload(43521) } wg.Done() }() } wg.Wait() if dmp.download.GetTotalDataVolume() != 43521000000 { t.Fatal() } } func TestDataManager_GetAvgDownloadRate(t *testing.T) { dm := NewDataManager() dm.download.totalDataVolume = 3000000 dm.captureTime = time.Second * 10 result := dm.GetAvgDownloadRate() if result != 2.4 { t.Fatal() } } func TestDynamicRate(t *testing.T) { server, _ := CustomServer("http://shenzhen.cmcc.speedtest.shunshiidc.com:8080/speedtest/upload.php") //server, _ := CustomServer("http://192.168.5.237:8080/speedtest/upload.php") oldDownTotal := server.Context.Manager.GetTotalDownload() oldUpTotal := server.Context.Manager.GetTotalUpload() server.Context.Manager.SetRateCaptureFrequency(time.Millisecond * 100) server.Context.Manager.SetCaptureTime(time.Second) go func() { for i := 0; i < 2; i++ { time.Sleep(time.Second) newDownTotal := server.Context.Manager.GetTotalDownload() newUpTotal := server.Context.Manager.GetTotalUpload() downRate := float64(newDownTotal-oldDownTotal) * 8 / 1000 / 1000 upRate := float64(newUpTotal-oldUpTotal) * 8 / 1000 / 1000 oldDownTotal = newDownTotal oldUpTotal = newUpTotal fmt.Printf("downRate: %.2fMbps | upRate: %.2fMbps\n", downRate, upRate) } }() err := server.DownloadTest() if err != nil { fmt.Println("Warning: not found server") //t.Error(err) } server.Context.Manager.Wait() err = server.UploadTest() if err != nil { fmt.Println("Warning: not found server") //t.Error(err) } fmt.Printf(" \n") fmt.Printf("Download: %5.2f Mbit/s\n", server.DLSpeed) fmt.Printf("Upload: %5.2f Mbit/s\n\n", server.ULSpeed) valid := server.CheckResultValid() if !valid { fmt.Println("Warning: result seems to be wrong. Please test again.") } } speedtest-go-1.7.10/speedtest/debug.go000066400000000000000000000006631522770423200176310ustar00rootroot00000000000000package speedtest import ( "log" "os" ) type Debug struct { dbg *log.Logger flag bool } func NewDebug() *Debug { return &Debug{dbg: log.New(os.Stdout, "[DBG]", log.Ldate|log.Ltime)} } func (d *Debug) Enable() { d.flag = true } func (d *Debug) Println(v ...any) { if d.flag { d.dbg.Println(v...) } } func (d *Debug) Printf(format string, v ...any) { if d.flag { d.dbg.Printf(format, v...) } } var dbg = NewDebug() speedtest-go-1.7.10/speedtest/internal/000077500000000000000000000000001522770423200200235ustar00rootroot00000000000000speedtest-go-1.7.10/speedtest/internal/README.md000066400000000000000000000017571522770423200213140ustar00rootroot00000000000000# Issue #192 SpeedTest-Go (1) 1. Use welford alg to quickly calculate standard deviation and mean. 2. The welford alg integrated moving window feature, This allows us to ignore early data with excessive volatility. 3. Use the coefficient of variation(c.v) to reflect the confidence of the test result datasets. 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. 5. Perform EWMA operation on real-time global average, and use c.v as part of the EWMA feedback parameter. 6. The ewma value calculated is the result value of our test. 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. speedtest-go-1.7.10/speedtest/internal/welford.go000066400000000000000000000073041522770423200220200ustar00rootroot00000000000000package internal import ( "fmt" "math" "time" ) // Welford Fast standard deviation calculation with moving window // 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 type Welford struct { n int // data size cap int // queue capacity vector []float64 // data set mean float64 // mean sum float64 // sum eraseIndex int // the value will be erased next time currentStdDev float64 consecutiveStableIterations int consecutiveStableIterationsThreshold int cv float64 ewmaMean float64 steps int minSteps int beta float64 scale float64 movingVector []float64 // data set movingAvg float64 } // NewWelford recommended windowSize = cycle / sampling frequency func NewWelford(cycle, frequency time.Duration) *Welford { windowSize := int(cycle / frequency) return &Welford{ vector: make([]float64, windowSize), movingVector: make([]float64, windowSize), cap: windowSize, consecutiveStableIterationsThreshold: windowSize / 3, // 33% minSteps: windowSize * 2, // set minimum steps with 2x windowSize. beta: 2 / (float64(windowSize) + 1), // ewma beta ratio scale: float64(time.Second / frequency), } } // Update Enter the given value into the measuring system. // return bool stability evaluation func (w *Welford) Update(globalAvg, value float64) bool { value = value * w.scale if w.n == w.cap { delta := w.vector[w.eraseIndex] - w.mean w.mean -= delta / float64(w.n-1) w.sum -= delta * (w.vector[w.eraseIndex] - w.mean) // the calc error is approximated to zero if w.sum < 0 { w.sum = 0 } w.vector[w.eraseIndex] = globalAvg w.movingAvg -= w.movingVector[w.eraseIndex] w.movingVector[w.eraseIndex] = value w.movingAvg += value w.eraseIndex++ if w.eraseIndex == w.cap { w.eraseIndex = 0 } } else { w.vector[w.n] = globalAvg w.movingVector[w.n] = value w.movingAvg += value w.n++ } delta := globalAvg - w.mean w.mean += delta / float64(w.n) w.sum += delta * (globalAvg - w.mean) w.currentStdDev = math.Sqrt(w.Variance()) // update C.V if w.mean != 0 { w.cv = w.currentStdDev / w.mean } w.ewmaMean = value*w.beta + w.ewmaMean*(1-w.beta) // acc consecutiveStableIterations if w.n == w.cap && w.cv < 0.03 { w.consecutiveStableIterations++ } else if w.consecutiveStableIterations > 0 { w.consecutiveStableIterations-- } w.steps++ return w.consecutiveStableIterations >= w.consecutiveStableIterationsThreshold && w.steps > w.minSteps } func (w *Welford) Mean() float64 { return w.mean } func (w *Welford) CV() float64 { return w.cv } func (w *Welford) Variance() float64 { if w.n < 2 { return 0 } return w.sum / float64(w.n-1) } func (w *Welford) StandardDeviation() float64 { return w.currentStdDev } func (w *Welford) EWMA() float64 { return w.ewmaMean*0.5 + w.movingAvg/float64(w.n)*0.5 } func (w *Welford) String() string { return fmt.Sprintf("Mean: %.2f, Standard Deviation: %.2f, C.V: %.2f, EWMA: %.2f", w.Mean(), w.StandardDeviation(), w.CV(), w.EWMA()) } speedtest-go-1.7.10/speedtest/internal/welford_test.go000066400000000000000000000177551522770423200230720ustar00rootroot00000000000000package internal import ( "fmt" "math/rand" "testing" "time" ) func BenchmarkWOM(b *testing.B) { w := NewWelford(time.Second*5, time.Millisecond*50) rd := rand.New(rand.NewSource(0)) var arr []float64 for i := 0; i < 100; i++ { arr = append(arr, rd.Float64()) } for i := 0; i < b.N; i++ { w.Update(arr[i%100], arr[i%100]) } } func TestWOM(t *testing.T) { 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} // 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} w := NewWelford(time.Second*5, time.Millisecond*50) ok := false for i, x := range data { if w.Update(x, x) { ok = true break } fmt.Printf("[%d] %s\n", i, w) } if !ok { t.Fatal("TestWOM failed") } } speedtest-go-1.7.10/speedtest/location.go000066400000000000000000000070341522770423200203520ustar00rootroot00000000000000package speedtest import ( "errors" "fmt" "strconv" "strings" ) type Location struct { Name string CC string Lat float64 Lon float64 } // Locations TODO more location need to added var Locations = map[string]*Location{ "brasilia": {"brasilia", "br", -15.793876, -47.8835327}, "hongkong": {"hongkong", "hk", 22.3106806, 114.1700546}, "tokyo": {"tokyo", "jp", 35.680938, 139.7674114}, "london": {"london", "uk", 51.5072493, -0.1288861}, "moscow": {"moscow", "ru", 55.7497248, 37.615989}, "beijing": {"beijing", "cn", 39.8721243, 116.4077473}, "paris": {"paris", "fr", 48.8626026, 2.3477229}, "sanfrancisco": {"sanfrancisco", "us", 37.7540028, -122.4429967}, "newyork": {"newyork", "us", 40.7200876, -74.0220945}, "yishun": {"yishun", "sg", 1.4230218, 103.8404728}, "delhi": {"delhi", "in", 28.6251287, 77.1960896}, "monterrey": {"monterrey", "mx", 25.6881435, -100.3073485}, "berlin": {"berlin", "de", 52.5168128, 13.4009469}, "maputo": {"maputo", "mz", -25.9579267, 32.5760444}, "honolulu": {"honolulu", "us", 20.8247065, -156.918706}, "seoul": {"seoul", "kr", 37.6086268, 126.7179721}, "osaka": {"osaka", "jp", 34.6952743, 135.5006967}, "shanghai": {"shanghai", "cn", 31.2292105, 121.4661666}, "urumqi": {"urumqi", "cn", 43.8256624, 87.6058564}, "ottawa": {"ottawa", "ca", 45.4161836, -75.7035467}, "capetown": {"capetown", "za", -33.9391993, 18.4316716}, "sydney": {"sydney", "au", -33.8966622, 151.1731861}, "perth": {"perth", "au", -31.9551812, 115.8591904}, "warsaw": {"warsaw", "pl", 52.2396659, 21.0129345}, "kampala": {"kampala", "ug", 0.3070027, 32.5675581}, "bangkok": {"bangkok", "th", 13.7248936, 100.493026}, } func PrintCityList() { fmt.Println("Available city labels (case insensitive): ") fmt.Println(" CC\t\tCityLabel\tLocation") for k, v := range Locations { fmt.Printf("(%v)\t%20s\t[%v, %v]\n", v.CC, k, v.Lat, v.Lon) } } func GetLocation(locationName string) (*Location, error) { loc, ok := Locations[strings.ToLower(locationName)] if ok { return loc, nil } return nil, errors.New("not found location") } // NewLocation new a Location func NewLocation(locationName string, latitude float64, longitude float64) *Location { var loc Location loc.Lat = latitude loc.Lon = longitude loc.Name = locationName Locations[locationName] = &loc return &loc } // ParseLocation parse latitude and longitude string func ParseLocation(locationName string, coordinateStr string) (*Location, error) { ll := strings.Split(coordinateStr, ",") if len(ll) == 2 { // parameters check lat, err := betweenRange(ll[0], 90) if err != nil { return nil, err } lon, err := betweenRange(ll[1], 180) if err != nil { return nil, err } name := "Custom-%s" if len(locationName) == 0 { name = "Custom-Default" } return NewLocation(fmt.Sprintf(name, locationName), lat, lon), nil } return nil, fmt.Errorf("invalid location input: %s", coordinateStr) } func (l *Location) String() string { return fmt.Sprintf("(%s) [%v, %v]", l.Name, l.Lat, l.Lon) } // betweenRange latitude and longitude range check func betweenRange(inputStrValue string, interval float64) (float64, error) { value, err := strconv.ParseFloat(inputStrValue, 64) if err != nil { return 0, fmt.Errorf("invalid input: %v", inputStrValue) } if value < -interval || interval < value { return 0, fmt.Errorf("invalid input. got: %v, expected between -%v and %v", inputStrValue, interval, interval) } return value, nil } speedtest-go-1.7.10/speedtest/loss.go000066400000000000000000000105101522770423200175130ustar00rootroot00000000000000package speedtest import ( "context" "github.com/showwin/speedtest-go/speedtest/transport" "net" "sync" "time" ) type PacketLossAnalyzerOptions struct { RemoteSamplingInterval time.Duration SamplingDuration time.Duration PacketSendingInterval time.Duration PacketSendingTimeout time.Duration SourceInterface string // source interface TCPDialer *net.Dialer // tcp dialer for sampling UDPDialer *net.Dialer // udp dialer for sending packet } type PacketLossAnalyzer struct { options *PacketLossAnalyzerOptions } func NewPacketLossAnalyzer(options *PacketLossAnalyzerOptions) *PacketLossAnalyzer { if options == nil { options = &PacketLossAnalyzerOptions{} } if options.SamplingDuration == 0 { options.SamplingDuration = time.Second * 30 } if options.RemoteSamplingInterval == 0 { options.RemoteSamplingInterval = 1 * time.Second } if options.PacketSendingInterval == 0 { options.PacketSendingInterval = 67 * time.Millisecond } if options.PacketSendingTimeout == 0 { options.PacketSendingTimeout = 5 * time.Second } if options.TCPDialer == nil { options.TCPDialer = &net.Dialer{ Timeout: options.PacketSendingTimeout, } } if options.UDPDialer == nil { var addr net.Addr if len(options.SourceInterface) > 0 { // skip error and using auto-select addr, _ = net.ResolveUDPAddr("udp", options.SourceInterface) } options.UDPDialer = &net.Dialer{ Timeout: options.PacketSendingTimeout, LocalAddr: addr, } } return &PacketLossAnalyzer{ options: options, } } // RunMulti Mix all servers to get the average packet loss. func (pla *PacketLossAnalyzer) RunMulti(hosts []string) (*transport.PLoss, error) { ctx, cancel := context.WithTimeout(context.Background(), pla.options.SamplingDuration) defer cancel() return pla.RunMultiWithContext(ctx, hosts) } func (pla *PacketLossAnalyzer) RunMultiWithContext(ctx context.Context, hosts []string) (*transport.PLoss, error) { results := make(map[string]*transport.PLoss) mutex := &sync.Mutex{} wg := &sync.WaitGroup{} for _, host := range hosts { wg.Add(1) go func(h string) { defer wg.Done() _ = pla.RunWithContext(ctx, h, func(packetLoss *transport.PLoss) { if packetLoss.Sent != 0 { mutex.Lock() results[h] = packetLoss mutex.Unlock() } }) }(host) } wg.Wait() if len(results) == 0 { return nil, transport.ErrUnsupported } var pLoss transport.PLoss for _, hostPacketLoss := range results { pLoss.Sent += hostPacketLoss.Sent pLoss.Dup += hostPacketLoss.Dup pLoss.Max += hostPacketLoss.Max } return &pLoss, nil } func (pla *PacketLossAnalyzer) Run(host string, callback func(packetLoss *transport.PLoss)) error { ctx, cancel := context.WithTimeout(context.Background(), pla.options.SamplingDuration) defer cancel() return pla.RunWithContext(ctx, host, callback) } func (pla *PacketLossAnalyzer) RunWithContext(ctx context.Context, host string, callback func(packetLoss *transport.PLoss)) error { samplerClient, err := transport.NewClient(pla.options.TCPDialer) if err != nil { return transport.ErrUnsupported } senderClient, err := transport.NewPacketLossSender(samplerClient.ID(), pla.options.UDPDialer) if err != nil { return transport.ErrUnsupported } if err = samplerClient.Connect(ctx, host); err != nil { return transport.ErrUnsupported } if err = senderClient.Connect(ctx, host); err != nil { return transport.ErrUnsupported } if err = samplerClient.InitPacketLoss(); err != nil { return transport.ErrUnsupported } go pla.loopSender(ctx, senderClient) return pla.loopSampler(ctx, samplerClient, callback) } func (pla *PacketLossAnalyzer) loopSampler(ctx context.Context, client *transport.Client, callback func(packetLoss *transport.PLoss)) error { ticker := time.NewTicker(pla.options.RemoteSamplingInterval) defer ticker.Stop() for { select { case <-ticker.C: if pl, err1 := client.PacketLoss(); err1 == nil { if pl != nil { callback(pl) } } else { return err1 } case <-ctx.Done(): return nil } } } func (pla *PacketLossAnalyzer) loopSender(ctx context.Context, senderClient *transport.PacketLossSender) { order := 0 sendTick := time.NewTicker(pla.options.PacketSendingInterval) defer sendTick.Stop() for { select { case <-sendTick.C: _ = senderClient.Send(order) order++ case <-ctx.Done(): return } } } speedtest-go-1.7.10/speedtest/output.go000066400000000000000000000011161522770423200200750ustar00rootroot00000000000000package speedtest import ( "encoding/json" "fmt" "time" ) type fullOutput struct { Timestamp outputTime `json:"timestamp"` UserInfo *User `json:"user_info"` Servers Servers `json:"servers"` } type outputTime time.Time func (t outputTime) MarshalJSON() ([]byte, error) { stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("2006-01-02 15:04:05.000")) return []byte(stamp), nil } func (s *Speedtest) JSON(servers Servers) ([]byte, error) { return json.Marshal( fullOutput{ Timestamp: outputTime(time.Now()), UserInfo: s.User, Servers: servers, }, ) } speedtest-go-1.7.10/speedtest/request.go000066400000000000000000000302531522770423200202310ustar00rootroot00000000000000package speedtest import ( "context" "errors" "fmt" "github.com/showwin/speedtest-go/speedtest/transport" "io" "math" "net/http" "net/url" "path" "strings" "sync/atomic" "time" ) type ( downloadFunc func(context.Context, *Server, int) error uploadFunc func(context.Context, *Server, int) error ) var ( dlSizes = [...]int{350, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000} ulSizes = [...]int{100, 300, 500, 800, 1000, 1500, 2500, 3000, 3500, 4000} // kB ) var ( ErrConnectTimeout = errors.New("server connect timeout") ) func (s *Server) MultiDownloadTestContext(ctx context.Context, servers Servers) error { ss := servers.Available() if ss.Len() == 0 { return errors.New("not found available servers") } mainIDIndex := 0 var td *TestDirection _context, cancel := context.WithCancel(ctx) defer cancel() var errorTimes int64 = 0 var requestTimes int64 = 0 for i, server := range *ss { if server.ID == s.ID { mainIDIndex = i } sp := server dbg.Printf("Register Download Handler: %s\n", sp.URL) td = server.Context.RegisterDownloadHandler(func() { atomic.AddInt64(&requestTimes, 1) if err := downloadRequest(_context, sp, 3); err != nil { atomic.AddInt64(&errorTimes, 1) } }) } if td == nil { return ErrorUninitializedManager } td.Start(cancel, mainIDIndex) // block here s.DLSpeed = ByteRate(td.manager.GetEWMADownloadRate()) if s.DLSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 { s.DLSpeed = -1 // N/A } return nil } func (s *Server) MultiUploadTestContext(ctx context.Context, servers Servers) error { ss := servers.Available() if ss.Len() == 0 { return errors.New("not found available servers") } mainIDIndex := 0 var td *TestDirection _context, cancel := context.WithCancel(ctx) defer cancel() var errorTimes int64 = 0 var requestTimes int64 = 0 for i, server := range *ss { if server.ID == s.ID { mainIDIndex = i } sp := server dbg.Printf("Register Upload Handler: %s\n", sp.URL) td = server.Context.RegisterUploadHandler(func() { atomic.AddInt64(&requestTimes, 1) if err := uploadRequest(_context, sp, 3); err != nil { atomic.AddInt64(&errorTimes, 1) } }) } if td == nil { return ErrorUninitializedManager } td.Start(cancel, mainIDIndex) // block here s.ULSpeed = ByteRate(td.manager.GetEWMAUploadRate()) if s.ULSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 { s.ULSpeed = -1 // N/A } return nil } // DownloadTest executes the test to measure download speed func (s *Server) DownloadTest() error { return s.downloadTestContext(context.Background(), downloadRequest) } // DownloadTestContext executes the test to measure download speed, observing the given context. func (s *Server) DownloadTestContext(ctx context.Context) error { return s.downloadTestContext(ctx, downloadRequest) } func (s *Server) downloadTestContext(ctx context.Context, downloadRequest downloadFunc) error { var errorTimes int64 = 0 var requestTimes int64 = 0 start := time.Now() _context, cancel := context.WithCancel(ctx) s.Context.RegisterDownloadHandler(func() { atomic.AddInt64(&requestTimes, 1) if err := downloadRequest(_context, s, 3); err != nil { atomic.AddInt64(&errorTimes, 1) } }).Start(cancel, 0) duration := time.Since(start) s.DLSpeed = ByteRate(s.Context.GetEWMADownloadRate()) if s.DLSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 { s.DLSpeed = -1 // N/A } s.TestDuration.Download = &duration s.testDurationTotalCount() return nil } // UploadTest executes the test to measure upload speed func (s *Server) UploadTest() error { return s.uploadTestContext(context.Background(), uploadRequest) } // UploadTestContext executes the test to measure upload speed, observing the given context. func (s *Server) UploadTestContext(ctx context.Context) error { return s.uploadTestContext(ctx, uploadRequest) } func (s *Server) uploadTestContext(ctx context.Context, uploadRequest uploadFunc) error { var errorTimes int64 = 0 var requestTimes int64 = 0 start := time.Now() _context, cancel := context.WithCancel(ctx) s.Context.RegisterUploadHandler(func() { atomic.AddInt64(&requestTimes, 1) if err := uploadRequest(_context, s, 4); err != nil { atomic.AddInt64(&errorTimes, 1) } }).Start(cancel, 0) duration := time.Since(start) s.ULSpeed = ByteRate(s.Context.GetEWMAUploadRate()) if s.ULSpeed == 0 && float64(errorTimes)/float64(requestTimes) > 0.1 { s.ULSpeed = -1 // N/A } s.TestDuration.Upload = &duration s.testDurationTotalCount() return nil } func downloadRequest(ctx context.Context, s *Server, w int) error { size := dlSizes[w] u, err := url.Parse(s.URL) if err != nil { return err } u.Path = path.Dir(u.Path) xdlURL := u.JoinPath(fmt.Sprintf("random%dx%d.jpg", size, size)).String() dbg.Printf("XdlURL: %s\n", xdlURL) req, err := http.NewRequestWithContext(ctx, http.MethodGet, xdlURL, nil) if err != nil { return err } resp, err := s.Context.doer.Do(req) if err != nil { return err } defer resp.Body.Close() return s.Context.NewChunk().DownloadHandler(resp.Body) } func uploadRequest(ctx context.Context, s *Server, w int) error { size := ulSizes[w] chunkSize := int64(size*100-51) * 10 dc := s.Context.NewChunk().UploadHandler(chunkSize) req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.URL, io.NopCloser(dc)) if err != nil { return err } req.ContentLength = chunkSize dbg.Printf("Len=%d, XulURL: %s\n", req.ContentLength, s.URL) req.Header.Set("Content-Type", "application/octet-stream") resp, err := s.Context.doer.Do(req) if err != nil { return err } _, _ = io.Copy(io.Discard, resp.Body) defer resp.Body.Close() return err } // PingTest executes test to measure latency func (s *Server) PingTest(callback func(latency time.Duration)) error { return s.PingTestContext(context.Background(), callback) } // PingTestContext executes test to measure latency, observing the given context. func (s *Server) PingTestContext(ctx context.Context, callback func(latency time.Duration)) (err error) { start := time.Now() var vectorPingResult []int64 if s.Context.config.PingMode == TCP { vectorPingResult, err = s.TCPPing(ctx, 10, time.Millisecond*200, callback) } else if s.Context.config.PingMode == ICMP { vectorPingResult, err = s.ICMPPing(ctx, time.Second*4, 10, time.Millisecond*200, callback) } else { vectorPingResult, err = s.HTTPPing(ctx, 10, time.Millisecond*200, callback) } if err != nil || len(vectorPingResult) == 0 { return err } dbg.Printf("Before StandardDeviation: %v\n", vectorPingResult) mean, _, std, minLatency, maxLatency := StandardDeviation(vectorPingResult) duration := time.Since(start) s.Latency = time.Duration(mean) * time.Nanosecond s.Jitter = time.Duration(std) * time.Nanosecond s.MinLatency = time.Duration(minLatency) * time.Nanosecond s.MaxLatency = time.Duration(maxLatency) * time.Nanosecond s.TestDuration.Ping = &duration s.testDurationTotalCount() return nil } // TestAll executes ping, download and upload tests one by one func (s *Server) TestAll() error { err := s.PingTest(nil) if err != nil { return err } err = s.DownloadTest() if err != nil { return err } return s.UploadTest() } func (s *Server) TCPPing( ctx context.Context, echoTimes int, echoFreq time.Duration, callback func(latency time.Duration), ) (latencies []int64, err error) { var pingDst string if len(s.Host) == 0 { u, err := url.Parse(s.URL) if err != nil || len(u.Host) == 0 { return nil, err } pingDst = u.Host } else { pingDst = s.Host } failTimes := 0 client, err := transport.NewClient(s.Context.tcpDialer) if err != nil { return nil, err } err = client.Connect(ctx, pingDst) if err != nil { return nil, err } for i := 0; i < echoTimes; i++ { latency, err := client.PingContext(ctx) if err != nil { failTimes++ continue } latencies = append(latencies, latency) if callback != nil { callback(time.Duration(latency)) } time.Sleep(echoFreq) } if failTimes == echoTimes { return nil, ErrConnectTimeout } return } func (s *Server) HTTPPing( ctx context.Context, echoTimes int, echoFreq time.Duration, callback func(latency time.Duration), ) (latencies []int64, err error) { var contextErr error u, err := url.Parse(s.URL) if err != nil || len(u.Host) == 0 { return nil, err } u.Path = path.Dir(u.Path) pingDst := u.JoinPath("latency.txt").String() dbg.Printf("Echo: %s\n", pingDst) failTimes := 0 req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingDst, nil) if err != nil { return nil, err } // carry out an extra request to warm up the connection and ensure the first request is not going to affect the // overall estimation echoTimes++ for i := 0; i < echoTimes; i++ { sTime := time.Now() resp, err := s.Context.doer.Do(req) endTime := time.Since(sTime) if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { contextErr = err break } failTimes++ continue } _, _ = io.Copy(io.Discard, resp.Body) _ = resp.Body.Close() if i > 0 { latency := endTime.Nanoseconds() latencies = append(latencies, latency) dbg.Printf("RTT: %d\n", latency) if callback != nil { callback(endTime) } } time.Sleep(echoFreq) } if contextErr != nil { return latencies, contextErr } if failTimes == echoTimes { return nil, ErrConnectTimeout } return } const PingTimeout = -1 const echoOptionDataSize = 32 // `echoMessage` need to change at same time // ICMPPing privileged method func (s *Server) ICMPPing( ctx context.Context, readTimeout time.Duration, echoTimes int, echoFreq time.Duration, callback func(latency time.Duration), ) (latencies []int64, err error) { u, err := url.ParseRequestURI(s.URL) if err != nil || len(u.Host) == 0 { return nil, err } dbg.Printf("Echo: %s\n", strings.Split(u.Host, ":")[0]) dialContext, err := s.Context.ipDialer.DialContext(ctx, "ip:icmp", strings.Split(u.Host, ":")[0]) if err != nil { return nil, err } defer dialContext.Close() ICMPData := make([]byte, 8+echoOptionDataSize) // header + data ICMPData[0] = 8 // echo ICMPData[1] = 0 // code ICMPData[2] = 0 // checksum ICMPData[3] = 0 // checksum ICMPData[4] = 0 // id ICMPData[5] = 1 // id ICMPData[6] = 0 // seq ICMPData[7] = 1 // seq var echoMessage = "Hi! SpeedTest-Go \\(●'◡'●)/" for i := 0; i < len(echoMessage); i++ { ICMPData[7+i] = echoMessage[i] } failTimes := 0 for i := 0; i < echoTimes; i++ { ICMPData[2] = byte(0) ICMPData[3] = byte(0) ICMPData[6] = byte(1 >> 8) ICMPData[7] = byte(1) ICMPData[8+echoOptionDataSize-1] = 6 cs := checkSum(ICMPData) ICMPData[2] = byte(cs >> 8) ICMPData[3] = byte(cs) sTime := time.Now() _ = dialContext.SetDeadline(sTime.Add(readTimeout)) _, err = dialContext.Write(ICMPData) if err != nil { failTimes += echoTimes - i break } buf := make([]byte, 20+echoOptionDataSize+8) _, err = dialContext.Read(buf) if err != nil || buf[20] != 0x00 { failTimes++ continue } endTime := time.Since(sTime) latencies = append(latencies, endTime.Nanoseconds()) dbg.Printf("1RTT: %s\n", endTime) if callback != nil { callback(endTime) } time.Sleep(echoFreq) } if failTimes == echoTimes { return nil, ErrConnectTimeout } return } func checkSum(data []byte) uint16 { var sum uint32 var length = len(data) var index int for length > 1 { sum += uint32(data[index])<<8 + uint32(data[index+1]) index += 2 length -= 2 } if length > 0 { sum += uint32(data[index]) } sum += sum >> 16 return uint16(^sum) } func StandardDeviation(vector []int64) (mean, variance, stdDev, min, max int64) { if len(vector) == 0 { return } var sumNum, accumulate int64 min = math.MaxInt64 max = math.MinInt64 for _, value := range vector { sumNum += value if min > value { min = value } if max < value { max = value } } mean = sumNum / int64(len(vector)) for _, value := range vector { accumulate += (value - mean) * (value - mean) } variance = accumulate / int64(len(vector)) stdDev = int64(math.Sqrt(float64(variance))) return } speedtest-go-1.7.10/speedtest/request_test.go000066400000000000000000000060331522770423200212670ustar00rootroot00000000000000package speedtest import ( "context" "fmt" "runtime" "testing" "time" ) func TestDownloadTestContext(t *testing.T) { idealSpeed := 0.1 * 8 * float64(runtime.NumCPU()) * 10 / 0.1 // one mockRequest per second with all CPU cores delta := 0.15 latency, _ := time.ParseDuration("5ms") server := Server{ URL: "https://dummy.com/upload.php", Latency: latency, Context: defaultClient, } server.Context.Manager.Reset() server.Context.SetRateCaptureFrequency(time.Millisecond) server.Context.SetCaptureTime(time.Second) err := server.downloadTestContext( context.Background(), mockRequest, ) if err != nil { t.Errorf(err.Error()) } value := server.Context.Manager.GetAvgDownloadRate() if value < idealSpeed*(1-delta) || idealSpeed*(1+delta) < value { t.Errorf("got unexpected server.DLSpeed '%v', expected between %v and %v", value, idealSpeed*(1-delta), idealSpeed*(1+delta)) } if server.TestDuration.Download == nil || *server.TestDuration.Download != *server.TestDuration.Total { t.Errorf("can't count test duration, server.TestDuration.Download=%v, server.TestDuration.Total=%v", server.TestDuration.Download, server.TestDuration.Total) } } func TestUploadTestContext(t *testing.T) { idealSpeed := 0.1 * 8 * float64(runtime.NumCPU()) * 10 / 0.1 // one mockRequest per second with all CPU cores delta := 0.15 // tolerance scope (-0.05, +0.05) latency, _ := time.ParseDuration("5ms") server := Server{ URL: "https://dummy.com/upload.php", Latency: latency, Context: defaultClient, } server.Context.Manager.Reset() server.Context.SetRateCaptureFrequency(time.Millisecond) server.Context.SetCaptureTime(time.Second) err := server.uploadTestContext( context.Background(), mockRequest, ) if err != nil { t.Errorf(err.Error()) } value := server.Context.Manager.GetAvgUploadRate() if value < idealSpeed*(1-delta) || idealSpeed*(1+delta) < value { t.Errorf("got unexpected server.ULSpeed '%v', expected between %v and %v", value, idealSpeed*(1-delta), idealSpeed*(1+delta)) } if server.TestDuration.Upload == nil || *server.TestDuration.Upload != *server.TestDuration.Total { t.Errorf("can't count test duration, server.TestDuration.Upload=%v, server.TestDuration.Total=%v", server.TestDuration.Upload, server.TestDuration.Total) } } func mockRequest(ctx context.Context, s *Server, w int) error { fmt.Sprintln(w) dc := s.Context.Manager.NewChunk() // (0.1MegaByte * 8bit * nConn * 10loop) / 0.1s = n*80Megabit // sleep has bad deviation on windows // ref https://github.com/golang/go/issues/44343 dc.GetParent().AddTotalDownload(1 * 1000 * 1000) dc.GetParent().AddTotalUpload(1 * 1000 * 1000) time.Sleep(time.Millisecond * 100) return nil } func TestPautaFilter(t *testing.T) { //vector := []float64{6, 6, 6, 6, 6, 6, 6, 6, 6, 6} vector0 := []int64{26, 23, 32} vector1 := []int64{3, 4, 5, 6, 6, 6, 1, 7, 9, 5, 200} _, _, std, _, _ := StandardDeviation(vector0) if std != 3 { t.Fail() } result := pautaFilter(vector1) if len(result) != 10 { t.Fail() } } speedtest-go-1.7.10/speedtest/server.go000066400000000000000000000275441522770423200200600ustar00rootroot00000000000000package speedtest import ( "context" "encoding/json" "encoding/xml" "errors" "fmt" "github.com/showwin/speedtest-go/speedtest/transport" "math" "net/http" "net/url" "sort" "strconv" "strings" "sync" "time" ) const ( speedTestServersUrl = "https://www.speedtest.net/api/js/servers" speedTestServersAlternativeUrl = "https://www.speedtest.net/speedtest-servers-static.php" speedTestServersAdvanced = "https://www.speedtest.net/api/ios-config.php" ) type payloadType int const ( typeJSONPayload payloadType = iota typeXMLPayload ) var ( ErrServerNotFound = errors.New("no server available or found") ) // Server information type Server struct { URL string `xml:"url,attr" json:"url"` Lat string `xml:"lat,attr" json:"lat"` Lon string `xml:"lon,attr" json:"lon"` Name string `xml:"name,attr" json:"name"` Country string `xml:"country,attr" json:"country"` Sponsor string `xml:"sponsor,attr" json:"sponsor"` ID string `xml:"id,attr" json:"id"` Host string `xml:"host,attr" json:"host"` Distance float64 `json:"distance"` Latency time.Duration `json:"latency"` MaxLatency time.Duration `json:"max_latency"` MinLatency time.Duration `json:"min_latency"` Jitter time.Duration `json:"jitter"` DLSpeed ByteRate `json:"dl_speed"` ULSpeed ByteRate `json:"ul_speed"` TestDuration TestDuration `json:"test_duration"` PacketLoss transport.PLoss `json:"packet_loss"` Context *Speedtest `json:"-"` } type TestDuration struct { Ping *time.Duration `json:"ping"` Download *time.Duration `json:"download"` Upload *time.Duration `json:"upload"` Total *time.Duration `json:"total"` } // CustomServer use defaultClient, given a URL string, return a new Server object, with as much // filled in as we can func CustomServer(host string) (*Server, error) { return defaultClient.CustomServer(host) } // CustomServer given a URL string, return a new Server object, with as much // filled in as we can func (s *Speedtest) CustomServer(host string) (*Server, error) { u, err := url.Parse(host) if err != nil { return nil, err } parseHost := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, "/speedtest/upload.php") return &Server{ ID: "Custom", Lat: "?", Lon: "?", Country: "?", URL: parseHost, Name: u.Host, Host: u.Host, Sponsor: "?", Context: s, }, nil } // ServerList list of Server // Users(Client) also exists with @param speedTestServersAdvanced type ServerList struct { Servers []*Server `xml:"servers>server"` Users []User `xml:"client"` } // Servers for sorting servers. type Servers []*Server // ByDistance for sorting servers. type ByDistance struct { Servers } func (servers Servers) Available() *Servers { retServer := Servers{} for _, server := range servers { if server.Latency != PingTimeout { retServer = append(retServer, server) } } for i := 0; i < len(retServer)-1; i++ { for j := 0; j < len(retServer)-i-1; j++ { if retServer[j].Latency > retServer[j+1].Latency { retServer[j], retServer[j+1] = retServer[j+1], retServer[j] } } } return &retServer } // Len finds length of servers. For sorting servers. func (servers Servers) Len() int { return len(servers) } // Swap swaps i-th and j-th. For sorting servers. func (servers Servers) Swap(i, j int) { servers[i], servers[j] = servers[j], servers[i] } // Hosts return hosts of servers func (servers Servers) Hosts() []string { var retServer []string for _, server := range servers { retServer = append(retServer, server.Host) } return retServer } // Less compares the distance. For sorting servers. func (b ByDistance) Less(i, j int) bool { return b.Servers[i].Distance < b.Servers[j].Distance } // FetchServerByID retrieves a server by given serverID. func (s *Speedtest) FetchServerByID(serverID string) (*Server, error) { return s.FetchServerByIDContext(context.Background(), serverID) } // FetchServerByID retrieves a server by given serverID. func FetchServerByID(serverID string) (*Server, error) { return defaultClient.FetchServerByID(serverID) } // FetchServerByIDContext retrieves a server by given serverID, observing the given context. func (s *Speedtest) FetchServerByIDContext(ctx context.Context, serverID string) (*Server, error) { u, err := url.Parse(speedTestServersAdvanced) if err != nil { return nil, err } query := u.Query() query.Set(strings.ToLower("serverID"), serverID) u.RawQuery = query.Encode() req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, err } resp, err := s.doer.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var list ServerList decoder := xml.NewDecoder(resp.Body) if err = decoder.Decode(&list); err != nil { return nil, err } for i := range list.Servers { if list.Servers[i].ID == serverID { list.Servers[i].Context = s if len(list.Users) > 0 { sLat, _ := strconv.ParseFloat(list.Servers[i].Lat, 64) sLon, _ := strconv.ParseFloat(list.Servers[i].Lon, 64) uLat, _ := strconv.ParseFloat(list.Users[0].Lat, 64) uLon, _ := strconv.ParseFloat(list.Users[0].Lon, 64) list.Servers[i].Distance = distance(sLat, sLon, uLat, uLon) } return list.Servers[i], err } } return nil, ErrServerNotFound } // FetchServers retrieves a list of available servers func (s *Speedtest) FetchServers() (Servers, error) { return s.FetchServerListContext(context.Background()) } // FetchServers retrieves a list of available servers func FetchServers() (Servers, error) { return defaultClient.FetchServers() } // FetchServerListContext retrieves a list of available servers, observing the given context. func (s *Speedtest) FetchServerListContext(ctx context.Context) (Servers, error) { u, err := url.Parse(speedTestServersUrl) if err != nil { return Servers{}, err } query := u.Query() if len(s.config.Keyword) > 0 { query.Set("search", s.config.Keyword) } if s.config.Location != nil { query.Set("lat", strconv.FormatFloat(s.config.Location.Lat, 'f', -1, 64)) query.Set("lon", strconv.FormatFloat(s.config.Location.Lon, 'f', -1, 64)) } u.RawQuery = query.Encode() dbg.Printf("Retrieving servers: %s\n", u.String()) req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return Servers{}, err } resp, err := s.doer.Do(req) if err != nil { return Servers{}, err } _payloadType := typeJSONPayload if resp.ContentLength == 0 { _ = resp.Body.Close() req, err = http.NewRequestWithContext(ctx, http.MethodGet, speedTestServersAlternativeUrl, nil) if err != nil { return Servers{}, err } resp, err = s.doer.Do(req) if err != nil { return Servers{}, err } _payloadType = typeXMLPayload } defer resp.Body.Close() var servers Servers switch _payloadType { case typeJSONPayload: // Decode xml decoder := json.NewDecoder(resp.Body) if err = decoder.Decode(&servers); err != nil { return servers, err } case typeXMLPayload: var list ServerList // Decode xml decoder := xml.NewDecoder(resp.Body) if err = decoder.Decode(&list); err != nil { return servers, err } servers = list.Servers default: return servers, errors.New("response payload decoding not implemented") } dbg.Printf("Servers Num: %d\n", len(servers)) // set doer of server for _, server := range servers { server.Context = s } // ping once var wg sync.WaitGroup pCtx, fc := context.WithTimeout(context.Background(), time.Second*4) dbg.Println("Echo each server...") for _, server := range servers { wg.Add(1) go func(gs *Server) { var latency []int64 var errPing error if s.config.PingMode == TCP { latency, errPing = gs.TCPPing(pCtx, 1, time.Millisecond, nil) } else if s.config.PingMode == ICMP { latency, errPing = gs.ICMPPing(pCtx, 4*time.Second, 1, time.Millisecond, nil) } else { latency, errPing = gs.HTTPPing(pCtx, 1, time.Millisecond, nil) } if errPing != nil || len(latency) < 1 { gs.Latency = PingTimeout } else { gs.Latency = time.Duration(latency[0]) * time.Nanosecond } wg.Done() }(server) } wg.Wait() fc() // Calculate distance // If we don't call FetchUserInfo() before FetchServers(), // we don't calculate the distance, instead we use the // remote computing distance provided by Ookla as default. if s.User != nil { for _, server := range servers { sLat, _ := strconv.ParseFloat(server.Lat, 64) sLon, _ := strconv.ParseFloat(server.Lon, 64) uLat, _ := strconv.ParseFloat(s.User.Lat, 64) uLon, _ := strconv.ParseFloat(s.User.Lon, 64) server.Distance = distance(sLat, sLon, uLat, uLon) } } // Sort by distance sort.Sort(ByDistance{servers}) if len(servers) <= 0 { return servers, ErrServerNotFound } return servers, nil } // FetchServerListContext retrieves a list of available servers, observing the given context. func FetchServerListContext(ctx context.Context) (Servers, error) { return defaultClient.FetchServerListContext(ctx) } func distance(lat1 float64, lon1 float64, lat2 float64, lon2 float64) float64 { radius := 6378.137 phi1 := lat1 * math.Pi / 180.0 phi2 := lat2 * math.Pi / 180.0 deltaPhiHalf := (lat1 - lat2) * math.Pi / 360.0 deltaLambdaHalf := (lon1 - lon2) * math.Pi / 360.0 sinePhiHalf2 := math.Sin(deltaPhiHalf)*math.Sin(deltaPhiHalf) + math.Cos(phi1)*math.Cos(phi2)*math.Sin(deltaLambdaHalf)*math.Sin(deltaLambdaHalf) // phi half-angle sine ^ 2 delta := 2 * math.Atan2(math.Sqrt(sinePhiHalf2), math.Sqrt(1-sinePhiHalf2)) // 2 arc sine return radius * delta // r * delta } // FindServer finds server by serverID in given server list. // If the id is not found in the given list, return the server with the lowest latency. func (servers Servers) FindServer(serverID []int) (Servers, error) { retServer := Servers{} if len(servers) <= 0 { return retServer, ErrServerNotFound } for _, sid := range serverID { for _, s := range servers { id, _ := strconv.Atoi(s.ID) if sid == id { retServer = append(retServer, s) break } } } if len(retServer) == 0 { // choose the lowest latency server var minLatency int64 = math.MaxInt64 var minServerIndex int for index, server := range servers { if server.Latency <= 0 { continue } if minLatency > server.Latency.Milliseconds() { minLatency = server.Latency.Milliseconds() minServerIndex = index } } retServer = append(retServer, servers[minServerIndex]) } return retServer, nil } // String representation of ServerList func (servers ServerList) String() string { slr := "" for _, server := range servers.Servers { slr += server.String() } return slr } // String representation of Servers func (servers Servers) String() string { slr := "" for _, server := range servers { slr += server.String() } return slr } // String representation of Server func (s *Server) String() string { if s.Sponsor == "?" { return fmt.Sprintf("[%4s] %s", s.ID, s.Name) } if len(s.Country) == 0 { return fmt.Sprintf("[%4s] %.2fkm %s by %s", s.ID, s.Distance, s.Name, s.Sponsor) } return fmt.Sprintf("[%4s] %.2fkm %s (%s) by %s", s.ID, s.Distance, s.Name, s.Country, s.Sponsor) } // CheckResultValid checks that results are logical given UL and DL speeds func (s *Server) CheckResultValid() bool { return !(s.DLSpeed*100 < s.ULSpeed) || !(s.DLSpeed > s.ULSpeed*100) } func (s *Server) testDurationTotalCount() { total := s.getNotNullValue(s.TestDuration.Ping) + s.getNotNullValue(s.TestDuration.Download) + s.getNotNullValue(s.TestDuration.Upload) s.TestDuration.Total = &total } func (s *Server) getNotNullValue(time *time.Duration) time.Duration { if time == nil { return 0 } return *time } speedtest-go-1.7.10/speedtest/server_test.go000066400000000000000000000110741522770423200211060ustar00rootroot00000000000000package speedtest import ( "errors" "math" "math/rand" "testing" "time" ) func TestFetchServerList(t *testing.T) { client := New() client.User = &User{ IP: "111.111.111.111", Lat: "35.22", Lon: "138.44", Isp: "Hello", } servers, err := client.FetchServers() if err != nil { t.Errorf(err.Error()) } if len(servers) == 0 { t.Errorf("Failed to fetch server list.") return } if len(servers[0].Country) == 0 { t.Errorf("got unexpected country name '%v'", servers[0].Country) } } func TestDistanceSame(t *testing.T) { for i := 0; i < 10000000; i++ { v1 := rand.Float64() * 90 v2 := rand.Float64() * 180 v3 := rand.Float64() * 90 v4 := rand.Float64() * 180 k1 := distance(v1, v2, v1, v2) k2 := distance(v1, v2, v3, v4) if math.IsNaN(k1) || math.IsNaN(k2) { t.Fatalf("NaN distance: %f, %f, %f, %f", v1, v2, v3, v4) } } testdata := [][]float64{ {32.0803, 34.7805, 32.0803, 34.7805}, {0, 0, 0, 0}, {1, 1, 1, 1}, {2, 2, 2, 2}, {-123.23, 123.33, -123.23, 123.33}, {90, 180, 90, 180}, } for i := range testdata { k := distance(testdata[i][0], testdata[i][1], testdata[i][2], testdata[i][3]) if math.IsNaN(k) { t.Fatalf("NaN distance: %f, %f, %f, %f", testdata[i][0], testdata[i][1], testdata[i][2], testdata[i][3]) } } } func TestDistance(t *testing.T) { d := distance(0.0, 0.0, 1.0, 1.0) if d < 157 || 158 < d { t.Errorf("got: %v, expected between 157 and 158", d) } d = distance(0.0, 180.0, 0.0, -180.0) if d < 0 && d > 1 { t.Errorf("got: %v, expected 0", d) } d1 := distance(100.0, 100.0, 100.0, 101.0) d2 := distance(100.0, 100.0, 100.0, 99.0) if d1 != d2 { t.Errorf("%v and %v should be same value", d1, d2) } d = distance(35.0, 140.0, -40.0, -140.0) if d < 11000 || 12000 < d { t.Errorf("got: %v, expected ~11694.5122", d) } } func TestFindServer(t *testing.T) { servers := Servers{ &Server{ ID: "1", }, &Server{ ID: "2", }, &Server{ ID: "3", }, } var serverID []int s, err := servers.FindServer(serverID) if err != nil { t.Errorf(err.Error()) } if len(s) != 1 { t.Errorf("unexpected server length. got: %v, expected: 1", len(s)) } if s[0].ID != "1" { t.Errorf("unexpected server ID. got: %v, expected: '1'", s[0].ID) } serverID = []int{2} s, err = servers.FindServer(serverID) if err != nil { t.Errorf(err.Error()) } if len(s) != 1 { t.Errorf("unexpected server length. got: %v, expected: 1", len(s)) } if s[0].ID != "2" { t.Errorf("unexpected server ID. got: %v, expected: '2'", s[0].ID) } serverID = []int{3, 1} s, err = servers.FindServer(serverID) if err != nil { t.Errorf(err.Error()) } if len(s) != 2 { t.Errorf("unexpected server length. got: %v, expected: 2", len(s)) } if s[0].ID != "3" { t.Errorf("unexpected server ID. got: %v, expected: '3'", s[0].ID) } if s[1].ID != "1" { t.Errorf("unexpected server ID. got: %v, expected: '1'", s[0].ID) } } func TestCustomServer(t *testing.T) { // Good server got, err := CustomServer("https://example.com/upload.php") if err != nil { t.Errorf(err.Error()) } if got == nil { t.Error("empty server") return } if got.Host != "example.com" { t.Error("did not properly set the Host field on a custom server") } } func TestFetchServerByID(t *testing.T) { remoteList, err := FetchServers() if err != nil { t.Fatal(err) } if remoteList.Len() < 1 { t.Fatal(errors.New("server not found")) } testData := map[string]bool{ remoteList[0].ID: true, "-99999999": false, "どうも": false, "hello": false, "你好": false, } for id, b := range testData { server, err := FetchServerByID(id) if err != nil && b { t.Error(err) } if server != nil && (server.ID == id) != b { t.Errorf("id %s == %s is not %v", id, server.ID, b) } } } func TestTotalDurationCount(t *testing.T) { server, _ := CustomServer("https://example.com/upload.php") uploadTime := time.Duration(10000805542) server.TestDuration.Upload = &uploadTime server.testDurationTotalCount() if server.TestDuration.Total.Nanoseconds() != 10000805542 { t.Error("addition in testDurationTotalCount didn't work") } downloadTime := time.Duration(10000403875) server.TestDuration.Download = &downloadTime server.testDurationTotalCount() if server.TestDuration.Total.Nanoseconds() != 20001209417 { t.Error("addition in testDurationTotalCount didn't work") } pingTime := time.Duration(2183156458) server.TestDuration.Ping = &pingTime server.testDurationTotalCount() if server.TestDuration.Total.Nanoseconds() != 22184365875 { t.Error("addition in testDurationTotalCount didn't work") } } speedtest-go-1.7.10/speedtest/speedtest.go000066400000000000000000000124301522770423200205360ustar00rootroot00000000000000package speedtest import ( "context" "fmt" "net" "net/http" "net/url" "runtime" "strings" "syscall" "time" ) var ( version = "1.7.10" DefaultUserAgent = fmt.Sprintf("showwin/speedtest-go %s", version) ) type Proto int const ( HTTP Proto = iota TCP ICMP ) // Speedtest is a speedtest client. type Speedtest struct { User *User Manager doer *http.Client config *UserConfig tcpDialer *net.Dialer ipDialer *net.Dialer } type UserConfig struct { T *http.Transport UserAgent string Proxy string Source string DnsBindSource bool DialerControl func(network, address string, c syscall.RawConn) error Debug bool PingMode Proto SavingMode bool MaxConnections int CityFlag string LocationFlag string Location *Location Keyword string // Fuzzy search } func parseAddr(addr string) (string, string) { prefixIndex := strings.Index(addr, "://") if prefixIndex != -1 { return addr[:prefixIndex], addr[prefixIndex+3:] } return "", addr // ignore address network prefix } func (s *Speedtest) NewUserConfig(uc *UserConfig) { if uc.Debug { dbg.Enable() } if uc.SavingMode { uc.MaxConnections = 1 // Set the number of concurrent connections to 1 } s.SetNThread(uc.MaxConnections) if len(uc.CityFlag) > 0 { var err error uc.Location, err = GetLocation(uc.CityFlag) if err != nil { dbg.Printf("Warning: skipping command line arguments: --city. err: %v\n", err.Error()) } } if len(uc.LocationFlag) > 0 { var err error uc.Location, err = ParseLocation(uc.CityFlag, uc.LocationFlag) if err != nil { dbg.Printf("Warning: skipping command line arguments: --location. err: %v\n", err.Error()) } } var tcpSource net.Addr // If nil, a local address is automatically chosen. var icmpSource net.Addr var proxy = http.ProxyFromEnvironment s.config = uc if len(s.config.UserAgent) == 0 { s.config.UserAgent = DefaultUserAgent } if len(uc.Source) > 0 { _, address := parseAddr(uc.Source) addr0, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("[%s]:0", address)) // dynamic tcp port if err == nil { tcpSource = addr0 } else { dbg.Printf("Warning: skipping parse the source address. err: %s\n", err.Error()) } addr1, err := net.ResolveIPAddr("ip", address) // dynamic tcp port if err == nil { icmpSource = addr1 } else { dbg.Printf("Warning: skipping parse the source address. err: %s\n", err.Error()) } if uc.DnsBindSource { net.DefaultResolver.Dial = func(ctx context.Context, network, dnsServer string) (net.Conn, error) { dialer := &net.Dialer{ Timeout: 5 * time.Second, LocalAddr: func(network string) net.Addr { switch network { case "udp", "udp4", "udp6": return &net.UDPAddr{IP: net.ParseIP(address)} case "tcp", "tcp4", "tcp6": return &net.TCPAddr{IP: net.ParseIP(address)} default: return nil } }(network), } return dialer.DialContext(ctx, network, dnsServer) } } } if len(uc.Proxy) > 0 { if parse, err := url.Parse(uc.Proxy); err != nil { dbg.Printf("Warning: skipping parse the proxy host. err: %s\n", err.Error()) } else { proxy = func(_ *http.Request) (*url.URL, error) { return parse, err } } } s.tcpDialer = &net.Dialer{ LocalAddr: tcpSource, Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, Control: uc.DialerControl, } s.ipDialer = &net.Dialer{ LocalAddr: icmpSource, Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, Control: uc.DialerControl, } s.config.T = &http.Transport{ Proxy: proxy, DialContext: s.tcpDialer.DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } s.doer.Transport = s } func (s *Speedtest) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Add("User-Agent", s.config.UserAgent) return s.config.T.RoundTrip(req) } // Option is a function that can be passed to New to modify the Client. type Option func(*Speedtest) // WithDoer sets the http.Client used to make requests. func WithDoer(doer *http.Client) Option { return func(s *Speedtest) { s.doer = doer } } // WithUserConfig adds a custom user config for speedtest. // This configuration may be overwritten again by WithDoer, // because client and transport are parent-child relationship: // `New(WithDoer(myDoer), WithUserAgent(myUserAgent), WithDoer(myDoer))` func WithUserConfig(userConfig *UserConfig) Option { return func(s *Speedtest) { s.NewUserConfig(userConfig) dbg.Printf("Source: %s\n", s.config.Source) dbg.Printf("Proxy: %s\n", s.config.Proxy) dbg.Printf("SavingMode: %v\n", s.config.SavingMode) dbg.Printf("Keyword: %v\n", s.config.Keyword) dbg.Printf("PingType: %v\n", s.config.PingMode) dbg.Printf("OS: %s, ARCH: %s, NumCPU: %d\n", runtime.GOOS, runtime.GOARCH, runtime.NumCPU()) } } // New creates a new speedtest client. func New(opts ...Option) *Speedtest { s := &Speedtest{ doer: http.DefaultClient, Manager: NewDataManager(), } // load default config s.NewUserConfig(&UserConfig{UserAgent: DefaultUserAgent}) for _, opt := range opts { opt(s) } return s } func Version() string { return version } var defaultClient = New() speedtest-go-1.7.10/speedtest/speedtest_test.go000066400000000000000000000035561522770423200216060ustar00rootroot00000000000000package speedtest import ( "net/http" "net/http/httptest" "testing" ) func BenchmarkLogSpeed(b *testing.B) { s := New() config := &UserConfig{ UserAgent: DefaultUserAgent, Debug: false, } WithUserConfig(config)(s) for i := 0; i < b.N; i++ { dbg.Printf("hello %s\n", "s20080123") // ~1ns/op } } func TestNew(t *testing.T) { t.Run("DefaultDoer", func(t *testing.T) { c := New() if c.doer == nil { t.Error("doer is nil by") } }) t.Run("CustomDoer", func(t *testing.T) { doer := &http.Client{} c := New(WithDoer(doer)) if c.doer != doer { t.Error("doer is not the same") } }) } func TestUserAgent(t *testing.T) { testServer := func(expectedUserAgent string) *httptest.Server { return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.UserAgent() == "" { t.Error("did not receive User-Agent header") } else if r.UserAgent() != expectedUserAgent { t.Errorf("incorrect User-Agent header: %s, expected: %s", r.UserAgent(), expectedUserAgent) } })) } t.Run("DefaultUserAgent", func(t *testing.T) { c := New(WithUserConfig(&UserConfig{UserAgent: DefaultUserAgent})) s := testServer(DefaultUserAgent) _, err := c.doer.Get(s.URL) if err != nil { t.Errorf(err.Error()) } }) t.Run("CustomUserAgent", func(t *testing.T) { testAgent := "1234" s := testServer(testAgent) c := New(WithUserConfig(&UserConfig{UserAgent: testAgent})) _, err := c.doer.Get(s.URL) if err != nil { t.Errorf(err.Error()) } }) // Test that With t.Run("CustomUserAgentAndDoer", func(t *testing.T) { testAgent := "4321" doer := &http.Client{} s := testServer(testAgent) c := New(WithDoer(doer), WithUserConfig(&UserConfig{UserAgent: testAgent})) if c.doer != doer { t.Error("doer is not the same") } _, err := c.doer.Get(s.URL) if err != nil { t.Errorf(err.Error()) } }) } speedtest-go-1.7.10/speedtest/transport/000077500000000000000000000000001522770423200202435ustar00rootroot00000000000000speedtest-go-1.7.10/speedtest/transport/tcp.go000066400000000000000000000134641522770423200213700ustar00rootroot00000000000000package transport import ( "bufio" "bytes" "context" "errors" "fmt" "net" "strconv" "time" ) var ( pingPrefix = []byte{0x50, 0x49, 0x4e, 0x47, 0x20} // downloadPrefix = []byte{0x44, 0x4F, 0x57, 0x4E, 0x4C, 0x4F, 0x41, 0x44, 0x20} // uploadPrefix = []byte{0x55, 0x50, 0x4C, 0x4F, 0x41, 0x44, 0x20} initPacket = []byte{0x49, 0x4e, 0x49, 0x54, 0x50, 0x4c, 0x4f, 0x53, 0x53} packetLoss = []byte{0x50, 0x4c, 0x4f, 0x53, 0x53} hiFormat = []byte{0x48, 0x49} quitFormat = []byte{0x51, 0x55, 0x49, 0x54} ) var ( ErrEchoData = errors.New("incorrect echo data") ErrEmptyConn = errors.New("empty conn") ErrUnsupported = errors.New("unsupported protocol") // Some servers have disabled ip:8080, we return this error. ErrUninitializedPacketLossInst = errors.New("uninitialized packet loss inst") ) func pingFormat(locTime int64) []byte { return strconv.AppendInt(pingPrefix, locTime, 10) } type Client struct { id string conn net.Conn host string version string dialer *net.Dialer reader *bufio.Reader } func NewClient(dialer *net.Dialer) (*Client, error) { uuid, err := generateUUID() if err != nil { return nil, err } return &Client{ id: uuid, dialer: dialer, }, nil } func (client *Client) ID() string { return client.id } func (client *Client) Connect(ctx context.Context, host string) (err error) { client.host = host client.conn, err = client.dialer.DialContext(ctx, "tcp", client.host) if err != nil { return err } client.reader = bufio.NewReader(client.conn) return nil } func (client *Client) Disconnect() (err error) { _, _ = client.conn.Write(quitFormat) client.conn = nil client.reader = nil client.version = "" return } func (client *Client) Write(data []byte) (err error) { if client.conn == nil { return ErrEmptyConn } _, err = fmt.Fprintf(client.conn, "%s\n", data) return } func (client *Client) Read() ([]byte, error) { if client.conn == nil { return nil, ErrEmptyConn } return client.reader.ReadBytes('\n') } func (client *Client) Version() string { if len(client.version) == 0 { err := client.Write(hiFormat) if err == nil { message, err := client.Read() if err != nil || len(message) < 8 { return "unknown" } client.version = string(message[6 : len(message)-1]) } } return client.version } // PingContext Measure latency(RTT) between client and server. // We use the 2RTT method to obtain three RTT result in // order to get more data in less time (t2-t0, t4-t2, t3-t1). // And give lower weight to the delay measured by the server. // local factor = 0.4 * 2 and remote factor = 0.2 // latency = 0.4 * (t2 - t0) + 0.4 * (t4 - t2) + 0.2 * (t3 - t1) // @return cumulative delay in nanoseconds func (client *Client) PingContext(ctx context.Context) (int64, error) { resultChan := make(chan error, 1) var accumulatedLatency int64 = 0 var firstReceivedByServer int64 // t1 go func() { for i := 0; i < 2; i++ { t0 := time.Now().UnixNano() if err := client.Write(pingFormat(t0)); err != nil { resultChan <- err return } data, err := client.Read() t2 := time.Now().UnixNano() if err != nil { resultChan <- err return } if len(data) != 19 { resultChan <- ErrEchoData return } tx, err := strconv.ParseInt(string(data[5:18]), 10, 64) if err != nil { resultChan <- err return } accumulatedLatency += (t2 - t0) * 4 / 10 // 0.4 if i == 0 { firstReceivedByServer = tx } else { // append server-side latency result accumulatedLatency += (tx - firstReceivedByServer) * 1000 * 1000 * 2 / 10 // 0.2 } } resultChan <- nil close(resultChan) }() select { case err := <-resultChan: return accumulatedLatency, err case <-ctx.Done(): return 0, ctx.Err() } } func (client *Client) InitPacketLoss() error { id := client.id payload := append(hiFormat, 0x20) payload = append(payload, []byte(id)...) err := client.Write(payload) if err != nil { return err } return client.Write(initPacket) } // PLoss Packet loss statistics // The packet loss here generally refers to uplink packet loss. // We use the following formula to calculate the packet loss: // packetLoss = [1 - (Sent - Dup) / (Max + 1)] * 100% type PLoss struct { Sent int `json:"sent"` // Number of sent packets acknowledged by the remote. Dup int `json:"dup"` // Number of duplicate packets acknowledged by the remote. Max int `json:"max"` // The maximum index value received by the remote. } func (p PLoss) String() string { if p.Sent == 0 { // if p.Sent == 0, maybe all data is dropped by the upper gateway. // we believe this feature is not applicable on this server now. return "Packet Loss: N/A" } return fmt.Sprintf("Packet Loss: %.2f%% (Sent: %d/Dup: %d/Max: %d)", p.Loss()*100, p.Sent, p.Dup, p.Max) } func (p PLoss) Loss() float64 { if p.Sent == 0 { return -1 } return 1 - (float64(p.Sent-p.Dup))/float64(p.Max+1) } func (p PLoss) LossPercent() float64 { if p.Sent == 0 { return -1 } return p.Loss() * 100 } func (client *Client) PacketLoss() (*PLoss, error) { err := client.Write(packetLoss) if err != nil { return nil, err } result, err := client.Read() if err != nil { return nil, err } splitResult := bytes.Split(result, []byte{0x20}) if len(splitResult) < 3 || !bytes.Equal(splitResult[0], packetLoss) { return nil, nil } x0, err := strconv.Atoi(string(splitResult[1])) if err != nil { return nil, err } x1, err := strconv.Atoi(string(splitResult[2])) if err != nil { return nil, err } x2, err := strconv.Atoi(string(bytes.TrimRight(splitResult[3], "\n"))) if err != nil { return nil, err } return &PLoss{ Sent: x0, Dup: x1, Max: x2, }, nil } func (client *Client) Download() { panic("Unimplemented method: Client.Download()") } func (client *Client) Upload() { panic("Unimplemented method: Client.Upload()") } speedtest-go-1.7.10/speedtest/transport/udp.go000066400000000000000000000032251522770423200213640ustar00rootroot00000000000000package transport import ( "bytes" "context" "crypto/rand" "fmt" mrand "math/rand" "net" "strconv" "strings" "time" ) var ( loss = []byte{0x4c, 0x4f, 0x53, 0x53} ) type PacketLossSender struct { ID string // UUID nounce int64 // Random int (maybe) [0,10000000000) withTimestamp bool // With timestamp (ten seconds level) conn net.Conn // UDP Conn raw []byte host string dialer *net.Dialer } func NewPacketLossSender(uuid string, dialer *net.Dialer) (*PacketLossSender, error) { rd := mrand.New(mrand.NewSource(time.Now().UnixNano())) nounce := rd.Int63n(10000000000) p := &PacketLossSender{ ID: strings.ToUpper(uuid), nounce: nounce, withTimestamp: false, // we close it as default, we won't be able to use it right now. dialer: dialer, } p.raw = []byte(fmt.Sprintf("%s %d %s %s", loss, nounce, "#", uuid)) return p, nil } func (ps *PacketLossSender) Connect(ctx context.Context, host string) (err error) { ps.host = host ps.conn, err = ps.dialer.DialContext(ctx, "udp", ps.host) return err } // Send // @param order the value will be sent func (ps *PacketLossSender) Send(order int) error { payload := bytes.Replace(ps.raw, []byte{0x23}, []byte(strconv.Itoa(order)), 1) _, err := ps.conn.Write(payload) return err } func generateUUID() (string, error) { randUUID := make([]byte, 16) _, err := rand.Read(randUUID) if err != nil { return "", err } randUUID[8] = randUUID[8]&^0xc0 | 0x80 randUUID[6] = randUUID[6]&^0xf0 | 0x40 return fmt.Sprintf("%x-%x-%x-%x-%x", randUUID[0:4], randUUID[4:6], randUUID[6:8], randUUID[8:10], randUUID[10:]), nil } speedtest-go-1.7.10/speedtest/unit.go000066400000000000000000000047101522770423200175170ustar00rootroot00000000000000package speedtest import ( "strconv" ) type UnitType int // IEC and SI const ( UnitTypeDecimalBits = UnitType(iota) // auto scaled UnitTypeDecimalBytes // auto scaled UnitTypeBinaryBits // auto scaled UnitTypeBinaryBytes // auto scaled UnitTypeDefaultMbps // fixed ) var ( DecimalBitsUnits = []string{"bps", "Kbps", "Mbps", "Gbps"} DecimalBytesUnits = []string{"B/s", "KB/s", "MB/s", "GB/s"} BinaryBitsUnits = []string{"Kibps", "KiMbps", "KiGbps"} BinaryBytesUnits = []string{"KiB/s", "MiB/s", "GiB/s"} ) var unitMaps = map[UnitType][]string{ UnitTypeDecimalBits: DecimalBitsUnits, UnitTypeDecimalBytes: DecimalBytesUnits, UnitTypeBinaryBits: BinaryBitsUnits, UnitTypeBinaryBytes: BinaryBytesUnits, } const ( B = 1.0 KB = 1000 * B MB = 1000 * KB GB = 1000 * MB IB = 1 KiB = 1024 * IB MiB = 1024 * KiB GiB = 1024 * MiB ) type ByteRate float64 var globalByteRateUnit UnitType func (r ByteRate) String() string { if r == 0 { return "0.00 Mbps" } if r == -1 { return "N/A" } if globalByteRateUnit != UnitTypeDefaultMbps { return r.Byte(globalByteRateUnit) } return strconv.FormatFloat(float64(r/125000.0), 'f', 2, 64) + " Mbps" } // SetUnit Set global output units func SetUnit(unit UnitType) { globalByteRateUnit = unit } func (r ByteRate) Mbps() float64 { return float64(r) / 125000.0 } func (r ByteRate) Gbps() float64 { return float64(r) / 125000000.0 } // Byte Specifies the format output byte rate func (r ByteRate) Byte(formatType UnitType) string { if r == 0 { return "0.00 Mbps" } if r == -1 { return "N/A" } return format(float64(r), formatType) } func format(byteRate float64, i UnitType) string { val := byteRate if i%2 == 0 { val *= 8 } if i < UnitTypeBinaryBits { switch { case byteRate >= GB: return strconv.FormatFloat(val/GB, 'f', 2, 64) + " " + unitMaps[i][3] case byteRate >= MB: return strconv.FormatFloat(val/MB, 'f', 2, 64) + " " + unitMaps[i][2] case byteRate >= KB: return strconv.FormatFloat(val/KB, 'f', 2, 64) + " " + unitMaps[i][1] default: return strconv.FormatFloat(val/B, 'f', 2, 64) + " " + unitMaps[i][0] } } switch { case byteRate >= GiB: return strconv.FormatFloat(val/GiB, 'f', 2, 64) + " " + unitMaps[i][2] case byteRate >= MiB: return strconv.FormatFloat(val/MiB, 'f', 2, 64) + " " + unitMaps[i][1] default: return strconv.FormatFloat(val/KiB, 'f', 2, 64) + " " + unitMaps[i][0] } } speedtest-go-1.7.10/speedtest/unit_test.go000066400000000000000000000030721522770423200205560ustar00rootroot00000000000000package speedtest import ( "testing" ) func BenchmarkFmt(b *testing.B) { bt := ByteRate(1002031.0) for i := 0; i < b.N; i++ { _ = bt.Byte(UnitTypeDecimalBits) } } func BenchmarkDefaultFmt(b *testing.B) { bt := ByteRate(1002031.0) for i := 0; i < b.N; i++ { _ = bt.String() } } func TestFmt(t *testing.T) { testData := []struct { rate ByteRate format string t UnitType }{ {123123123.123, "984.98 Mbps", UnitTypeDecimalBits}, {1231231231.123, "9.85 Gbps", UnitTypeDecimalBits}, {123123.123, "984.98 Kbps", UnitTypeDecimalBits}, {123.1, "984.80 bps", UnitTypeDecimalBits}, {123123123.123, "123.12 MB/s", UnitTypeDecimalBytes}, {1231231231.123, "1.23 GB/s", UnitTypeDecimalBytes}, {123123.123, "123.12 KB/s", UnitTypeDecimalBytes}, {123.1, "123.10 B/s", UnitTypeDecimalBytes}, {123123123.123, "939.35 KiMbps", UnitTypeBinaryBits}, {1231231231.123, "9.17 KiGbps", UnitTypeBinaryBits}, {123123.123, "961.90 Kibps", UnitTypeBinaryBits}, {123.1, "0.96 Kibps", UnitTypeBinaryBits}, {123123123.123, "117.42 MiB/s", UnitTypeBinaryBytes}, {1231231231.123, "1.15 GiB/s", UnitTypeBinaryBytes}, {123123.123, "120.24 KiB/s", UnitTypeBinaryBytes}, {123.1, "0.12 KiB/s", UnitTypeBinaryBytes}, {-1, "N/A", UnitTypeBinaryBytes}, {0, "0.00 Mbps", UnitTypeDecimalBits}, } if testData[0].rate.String() != testData[0].format { t.Errorf("got: %s, want: %s", testData[0].rate.String(), testData[0].format) } for _, v := range testData { if got := v.rate.Byte(v.t); got != v.format { t.Errorf("got: %s, want: %s", got, v.format) } } } speedtest-go-1.7.10/speedtest/user.go000066400000000000000000000036321522770423200175200ustar00rootroot00000000000000package speedtest import ( "context" "encoding/xml" "errors" "fmt" "net/http" ) const speedTestConfigUrl = "https://www.speedtest.net/speedtest-config.php" // User represents information determined about the caller by speedtest.net type User struct { IP string `xml:"ip,attr"` Lat string `xml:"lat,attr"` Lon string `xml:"lon,attr"` Isp string `xml:"isp,attr"` } // Users for decode xml type Users struct { Users []User `xml:"client"` } // FetchUserInfo returns information about caller determined by speedtest.net func (s *Speedtest) FetchUserInfo() (*User, error) { return s.FetchUserInfoContext(context.Background()) } // FetchUserInfo returns information about caller determined by speedtest.net func FetchUserInfo() (*User, error) { return defaultClient.FetchUserInfo() } // FetchUserInfoContext returns information about caller determined by speedtest.net, observing the given context. func (s *Speedtest) FetchUserInfoContext(ctx context.Context) (*User, error) { dbg.Printf("Retrieving user info: %s\n", speedTestConfigUrl) req, err := http.NewRequestWithContext(ctx, http.MethodGet, speedTestConfigUrl, nil) if err != nil { return nil, err } resp, err := s.doer.Do(req) if err != nil { return nil, err } defer resp.Body.Close() // Decode xml decoder := xml.NewDecoder(resp.Body) var users Users if err = decoder.Decode(&users); err != nil { return nil, err } if len(users.Users) == 0 { return nil, errors.New("failed to fetch user information") } s.User = &users.Users[0] return s.User, nil } // FetchUserInfoContext returns information about caller determined by speedtest.net, observing the given context. func FetchUserInfoContext(ctx context.Context) (*User, error) { return defaultClient.FetchUserInfoContext(ctx) } // String representation of User func (u *User) String() string { extInfo := "" return fmt.Sprintf("%s (%s) [%s, %s] %s", u.IP, u.Isp, u.Lat, u.Lon, extInfo) } speedtest-go-1.7.10/speedtest/user_test.go000066400000000000000000000017061522770423200205570ustar00rootroot00000000000000package speedtest import ( "strconv" "strings" "testing" ) func TestFetchUserInfo(t *testing.T) { client := New() user, err := client.FetchUserInfo() if err != nil { t.Errorf(err.Error()) } if user == nil { t.Error("empty user info") return } // IP if len(user.IP) < 7 || len(user.IP) > 15 { t.Errorf("invalid IP length. got: %v;", user.IP) } if strings.Count(user.IP, ".") != 3 { t.Errorf("invalid IP format. got: %v", user.IP) } // Lat lat, err := strconv.ParseFloat(user.Lat, 64) if err != nil { t.Errorf(err.Error()) } if lat < -90 || 90 < lat { t.Errorf("invalid Latitude. got: %v, expected between -90 and 90", user.Lat) } // Lon lon, err := strconv.ParseFloat(user.Lon, 64) if err != nil { t.Errorf(err.Error()) } if lon < -180 || 180 < lon { t.Errorf("invalid Longitude. got: %v, expected between -180 and 180", user.Lon) } // Isp if len(user.Isp) == 0 { t.Errorf("invalid Iso. got: %v;", user.Isp) } } speedtest-go-1.7.10/task.go000066400000000000000000000052301522770423200155000ustar00rootroot00000000000000package main import ( "fmt" "github.com/chelnak/ysmrr" "os" "strings" ) type TaskManager struct { sm ysmrr.SpinnerManager isOut bool noProgress bool } type Task struct { spinner *ysmrr.Spinner manager *TaskManager title string } func InitTaskManager(jsonOutput, unixOutput bool) *TaskManager { isOut := !jsonOutput || unixOutput tm := &TaskManager{sm: ysmrr.NewSpinnerManager(), isOut: isOut, noProgress: unixOutput} if isOut && !unixOutput { tm.sm.Start() } return tm } func (tm *TaskManager) Reset() { if tm.isOut && !tm.noProgress { tm.sm.Stop() tm.sm = ysmrr.NewSpinnerManager() tm.sm.Start() } } func (tm *TaskManager) Stop() { if tm.isOut && !tm.noProgress { tm.sm.Stop() } } func (tm *TaskManager) Println(message string) { if tm.noProgress { fmt.Println(message) return } if tm.isOut { context := &Task{manager: tm} context.spinner = tm.sm.AddSpinner(message) context.Complete() } } func (tm *TaskManager) RunWithTrigger(enable bool, title string, callback func(task *Task)) { if enable { tm.Run(title, callback) } } func (tm *TaskManager) Run(title string, callback func(task *Task)) { context := &Task{manager: tm, title: title} if tm.isOut { if tm.noProgress { //fmt.Println(title) } else { context.spinner = tm.sm.AddSpinner(title) } } callback(context) } func (tm *TaskManager) AsyncRun(title string, callback func(task *Task)) { context := &Task{manager: tm, title: title} if tm.isOut { if tm.noProgress { //fmt.Println(title) } else { context.spinner = tm.sm.AddSpinner(title) } } go callback(context) } func (t *Task) Complete() { if t.manager.noProgress { return } if t.spinner == nil { return } t.spinner.Complete() } func (t *Task) Updatef(format string, a ...interface{}) { if t.spinner == nil || t.manager.noProgress { return } t.spinner.UpdateMessagef(format, a...) } func (t *Task) Update(format string) { if t.spinner == nil || t.manager.noProgress { return } t.spinner.UpdateMessage(format) } func (t *Task) Println(message string) { if t.manager.noProgress { fmt.Println(message) return } if t.spinner == nil { return } t.spinner.UpdateMessage(message) } func (t *Task) Printf(format string, a ...interface{}) { if t.manager.noProgress { fmt.Printf(format+"\n", a...) return } if t.spinner == nil { return } t.spinner.UpdateMessagef(format, a...) } func (t *Task) CheckError(err error) { if err != nil { if t.spinner != nil { t.Printf("Fatal: %s, err: %v", strings.ToLower(t.title), err) t.spinner.Error() t.manager.Stop() } else { fmt.Printf("Fatal: %s, err: %v", strings.ToLower(t.title), err) } os.Exit(0) } }