/
fast.go
173 lines (150 loc) · 4.53 KB
/
fast.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package providers
import (
"errors"
"fmt"
"github.com/gesquive/fast-cli/fast"
"golang.org/x/sync/errgroup"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
// This implementation relied on what is described in the following article.
// https://netflixtechblog.com/building-fast-com-4857fe0f8adb
// However, this implementation isn't using the 0-2048 chunk due to lack
// of information about how to use it during calculations
const FastDefaultTargetAmount uint = 5
const FastDefaultFileSizeInMBytes uint = 25
// FastProvider is being used for fast.com based speedtest.
type FastProvider struct {
FileSizeInMBytes uint
TargetAmount uint
targets []string
initialized bool
}
// Name returns human-readable name
func (f *FastProvider) Name() string {
return "fast"
}
// Init verifies initial config and receives test URL's for fast.com provider.
// In case of unexpected failures here the most probable reason is that netflix
// changed the token/layout format, because API token is being extracted from the
// website files
func (f *FastProvider) Init() error {
if f.TargetAmount <= 0 {
f.TargetAmount = FastDefaultTargetAmount
}
if f.FileSizeInMBytes <= 0 {
f.FileSizeInMBytes = FastDefaultFileSizeInMBytes
}
f.targets = fast.GetDlUrls(uint64(f.TargetAmount))
if uint(len(f.targets)) != f.TargetAmount {
return errors.New("can't fetch any targets")
}
f.initialized = true
return nil
}
// DownloadTest performs download speedtest.
func (f *FastProvider) DownloadTest() (bits uint64, err error) {
if !f.initialized {
return 0, errors.New("provider was not initialized")
}
return f.runCompositeSpeedTest("download")
}
// UploadTest performs upload speedtest.
func (f *FastProvider) UploadTest() (bits uint64, err error) {
if !f.initialized {
return 0, errors.New("provider was not initialized")
}
return f.runCompositeSpeedTest("upload")
}
// CompleteTest performs both download and upload speedtest.
func (f *FastProvider) CompleteTest() (dBits uint64, uBits uint64, err error) {
r, err := f.DownloadTest()
if err != nil {
return 0, 0, err
}
u, err := f.UploadTest()
if err != nil {
return 0, 0, err
}
return r, u, nil
}
// runCompositeSpeedTest starting a fast.com methodology-based test.
// For more info check the article. https://netflixtechblog.com/building-fast-com-4857fe0f8adb
func (f *FastProvider) runCompositeSpeedTest(mode string) (uint64, error) {
rampResult, err := f.runSpeedTest(mode, 1)
if err != nil {
return 0, err
}
speedResult, err := f.runSpeedTest(mode, 25*1024*1024)
if err != nil {
return 0, err
}
return f.calculateSpeed(speedResult - rampResult), nil
}
// runSpeedTest starting an upload or download speedtest with the given number of bytes.
func (f *FastProvider) runSpeedTest(mode string, payloadSize int) (time.Duration, error) {
if payloadSize < 1 {
return 0, errors.New("payload size should be at least 1")
}
eg := errgroup.Group{}
startTime := time.Now()
for _, target := range f.targets {
target := target
eg.Go(func() error {
switch mode {
case "download":
return downloadFastSample(target, payloadSize)
case "upload":
return uploadFastSample(target, payloadSize)
default:
return errors.New("unknown run mode")
}
})
}
if err := eg.Wait(); err != nil {
return 0, err
}
return time.Now().Sub(startTime), nil
}
// calculateSpeed returns amount of bits per second. Aggregated from all the other config and result variables
// obtained during testing
func (f *FastProvider) calculateSpeed(seconds time.Duration) (bits uint64) {
if seconds.Seconds() == 0 {
return 0
}
t := seconds.Seconds()
mBits := float64(f.FileSizeInMBytes*f.TargetAmount*8) / t
return uint64(mBits * 1024 * 1024)
}
// downloadFastSample downloads a sample of given size from the fast.com CDN.
// Being used for speedtest
func downloadFastSample(target string, payloadSize int) error {
var client = http.Client{}
u, _ := url.Parse(target)
u.Path = fmt.Sprintf("/speedtest/range/0-%d", payloadSize)
target = u.String()
resp, err := client.Get(target)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
return err
}
// uploadFastSample downloads a sample of given size from the fast.com CDN.
// Being used for speedtest
func uploadFastSample(target string, payloadSize int) error {
var client = http.Client{}
data := url.Values{}
data.Add("content", strings.Repeat("0", payloadSize))
resp, err := client.PostForm(target, data)
if err != nil {
return err
}
defer resp.Body.Close()
_, err = ioutil.ReadAll(resp.Body)
return err
}