├── .gitignore
├── README.md
├── binaries
├── darwin
│ └── amd64
│ │ └── goperf
├── freebsd
│ ├── 386
│ │ └── goperf
│ └── amd64
│ │ └── goperf
├── linux
│ ├── 386
│ │ └── goperf
│ └── amd64
│ │ └── goperf
└── windows
│ ├── 386
│ └── goperf.exe
│ └── amd64
│ └── goperf.exe
├── build.sh
├── goperf
├── goperf.go
├── httputils
├── httputils.go
├── httputils.test
├── httputils_test.go
├── out.prof
├── pprof002.svg
└── test_data
│ ├── test.html
│ └── test_basic.html
├── perf
└── perf.go
├── readme_imgs
├── Fetch.png
├── GoPerf.png
└── GoPerfOutput.png
└── request
├── combine.go
├── fetch.go
├── fetchall.go
└── structs.go
/.gitignore:
--------------------------------------------------------------------------------
1 | *swp
2 | *.swo
3 | output.json
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # goperf
2 | A highly concurrant website load tester with a simple intuitive command line syntax.
3 |
4 | 
5 |
6 | The header image shows goperf running on a 32 cpu machine. The machine being tested was a traditional web stack with a load balancer and 10 app servers.
7 |
8 | Goperf fetches the html document as well as all the img, css, and js assets in an effort to realistically simulate a a basic browser request to your site. *Support for follow up ajax requests is aimed for the next release*
9 |
10 | Goperf also supports simple http request headers like user-agent and cookies strings.
11 |
12 | ## Prebuilt Binaries
13 | [Darwin 64 bit](https://github.com/gnulnx/goperf/raw/master/binaries/darwin/amd64/goperf)
14 |
15 | [FreeBSD 64 bit](https://github.com/gnulnx/goperf/raw/master/binaries/freebsd/amd64/goperf)
16 |
17 | [FreeBSD 32 bit](https://github.com/gnulnx/goperf/raw/master/binaries/freebsd/386/goperf)
18 |
19 | [Linux 64 bit](https://github.com/gnulnx/goperf/raw/master/binaries/linux/amd64/goperf)
20 |
21 | [Linux 32 bit](https://github.com/gnulnx/goperf/raw/master/binaries/linux/386/goperf)
22 |
23 | [Windows 64 bit](https://github.com/gnulnx/goperf/raw/master/binaries/windows/amd64/goperf.exe)
24 |
25 | [Windows 32 bit](https://github.com/gnulnx/goperf/raw/master/binaries/windows/386/goperf.exe)
26 |
27 | ## Usage:
28 |
29 | ### Fetch a page and display info.
30 | ```
31 | ./goperf -url {url} -fetch
32 | ```
33 | This will print output like:
34 |
35 | 
36 |
37 | To Fetch a page and display all it's assets use:
38 | ```
39 | ./goperf -url {url} -fetch --printjson
40 | ```
41 | **NOTE** this will print the content of the body in each of the fetched assets. If you have large minified JS bundles it will be pretty messy. *A future version will support only showing the body text*
42 |
43 |
44 | Fetch a page that requires a session id (such as a django login)
45 | ```
46 | ./goperf -url http://192.168.33.11/student/ -fetch -cookies "sessionid_vagrant=0xkfeev882n0i9efkiq7vmd2i6efufz9;" --printjson
47 | ```
48 |
49 | ### Load testing
50 |
51 | Tell goperf the number of users you want to simulate and the number of seconds you want the simulation to run.
52 |
53 | ```
54 | ./goperf -url {url} -users {int} -sec {int}
55 | ```
56 |
57 | Goperf will kick off a seperate go routine for each user. Each user will then continiously fetch the url along with all it's page assets in seperate go routines. *Each users will make an initial GET request to fetch the cookies and then use them in follow up requests in order to simulate users sessions.*
58 |
59 | The light weight nature of goroutines allows this high concurancy to simulate many users with very litte memory. You will most likely overhewlm the test url servers or consume all of the available network bandwidth before memory becomes an issue.
60 |
61 | Load testing results:
62 |
63 | 
64 |
65 | ## Setup
66 | #### Ensure gopath is correctly setup
67 |
68 | Make sure you have your GOPATH setup to point to the go/bin directory.
69 | If you have a default go install on ubuntu it would be ~/go/bin.
70 | If so you would add this to your path.
71 | ```
72 | export PATH=$PATH:~/go/bin
73 | ```
74 | #### Install
75 |
76 | ```
77 | go get github.com/gnulnx/goperf
78 | ```
79 |
80 | #### Build
81 | ```
82 | go install github.com/gnulnx/goperf
83 | ```
84 |
85 |
86 | ### Run minimal unit and benchmark tests
87 | ```
88 | go test ./... -cover -bench=.
89 | ```
90 |
91 |
92 | ## Road map and future plans.
93 |
94 | Currently goperf is quite good at simulating browser requests that include the body, css, img, and js assets.
95 |
96 | However goper has no concept of an ajax request.
97 |
98 | The next phase of goperf will be adding in support for additional requests after intial page load. For example say you wanted to time how long it took for 10 users to hit your website and also request a specific api. This approach will allow us to have much better simulation for javacsript heavy sites.
99 |
100 | Longer term support for a chaos mode where the perf "users" move through the site randomly selecting a new url after each request.
101 |
--------------------------------------------------------------------------------
/binaries/darwin/amd64/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/darwin/amd64/goperf
--------------------------------------------------------------------------------
/binaries/freebsd/386/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/freebsd/386/goperf
--------------------------------------------------------------------------------
/binaries/freebsd/amd64/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/freebsd/amd64/goperf
--------------------------------------------------------------------------------
/binaries/linux/386/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/linux/386/goperf
--------------------------------------------------------------------------------
/binaries/linux/amd64/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/linux/amd64/goperf
--------------------------------------------------------------------------------
/binaries/windows/386/goperf.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/windows/386/goperf.exe
--------------------------------------------------------------------------------
/binaries/windows/amd64/goperf.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/binaries/windows/amd64/goperf.exe
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #/usr/bin/env bash
2 |
3 | # Build for WINDOWS
4 | env GOOS=windows GOARCH=amd64 go build
5 | mv goperf.exe binaries/windows/amd64
6 |
7 | env GOOS=windows GOARCH=386 go build
8 | mv goperf.exe binaries/windows/386
9 |
10 | # Build for FreeBSD
11 | env GOOS=freebsd GOARCH=amd64 go build
12 | mv goperf binaries/freebsd/amd64
13 |
14 | env GOOS=freebsd GOARCH=386 go build
15 | mv goperf binaries/freebsd/386/
16 |
17 | env GOOS=darwin GOARCH=amd64 go build
18 | mv goperf binaries/darwin/amd64/
19 |
20 | # Build for Linux
21 | env GOOS=linux GOARCH=386 go build
22 | mv goperf binaries/linux/386/
23 |
24 | env GOOS=linux GOARCH=amd64 go build
25 | mv goperf binaries/linux/amd64/
26 |
27 | # Build for current platform
28 | go build
29 |
--------------------------------------------------------------------------------
/goperf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gnulnx/goperf/d7e65ae890a4d241d7538a3113590b70dbc8577a/goperf
--------------------------------------------------------------------------------
/goperf.go:
--------------------------------------------------------------------------------
1 | /*
2 | Package goperf is a highly concurrant website load tester with a simple intuitive command line syntax.
3 |
4 | * Fetch a url and report stats
5 |
6 | This command will return all information for a given url.
7 | ./goperf -url http://qa.teaquinox.com -fetchall -printjson
8 |
9 | When fetchall is provided the returned struct will contain
10 | url, time, size, and data info.
11 |
12 | You can do a simpler request that leaves the data and headers out like this
13 | ./goperf -url http://qa.teaquinox.com -fetchall -printjson
14 |
15 |
16 | * Load testing
17 | ./goperf -url http://qa.teaquinox.com -sec 5 -users 5
18 | */
19 | package main
20 |
21 | import (
22 | "encoding/json"
23 | "flag"
24 | "fmt"
25 | "io"
26 | "net/http"
27 | "os"
28 | "runtime/pprof"
29 | "strconv"
30 |
31 | "github.com/gnulnx/color"
32 | "github.com/gnulnx/goperf/perf"
33 | "github.com/gnulnx/goperf/request"
34 | "github.com/gnulnx/vestigo"
35 | )
36 |
37 | func main() {
38 | // I ❤️ the way go handles command line arguments
39 | fetch := flag.Bool("fetch", false, "Fetch -url and report it's stats. Does not return resources")
40 | fetchall := flag.Bool("fetchall", false, "Fetch -url and report stats return all assets (js, css, img)")
41 | printjson := flag.Bool("printjson", false, "Print json output")
42 | perftest := flag.Bool("perftest", false, "Run the goland perf suite")
43 | users := flag.Int("users", 1, "Number of concurrent users/connections")
44 | url := flag.String("url", "https://qa.teaquinox.com", "url to test")
45 | seconds := flag.Int("sec", 2, "Number of seconds each concurrant user/connection should make consequitive requests")
46 | web := flag.Bool("web", false, "Run as a webserver -web {port}")
47 | port := flag.Int("port", 8080, "used with -web to specif which port to bind")
48 | cookies := flag.String("cookies", "{}", "Set up cookies for the request")
49 | headers := flag.String("headers", "{}", "Set up headers for the request")
50 | useragent := flag.String("useragent", "goperf", "Set the user agent string")
51 |
52 | // Not currently used, but could be
53 | iterations := flag.Int("iter", 1000, "Iterations per user/connection")
54 | output := flag.Int("output", 5, "Show user output every {n} iterations")
55 | verbose := flag.Bool("verbose", false, "Show verbose output")
56 | var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
57 | flag.Parse()
58 |
59 | http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100
60 |
61 | if *web {
62 | router := vestigo.NewRouter()
63 | router.SetGlobalCors(&vestigo.CorsAccessControl{
64 | AllowOrigin: []string{"*", "http://138.197.97.39:8080"},
65 | })
66 |
67 | router.Post("/api/", handler)
68 | router.SetCors("/api/", &vestigo.CorsAccessControl{
69 | AllowMethods: []string{"POST"}, // only allow cors for this resource on POST calls
70 | })
71 | sPort := ":" + strconv.Itoa(*port)
72 | color.Green("Your website is available at 127.0.0.1%s", sPort)
73 | http.ListenAndServe(sPort, router)
74 | }
75 |
76 | if *fetch || *fetchall {
77 | // TODO This method treats these command line arguments exactly the same... no good
78 | // -fetch -printjson should ONLY return the body of the primary request and not the other assets
79 |
80 | // This section will make an initial GET request and try to set any cookies we find
81 | if *cookies == "" {
82 | resp1, _ := http.Get(*url)
83 | if len(resp1.Header["Set-Cookie"]) > 0 {
84 | cookies = &resp1.Header["Set-Cookie"][0]
85 | }
86 | }
87 | resp := request.FetchAll(
88 | request.FetchInput{
89 | BaseURL: *url,
90 | Retdat: *fetchall,
91 | Cookies: *cookies,
92 | Headers: *headers,
93 | UserAgent: *useragent,
94 | },
95 | )
96 |
97 | if *printjson {
98 | tmp, _ := json.MarshalIndent(resp, "", " ")
99 | fmt.Println(string(tmp))
100 | }
101 |
102 | request.PrintFetchAllResponse(resp)
103 |
104 | os.Exit(1)
105 | }
106 |
107 | // TODO Declare an inline parameter struct...
108 | perfJob := &perf.Init{
109 | Iterations: *iterations,
110 | Threads: *users,
111 | URL: *url,
112 | Output: *output,
113 | Verbose: *verbose,
114 | Seconds: *seconds,
115 | Cookies: *cookies,
116 | Headers: *headers,
117 | UserAgent: *useragent,
118 | }
119 | f, _ := os.Create(*cpuprofile)
120 | results := perfJob.Basic()
121 |
122 | if *perftest {
123 | pprof.StartCPUProfile(f)
124 | defer pprof.StopCPUProfile()
125 | }
126 |
127 | // Write json response to file.
128 | outfile, _ := os.Create("./output.json")
129 |
130 | if *printjson {
131 | perfJob.JsonResults()
132 | } else {
133 | perfJob.Print()
134 | }
135 |
136 | tmp, _ := json.MarshalIndent(results, "", " ")
137 | outfile.WriteString(string(tmp))
138 | color.Magenta("Job Results Saved: ./output.json")
139 | }
140 |
141 | /*
142 | Check that the request parameters are correct and return them.
143 | Also return an array of error string if the parameters were not right
144 | */
145 | func checkParams(r *http.Request) ([]string, string, int, int) {
146 | errors := []string{}
147 | seconds := 0
148 | users := 0
149 | var err error
150 |
151 | // Check that url has been supplied
152 | url, ok := r.PostForm["url"]
153 | if !ok {
154 | errors = append(errors, " - url (string) is a required field")
155 | url = []string{""}
156 | }
157 |
158 | // Check that seconds is supplied
159 | strSeconds, ok := r.PostForm["sec"]
160 | if !ok {
161 | errors = append(errors, " - sec (int) is a required field")
162 | strSeconds = []string{}
163 | }
164 | if len(strSeconds) > 0 {
165 | seconds, err = strconv.Atoi(strSeconds[0])
166 | if err != nil {
167 | errors = append(errors, " - sec (int) is a required field")
168 | seconds = 0
169 | }
170 | }
171 |
172 | // Check user field has been supplied
173 | strUsers, ok := r.PostForm["users"]
174 | if !ok {
175 | errors = append(errors, " - users (int) is a required field")
176 | strUsers = []string{}
177 | }
178 | if len(strUsers) > 0 {
179 | users, err = strconv.Atoi(strUsers[0])
180 | if err != nil {
181 | errors = append(errors, " - users (int) is a required field")
182 | users = 0
183 | }
184 | }
185 |
186 | return errors, url[0], seconds, users
187 | }
188 |
189 | func handler(w http.ResponseWriter, r *http.Request) {
190 | r.ParseForm()
191 | errors, url, seconds, users := checkParams(r)
192 | if len(errors) > 0 {
193 | for i := 0; i < len(errors); i++ {
194 | e := errors[i] + "\n"
195 | w.Write([]byte(e))
196 | }
197 | return
198 | }
199 |
200 | perfJob := &perf.Init{
201 | URL: url,
202 | Threads: users,
203 | Seconds: seconds,
204 | }
205 | perfJob.Basic()
206 | jsonResults := perfJob.JsonResults()
207 |
208 | w.Header().Set("Content-Type", "application/json")
209 | w.WriteHeader(http.StatusCreated)
210 | io.WriteString(w, jsonResults)
211 | }
212 |
--------------------------------------------------------------------------------
/httputils/httputils.go:
--------------------------------------------------------------------------------
1 | package httputils
2 |
3 | import (
4 | "log"
5 | "regexp"
6 | "strings"
7 |
8 | "github.com/PuerkitoBio/goquery"
9 | )
10 |
11 | /*
12 | ParseAllAssetsSequential takes a string of text (typically from a http.Response.Body)
13 | and return the urls for the page