├── Makefile ├── url.hh ├── url.cc ├── README.md └── goofy.cc /Makefile: -------------------------------------------------------------------------------- 1 | SRCS = goofy.cc url.cc 2 | OBJS = goofy.o url.o 3 | CXXFLAGS = -g 4 | 5 | goofy: $(OBJS) 6 | $(CXX) -o goofy $(OBJS) 7 | 8 | clean: 9 | rm -f goofy $(OBJS) 10 | -------------------------------------------------------------------------------- /url.hh: -------------------------------------------------------------------------------- 1 | #ifndef URL_HH_ 2 | #define URL_HH_ 3 | #include 4 | struct url { 5 | url(const std::string& url_s); 6 | const std::string& full() { return url_; } 7 | const std::string& host() { return host_; } 8 | const std::string& request() { return request_; } 9 | int port() { return port_; } 10 | private: 11 | void parse(const std::string& url_s); 12 | private: 13 | std::string url_, protocol_, host_, path_, query_, request_; 14 | int port_; 15 | }; 16 | #endif /* URL_HH_ */ 17 | -------------------------------------------------------------------------------- /url.cc: -------------------------------------------------------------------------------- 1 | #include "url.hh" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | using namespace std; 8 | 9 | url::url(const std::string &url_s) : url_(url_s) { 10 | port_ = 0; 11 | parse(url_s); 12 | } 13 | 14 | void url::parse(const string& url_s) 15 | { 16 | int i; 17 | 18 | // Protocol is everything up to "://". 19 | const string prot_end("://"); 20 | string::const_iterator it = search(url_s.begin(), url_s.end(), 21 | prot_end.begin(), prot_end.end()); 22 | protocol_.reserve(distance(url_s.begin(), it)); 23 | transform(url_s.begin(), it, 24 | back_inserter(protocol_), 25 | ptr_fun(tolower)); // protocol is icase 26 | if( it == url_s.end() ) 27 | return; 28 | advance(it, prot_end.length()); 29 | 30 | // [user[:pass]@]host[:port]. user:pass is not yet supported. 31 | string::const_iterator path_i = find(it, url_s.end(), '/'); 32 | host_.reserve(distance(it, path_i)); 33 | host_.assign(it, path_i); 34 | if ((i=host_.find(':')) != host_.npos) { 35 | port_ = atoi(host_.substr(i+1, host_.npos).c_str()); 36 | host_.resize(i); 37 | } 38 | else { 39 | port_ = 80; 40 | } 41 | // host is icase 42 | transform(host_.begin(), host_.end(), host_.begin(), ptr_fun(tolower)); 43 | 44 | string::const_iterator query_i = find(path_i, url_s.end(), '?'); 45 | request_.assign(path_i, url_s.end()); 46 | path_.assign(path_i, query_i); 47 | if( query_i != url_s.end() ) 48 | ++query_i; 49 | query_.assign(query_i, url_s.end()); 50 | } 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Goofy is an HTTP load testing tool that simulates waves of surfers in 2 | an unusual way (get it? ask a surfer/boarder). 3 | 4 | Other HTTP load testing tools (such as ab and siege) perform an operation as 5 | quickly as possible and report server performance. The number of concurrent 6 | connections is specified and the connection rate is measured. The outcome of 7 | operations is summarized when the pre-defined test period is complete. 8 | 9 | Goofy does the opposite of that. It initiates operations at a specified rate 10 | and concurrency, and reports the outcome of each operation regardless of how 11 | long it takes. It shows the status of operations as they occur, so it is 12 | possible to determine the timing of intermediate steps. 13 | 14 | More specifically, Goofy initiates a fixed number of connections to a 15 | URL every specified time period, letting them all run in parallel 16 | until they finish. Each time period, it reports on the number of 17 | connections pending, established, and closed, as well as how 18 | connections closed (syscall error or HTTP status). 19 | 20 | ## Usage 21 | 22 | ``` 23 | Usage: goofy [args] url [url...] 24 | -n num number of requests per wave 25 | -t ms[:limit] milliseconds between waves; run limit total waves; 26 | default to one wave 27 | -r ms milliseconds between reports; defaults to -t or 1000 28 | -m secs total seconds to run test; default is unlimited 29 | -f fds maximum number of sockets to request from the os 30 | -h hdr add hdr ("Header: value") to each request 31 | -d debug 32 | ``` 33 | 34 | If multiple URLs are provided, goofy round-robins across them. 35 | 36 | ## Quick start 37 | 38 | Let's test whether Google handle 3 page requests at a time. 39 | 40 | ``` 41 | $ goofy -n 3 -t 2000 -r 1000 http://www.google.com/ 42 | ``` 43 | 44 | -n specifies the number of requests per wave, -t specifies the time between 45 | waves (ms), and -r specifies the reporting period (ms). In this case, we're 46 | sending 3 requests every 2 seconds, but reporting every second. Here's the 47 | output: 48 | 49 | ``` 50 | | delta | | total | | results | 51 | secs new estb clos pend estb errs 200 500 503 504 xxx 52 | ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- 53 | 0 3 0 0 3 0 0 0 0 0 0 0 54 | 1 0 3 3 0 0 0 3 0 0 0 0 55 | 2 3 0 0 3 0 0 0 0 0 0 0 56 | 3 0 3 3 0 0 0 3 0 0 0 0 57 | ``` 58 | 59 | The "delta" group shows what happened during the current reporting period. new 60 | is initiated but not yet established connections; estb is established 61 | connections, and clos is closed connections. 62 | 63 | The "total" group counts the status of all open but not yet complete 64 | connections. pend is initiated but not yet established, and estb is connected 65 | but not yet closed. 66 | 67 | The "results" group shows how many connections ended during that period and 68 | with what result. errs shows socket API errors (e.g. ECONNREFUSED), and the 69 | other columns show HTTP statuses. 70 | 71 | At time 0, we see 3 newly opened connections in the new column. They have not 72 | connected yet, so delta estb shows 0, and pend shows 3. No requests have 73 | completed. 74 | 75 | At time 1, the delta-estb and clos(ed) columns both show 3, indicating that the 76 | 3 requests from the first wave got established, completed, and closed since 77 | time 0. At that instant, no connections were still open, so the pend and total- 78 | estb columns show zero; if a request had taken longer than 1 second, it would 79 | still be open and total-estb would show it. We also see that all 3 connections 80 | that closed got HTTP status 200. 81 | 82 | At time 2, the second wave starts, and completes at time 3. 83 | 84 | So yes, Google can handle 3 requests at a time. :-) 85 | 86 | ## Test: PHP-FPM process launching 87 | 88 | [ This experiment was conducted in 2014. ] 89 | 90 | I noticed a problem with how the PHP-FPM ondemand process manager spawns worker 91 | processes. PHP-FPM is running under Apache, and is configured with: 92 | 93 | ``` 94 | pm = ondemand 95 | pm.max_children = 100 96 | pm.process_idle_timeout = 10s 97 | ``` 98 | 99 | To test it, I ran just one wave of 10 requests, each to a PHP script that 100 | sleeps for 3 seconds. By default, goofy reports results every second. Here's 101 | what happened: 102 | 103 | ``` 104 | $ ./goofy -n 10 'http://server/sleep.php?sleep=3' 105 | | delta | | total | | results | 106 | secs new estb clos pend estb errs 200 500 503 504 xxx Notes added by hand 107 | ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----------------------------------------------------- 108 | 0 10 0 0 10 0 0 0 0 0 0 0 10 connections opened, all still pending. 109 | 1 0 10 0 0 10 0 0 0 0 0 0 10 connections became established. 110 | 2 0 0 0 0 10 0 0 0 0 0 0 All 10 connections are still established. 111 | 4 0 0 4 0 6 0 4 0 0 0 0 Between 3-4s, *four* requests complete. Why not 10? 112 | 5 0 0 0 0 6 0 0 0 0 0 0 113 | 7 0 0 4 0 2 0 4 0 0 0 0 Between 6-7s, four more complete. 114 | 8 0 0 0 0 2 0 0 0 0 0 0 115 | 10 0 0 2 0 0 0 2 0 0 0 0 Between 9-10s, the last two complete. 116 | 11 0 0 0 0 0 0 0 0 0 0 0 117 | ``` 118 | 119 | PHP-FPM should have spawned 10 processes, but this shows that it only spawned 120 | four; ps on the server confirms this observation. Even when the first set of 121 | requests finished and there were still six pending, still no new workers were 122 | spawned. 123 | 124 | Clearly, PHP-FPM is not behaving correctly. This data helped me track down the 125 | bug in the ondemand process manager; a fix is forthcoming. 126 | 127 | ## A lengthy example: A History of a Thousand Connections 128 | 129 | [ This experiment was conducted in 2010. ] 130 | 131 | With an Apache web server running PHP under FastCGI using mod_fcgid, I 132 | wanted to understand exactly how the number of Apache processes, 133 | php-cgi processes, and the various timeouts affected how HTTP requests 134 | get served. 135 | 136 | I accomplished this by having Goofy initiate 1,000 simultaneous 137 | connections to a URL that just sleeps for 60 seconds and waits for 138 | them all to finish one way or another. The command line is: 139 | 140 | ``` 141 | $ ./goofy -n 1000 -r 2000 'http://server/sleep.php?sleep=60' 142 | ``` 143 | 144 | These options say to open 1,000 connections and to report results 145 | every 2 seconds. Goofy produces no output if nothing happens during a 146 | particular reporting interval. 147 | 148 | For this test, the server was an EC2 m1.small instance configured with 149 | 256 Apache processes but only 10 php-cgi processes. I learned: 150 | 151 | * The server kernel/Apache accepts more than one TCP 152 | connection per Apache process, but not even as many as two per 153 | Apache process. So the "listen queue" is not behaving as I 154 | expect. It seems like we should expect to get at least 1,280 155 | established TCP connections to a server with 256 Apache processes 156 | before connection errors occur. 157 | 158 | * The lucky requests that actually get through to a PHP process work 159 | even under these conditions. 160 | 161 | * mod_fcgi consistently kicks out PHP requests that cannot get a PHP 162 | process after 65 seconds. 163 | 164 | ``` 165 | | delta | | total | | results | 166 | secs new estb clos totl err 200 500 503 504 xxx Notes added by hand 167 | ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----------------------------------------------------- 168 | 0 1000 0 0 1000 0 0 0 0 0 0 1000 new non-blocking connections opened. 169 | 2 0 256 0 1000 0 0 0 0 0 0 256 of the non-blocking connections actually connect. 170 | 5 0 256 0 1000 0 0 0 0 0 0 256 more connect. 171 | 11 0 30 0 1000 0 0 0 0 0 0 30 more connect. 172 | 23 0 66 0 1000 0 0 0 0 0 0 66 more connect. We're up 608 TCP connections for 256 Apache kids. 173 | 38 0 0 99 901 99 0 0 0 0 0 The remote kernel kicks out 99 requests with "connection reset". 174 | connect: Connection reset by peer:99 Why not the other 293 also? I have no idea. 175 | 47 0 2 0 901 0 0 0 0 0 0 No Apache has released its socket, yet now 2 more connects complete! 176 | 61 0 0 1 900 0 1 0 0 0 0 It's been 60 secs so the 10 lucky connections... 177 | 64 0 0 9 891 0 9 0 0 0 0 ...that got a php-cgi complete with status 200. 178 | 67 0 0 142 749 0 0 0 142 0 0 At 65 secs, mod_fcigd times out pending reqs with a 503. 179 | 69 0 0 2 747 0 0 0 2 0 0 More mod_fcgid 503s... 180 | 71 0 0 2 745 0 0 0 2 0 0 more 181 | 73 0 0 2 743 0 0 0 2 0 0 more 182 | 76 0 0 2 741 0 0 0 2 0 0 more 183 | 78 0 0 5 736 0 0 0 5 0 0 more 184 | 80 0 0 12 724 0 0 0 13 0 0 more 185 | 82 0 0 14 710 0 0 0 13 0 0 more 186 | 85 0 0 30 680 0 0 0 30 0 0 more 187 | 87 0 0 32 648 0 0 0 32 0 0 more 188 | 89 0 0 1 647 0 0 0 1 0 0 more 189 | ``` 190 | 191 | So, after 90 seconds, at the TCP level we've seen 610 connections and 192 | 99 connection resets. After all that, we had the 10 status 200 193 | completions, then mod_fcgid kicked out 244 requests with status 503. 194 | 195 | ``` 196 | 95 0 282 0 647 0 0 0 0 0 0 The server accepts 282 more TCP connections. 197 | 121 0 0 1 646 0 1 0 0 0 0 It's been 120 seconds. 10 more lucky php requests complete. 198 | 124 0 0 9 637 0 9 0 0 0 0 199 | 127 0 0 5 632 4 0 0 1 0 0 4 TCP resets, and (at about 65*2 secs) the 503s start again. 200 | connect: Connection reset by peer:4 201 | 129 0 0 6 626 5 0 0 1 0 0 more of both 202 | connect: Connection reset by peer:5 203 | 133 0 0 142 484 22 0 0 120 0 0 more of both 204 | connect: Connection reset by peer:22 205 | 135 0 0 3 481 0 0 0 3 0 0 503s forever! 206 | 137 0 0 2 479 0 0 0 2 0 0 more 207 | 139 0 0 2 477 0 0 0 2 0 0 more 208 | 141 0 0 2 475 0 0 0 2 0 0 more 209 | 144 0 0 7 468 0 0 0 7 0 0 more 210 | 146 0 0 19 449 0 0 0 19 0 0 more 211 | 148 0 0 15 434 0 0 0 15 0 0 more 212 | 150 0 0 41 393 0 0 0 41 0 0 more 213 | 152 0 0 31 362 0 0 0 31 0 0 more 214 | 154 0 0 1 361 0 0 0 1 0 0 more 215 | 181 0 0 1 360 0 1 0 0 0 0 Oh, 180 seconds! 10 more luck php requests complete. 216 | 184 0 0 9 351 0 9 0 0 0 0 217 | 189 0 0 1 350 0 0 0 1 0 0 The 65*3 secs 503s start. 218 | 191 0 0 108 242 108 0 0 0 0 0 Now our LOCAL KERNEL is saying "connection timed out" for 108 reqs. 219 | connect: Connection timed out:108 220 | 194 0 0 2 240 0 0 0 2 0 0 503s... 221 | 196 0 0 29 211 0 0 0 29 0 0 more 222 | 198 0 0 86 125 0 0 0 86 0 0 more 223 | 200 0 0 3 122 0 0 0 3 0 0 more 224 | 202 0 0 2 120 0 0 0 2 0 0 more 225 | 204 0 0 1 119 0 0 0 1 0 0 more 226 | 208 0 0 1 118 0 0 0 1 0 0 more 227 | 227 0 0 17 101 0 0 0 17 0 0 more 228 | 230 0 0 20 81 0 0 0 20 0 0 more 229 | 232 0 0 34 47 0 0 0 34 0 0 more 230 | 234 0 0 37 10 0 0 0 37 0 0 more 231 | 241 0 0 1 9 0 1 0 0 0 0 At four minutes, the last 10 luck php requests complete. 232 | 244 0 0 9 0 0 9 0 0 0 0 And now we're done. 233 | ``` 234 | -------------------------------------------------------------------------------- /goofy.cc: -------------------------------------------------------------------------------- 1 | /* 2 | * Goofy: A slightly different web load testing tool that simulates 3 | * waves of surfers hitting a site. Goofy initiates a fixed number of 4 | * connections to a URL every specified time period, letting them all 5 | * run in parallel until they finish. Each time period, it reports on 6 | * the number of connections opened, closed, and open, as well as how 7 | * connections closed (syscall error or HTTP status). 8 | */ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | #include 27 | #include 28 | #include 29 | 30 | #include "url.hh" 31 | 32 | typedef std::map intmap; 33 | typedef std::map strmap; 34 | typedef std::vector strvec; 35 | typedef std::vector urlvec; 36 | typedef std::vector addrvec; 37 | 38 | struct wave_stat { 39 | wave_stat() { 40 | clear(); 41 | } 42 | void clear() { 43 | opened = closed = connected = 0; 44 | socket.clear(); 45 | connect.clear(); 46 | write.clear(); 47 | http_code.clear(); 48 | } 49 | int opened; 50 | int connected; 51 | int closed; 52 | intmap socket; 53 | intmap connect; 54 | intmap read; 55 | intmap write; 56 | intmap http_code; 57 | }; 58 | 59 | class time_interval { 60 | public: 61 | time_interval(const char *_label) : label(_label) { 62 | set(0); 63 | mark(); 64 | } 65 | 66 | // Set this.marked to the current time. 67 | void mark() { 68 | gettod(&marked); 69 | } 70 | 71 | // Set this.marked to the given timeval. 72 | void mark(struct timeval *t) { 73 | memcpy(&marked, t, sizeof(marked)); 74 | } 75 | 76 | // Set and get the interval used by passed(). 77 | void set(int _interval) { interval = _interval; } 78 | int get() const { return interval; } 79 | 80 | // Return TRUE if now-this.marked exceeds the interval. 81 | int passed(struct timeval *now) const { 82 | struct timeval delta; 83 | timeval_subtract(&delta, now, &marked); 84 | //printf("%s: %d, %d, %d, %d, %s\n", label, interval, delta.tv_sec, delta.tv_usec, delta.tv_sec*1000000+delta.tv_usec, delta.tv_sec*1000000+delta.tv_usec > interval ? "true" : "false" ); 85 | return ((delta.tv_sec*1000000+delta.tv_usec) > interval); 86 | } 87 | 88 | // Fill in out with time elapsed since last mark. 89 | void since(struct timeval *out) const { 90 | struct timeval now; 91 | gettod(&now); 92 | timeval_subtract(out, &now, &marked); 93 | } 94 | 95 | // STATIC. Fill in now with the current time of day. 96 | static void gettod(struct timeval *now) { 97 | if (gettimeofday(now, NULL) < 0) { 98 | perror("gettimeofday"); 99 | exit(1); 100 | } 101 | } 102 | 103 | // STATIC. Fill in RESULT with X-Y. 104 | // Return 1 if the difference is negative, otherwise 0. 105 | static int timeval_subtract (struct timeval *result, const struct timeval *x, const struct timeval *y) 106 | { 107 | struct timeval _y(*y); 108 | 109 | /* Perform the carry for the later subtraction by updating y. */ 110 | if (x->tv_usec < _y.tv_usec) { 111 | int nsec = (_y.tv_usec - x->tv_usec) / 1000000 + 1; 112 | _y.tv_usec -= 1000000 * nsec; 113 | _y.tv_sec += nsec; 114 | } 115 | if (x->tv_usec - _y.tv_usec > 1000000) { 116 | int nsec = (x->tv_usec - _y.tv_usec) / 1000000; 117 | _y.tv_usec += 1000000 * nsec; 118 | _y.tv_sec -= nsec; 119 | } 120 | 121 | /* Compute the time remaining to wait. 122 | tv_usec is certainly positive. */ 123 | result->tv_sec = x->tv_sec - _y.tv_sec; 124 | result->tv_usec = x->tv_usec - _y.tv_usec; 125 | 126 | /* Return 1 if result is negative. */ 127 | return x->tv_sec < _y.tv_sec; 128 | } 129 | 130 | struct timeval marked, now; 131 | int interval; 132 | const char *label; 133 | }; 134 | 135 | enum conn_state { CONN_UNUSED = 0, CONN_CONNECTING, CONN_ESTABLISHED, }; 136 | struct conn_info_t { 137 | int request_number; 138 | int url_number; 139 | enum conn_state state; 140 | struct timeval connecting; 141 | struct timeval connected; 142 | }; 143 | 144 | struct wave_stat wave_stats; 145 | struct conn_info_t *conn_info; 146 | int request_count; 147 | int debug; 148 | struct pollfd *fds; 149 | int fds_len = 0; 150 | strmap http_codes; 151 | 152 | void usage() { 153 | fprintf(stderr, "Usage: goofy [args] url [url...]\n" 154 | " -n num number of requests per wave\n" 155 | " -t ms[:limit] milliseconds between waves; limit total waves\n" 156 | " default is one wave\n" 157 | " -r ms milliseconds between reports; defaults to -t or 1000\n" 158 | " -m secs total seconds to run test; default is unlimited\n" 159 | " -f fds maximum number of sockets to request from the os\n" 160 | " -h hdr add hdr (\"Header: value\") to each request\n" 161 | " -d debug\n"); 162 | exit(1); 163 | } 164 | 165 | /* Set fd to be non-blocking. */ 166 | void setnonblocking(int fd) { 167 | long arg; 168 | if ((arg = fcntl(fd, F_GETFL, NULL)) < 0) { 169 | perror("fcntl(F_GETFL)"); 170 | exit(1); 171 | } 172 | arg |= O_NONBLOCK; 173 | if (fcntl(fd, F_SETFL, arg) < 0) { 174 | perror("fcntl(F_SETFL)"); 175 | exit(1); 176 | } 177 | } 178 | 179 | /* Set fd to be blocking. */ 180 | void setblocking(int fd) { 181 | long arg; 182 | if ((arg = fcntl(fd, F_GETFL, NULL)) < 0) { 183 | perror("fcntl(F_GETFL)"); 184 | exit(1); 185 | } 186 | arg &= ~O_NONBLOCK; 187 | if (fcntl(fd, F_SETFL, arg) < 0) { 188 | perror("fcntl(F_SETFL)"); 189 | exit(1); 190 | } 191 | } 192 | 193 | /** 194 | * Initiate num new non-blocking connections to addr. 195 | */ 196 | void open_connections(int num, const addrvec &addrs, int ¤t_url) { 197 | int i, j; 198 | 199 | for (i = 0; i < num; ++i) { 200 | // Find the first available slot. 201 | for (j = 0; j < fds_len; j++) { 202 | if (conn_info[j].state == CONN_UNUSED) 203 | break; 204 | } 205 | if (j == fds_len) { 206 | fprintf(stderr, "out of fds\n"); 207 | exit(1); 208 | } 209 | 210 | // Create the socket. 211 | int fd = socket(AF_INET, SOCK_STREAM, 0); 212 | if (fd < 0) { 213 | wave_stats.socket[errno]++; 214 | continue; 215 | } 216 | 217 | // Select the next address; 218 | const struct sockaddr *addr = (struct sockaddr *) &addrs[current_url]; 219 | conn_info[j].url_number = current_url; 220 | current_url = (current_url + 1) % addrs.size(); 221 | 222 | // Use non-blocking connects which correctly fail with EINPROGRESS. 223 | setnonblocking(fd); 224 | if (! (connect(fd, (struct sockaddr *) addr, sizeof(*addr))<0 225 | && errno == EINPROGRESS)) { 226 | wave_stats.connect[errno]++; 227 | close(fd); 228 | continue; 229 | } 230 | 231 | // Record the socket. Request POLLOUT so poll() informs us on connect. 232 | fds[j].fd = fd; 233 | fds[j].events = POLLIN|POLLOUT; 234 | conn_info[j].state = CONN_CONNECTING; 235 | conn_info[j].request_number = request_count++; 236 | time_interval::gettod(&conn_info[j].connecting); 237 | wave_stats.opened++; 238 | 239 | if (debug) 240 | printf("open: fds %d, fd %d\n", j, fd); 241 | } 242 | } 243 | 244 | /** 245 | * Display strerror() strings from map, prefixed by label. 246 | */ 247 | void report_errors(intmap &map, const char *label) { 248 | if (map.size() > 0) { 249 | std::cout << "\t" << label << ": "; 250 | for (intmap::iterator it = map.begin(); it != map.end(); it++) { 251 | std::cout << strerror(it->first) << ":" << it->second << " "; 252 | } 253 | std::cout << std::endl; 254 | } 255 | } 256 | 257 | /** 258 | * Display errmap error strings from map, prefixed by label. 259 | */ 260 | void report_errors(strmap &errmap, intmap &map, const char *label) { 261 | if (map.size() > 0) { 262 | std::cout << "\t" << label << ": "; 263 | for (intmap::iterator it = map.begin(); it != map.end(); it++) { 264 | std::cout << it->first << " " << errmap[it->first] << ":" << it->second << " "; 265 | } 266 | std::cout << std::endl; 267 | } 268 | } 269 | 270 | /** 271 | * Display events since the last reporting period, then reset the counters. 272 | */ 273 | void report_connections(time_interval *start) { 274 | static int rows = 0; 275 | 276 | if (rows == 0) { 277 | printf(" | delta | | total | | results |\n" 278 | "secs new estb clos pend estb errs 200 500 503 504 xxx\n" 279 | "---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ---- ----\n"); 280 | } 281 | rows++; 282 | 283 | static int skip_if_nothing_happened = 0; 284 | int nothing_happened = (wave_stats.opened == 0 && 285 | wave_stats.connected == 0 && 286 | wave_stats.closed == 0 && 287 | wave_stats.socket.size() == 0 && 288 | wave_stats.connect.size() == 0 && 289 | wave_stats.read.size() == 0 && 290 | wave_stats.write.size() == 0 && 291 | wave_stats.http_code.size() == 0); 292 | if (nothing_happened) { 293 | if (skip_if_nothing_happened) { 294 | return; 295 | } 296 | else { 297 | skip_if_nothing_happened = 1; 298 | } 299 | } 300 | else { 301 | skip_if_nothing_happened = 0; 302 | } 303 | 304 | intmap::iterator it; 305 | int connecting = 0, established = 0, errs = 0, http_errs = 0; 306 | 307 | // Sum all syscall errors. 308 | for (it = wave_stats.socket.begin(); it != wave_stats.socket.end(); it++) { 309 | errs += it->second; 310 | } 311 | for (it = wave_stats.connect.begin(); it != wave_stats.connect.end(); it++) { 312 | errs += it->second; 313 | } 314 | for (it = wave_stats.read.begin(); it != wave_stats.read.end(); it++) { 315 | errs += it->second; 316 | } 317 | for (it = wave_stats.write.begin(); it != wave_stats.write.end(); it++) { 318 | errs += it->second; 319 | } 320 | // Sum the HTTP codes to report collectively. 321 | for (it = wave_stats.http_code.begin(); it != wave_stats.http_code.end(); it++) { 322 | switch (it->first) { 323 | case 200: 324 | case 500: 325 | case 503: 326 | case 504: 327 | break; 328 | default: 329 | http_errs += it->second; 330 | break; 331 | } 332 | } 333 | // Sum statuses. 334 | for (int i = 0; i < fds_len; ++i) { 335 | switch (conn_info[i].state) { 336 | case CONN_UNUSED: 337 | break; 338 | case CONN_CONNECTING: 339 | connecting++; 340 | break; 341 | case CONN_ESTABLISHED: 342 | established++; 343 | break; 344 | } 345 | } 346 | 347 | struct timeval since; 348 | start->since(&since); 349 | printf("%4lu %4d %4d %4d %4d %4d %4d %4d %4d %4d %4d %4d\n", since.tv_sec, wave_stats.opened, wave_stats.connected, wave_stats.closed, connecting, established, errs, wave_stats.http_code[200], wave_stats.http_code[500], wave_stats.http_code[503], wave_stats.http_code[504], http_errs); 350 | report_errors(wave_stats.socket, "socket"); 351 | report_errors(wave_stats.connect, "connect"); 352 | report_errors(wave_stats.read, "read"); 353 | report_errors(wave_stats.write, "write"); 354 | wave_stats.http_code.erase(200); 355 | wave_stats.http_code.erase(500); 356 | wave_stats.http_code.erase(503); 357 | wave_stats.http_code.erase(504); 358 | report_errors(http_codes, wave_stats.http_code, "http"); 359 | 360 | wave_stats.clear(); 361 | 362 | fflush(stdout); 363 | } 364 | 365 | /** 366 | * Clean up a connection slot. 367 | */ 368 | void close_connection(int i) { 369 | close(fds[i].fd); 370 | wave_stats.closed++; 371 | fds[i].fd = fds[i].events = 0; 372 | conn_info[i].state = CONN_UNUSED; 373 | 374 | // poll() can return POLLIN with an empty read and POLLHUP at the 375 | // same time. Prevent this from being called again. 376 | fds[i].revents &= ~(POLLIN | POLLHUP); 377 | } 378 | 379 | /** 380 | * Get the socket error for a connection slot. 381 | */ 382 | int get_sock_error(int i) { 383 | int optval; 384 | socklen_t optlen; 385 | optlen = sizeof(optval); 386 | if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &optval, &optlen)< 0) { 387 | perror("getsockopt(SO_ERROR, SOL_SOCKET"); 388 | exit(1); 389 | } 390 | return optval; 391 | } 392 | 393 | /** 394 | * Initialize the table of HTTP response code strings. 395 | */ 396 | void init_http_codes() { 397 | http_codes[100] = "Continue"; 398 | http_codes[101] = "Switching Protocols"; 399 | http_codes[200] = "OK"; 400 | http_codes[201] = "Created"; 401 | http_codes[202] = "Accepted"; 402 | http_codes[203] = "Non-Authoritative Information"; 403 | http_codes[204] = "No Content"; 404 | http_codes[205] = "Reset Content"; 405 | http_codes[206] = "Partial Content"; 406 | http_codes[300] = "Multiple Choices"; 407 | http_codes[301] = "Moved Permanently"; 408 | http_codes[302] = "Found"; 409 | http_codes[303] = "See Other"; 410 | http_codes[304] = "Not Modified"; 411 | http_codes[305] = "Use Proxy"; 412 | http_codes[306] = "(Unused)"; 413 | http_codes[307] = "Temporary Redirect"; 414 | http_codes[400] = "Bad Request"; 415 | http_codes[401] = "Unauthorized"; 416 | http_codes[402] = "Payment Required"; 417 | http_codes[403] = "Forbidden"; 418 | http_codes[404] = "Not Found"; 419 | http_codes[405] = "Method Not Allowed"; 420 | http_codes[406] = "Not Acceptable"; 421 | http_codes[407] = "Proxy Authentication Required"; 422 | http_codes[408] = "Request Timeout"; 423 | http_codes[409] = "Conflict"; 424 | http_codes[410] = "Gone"; 425 | http_codes[411] = "Length Required"; 426 | http_codes[412] = "Precondition Failed"; 427 | http_codes[413] = "Request Entity Too Large"; 428 | http_codes[414] = "Request-URI Too Long"; 429 | http_codes[415] = "Unsupported Media Type"; 430 | http_codes[416] = "Requested Range Not Satisfiable"; 431 | http_codes[417] = "Expectation Failed"; 432 | http_codes[500] = "Internal Server Error"; 433 | http_codes[501] = "Not Implemented"; 434 | http_codes[502] = "Bad Gateway"; 435 | http_codes[503] = "Service Unavailable"; 436 | http_codes[504] = "Gateway Timeout"; 437 | http_codes[505] = "HTTP Version Not Supported"; 438 | } 439 | 440 | int main(int argc, char **argv) { 441 | time_interval wave_interval("wave"), report_interval("report"), start("start"); 442 | const char *wave_spec, *p; 443 | char ch; 444 | int num, stop_after, unique, wave_limit, no_wave_limit; 445 | rlim_t max_fds; 446 | strvec headers; 447 | 448 | num = debug = stop_after = unique = wave_limit = 0; 449 | no_wave_limit = 1; 450 | // default wave spec is just one wave 451 | wave_spec = "1000:1"; 452 | max_fds = 256; 453 | while ((ch = getopt(argc, argv, "un:t:r:df:m:h:")) != -1) { 454 | switch (ch) { 455 | case 'u': 456 | unique = 1; 457 | break; 458 | case 'n': 459 | num = atoi(optarg); 460 | break; 461 | case 't': 462 | wave_spec = optarg; 463 | break; 464 | case 'r': 465 | report_interval.set(atoi(optarg)*1000); 466 | break; 467 | case 'd': 468 | debug++; 469 | break; 470 | case 'f': 471 | max_fds = atoi(optarg); 472 | break; 473 | case 'm': 474 | stop_after = atoi(optarg); 475 | break; 476 | case 'h': 477 | headers.push_back(optarg); 478 | break; 479 | default: 480 | usage(); 481 | } 482 | } 483 | 484 | wave_interval.set(atoi(wave_spec)*1000); 485 | p = strchr(wave_spec, ':'); 486 | if (p != NULL) { 487 | wave_limit = atoi(p+1); 488 | no_wave_limit = 0; 489 | } 490 | 491 | if (report_interval.get() == 0) { 492 | report_interval.set(wave_interval.get()); 493 | } 494 | if (num == 0 || wave_interval.get() == 0 || report_interval.get() == 0) { 495 | usage(); 496 | } 497 | 498 | argc -= optind; 499 | argv += optind; 500 | if (argc < 1) 501 | usage(); 502 | 503 | urlvec urls; 504 | while (*argv) { 505 | url url(*argv++); 506 | urls.push_back(url); 507 | } 508 | url url = urls[0]; 509 | 510 | // Decide how many fds we can use. 511 | struct rlimit rlim; 512 | if (getrlimit(RLIMIT_NOFILE, &rlim) < 0) { 513 | perror("getrlimit"); 514 | exit(1); 515 | } 516 | rlim.rlim_cur = std::max(rlim.rlim_cur, max_fds); 517 | rlim.rlim_max = std::max(rlim.rlim_max, max_fds); 518 | if (setrlimit(RLIMIT_NOFILE, &rlim) < 0) { 519 | perror("setrlimit"); 520 | exit(1); 521 | } 522 | 523 | // Allocate/initialize various data structures. 524 | fds_len = rlim.rlim_cur; 525 | fds = (struct pollfd *)calloc(fds_len, sizeof(struct pollfd)); 526 | conn_info = (struct conn_info_t *)calloc(fds_len, sizeof(struct conn_info_t)); 527 | init_http_codes(); 528 | wave_stats.clear(); 529 | 530 | addrvec addrs; 531 | for (int i = 0; i < urls.size(); i++) { 532 | struct hostent *h = gethostbyname(urls[i].host().c_str()); 533 | if (h == NULL) { 534 | fprintf(stderr, "cannot resolve host: %s\n", urls[i].host().c_str()); 535 | exit(1); 536 | } 537 | 538 | struct sockaddr_in addr; 539 | memset(&addr, 0, sizeof(addr)); 540 | addr.sin_family = AF_INET; 541 | addr.sin_port = htons(url.port()); 542 | memcpy(&addr.sin_addr.s_addr, h->h_addr, h->h_length); 543 | addrs.push_back(addr); 544 | } 545 | 546 | int current_url = 0; 547 | 548 | // Mark time and kick off the first wave. 549 | struct timeval now; 550 | time_interval::gettod(&now); 551 | start.mark(&now); 552 | if (no_wave_limit || wave_limit-- > 0) { 553 | open_connections(num, addrs, current_url); 554 | } 555 | report_connections(&start); 556 | 557 | int wait_interval = std::min(wave_interval.get(), report_interval.get())/1000; 558 | while (1) { 559 | if (stop_after > 0) { 560 | struct timeval since; 561 | start.since(&since); 562 | if (since.tv_sec > stop_after) { 563 | break; 564 | } 565 | } 566 | 567 | int nfds = poll(fds, fds_len, wait_interval); 568 | if (nfds < 0) { 569 | perror("poll"); 570 | exit(1); 571 | } 572 | 573 | time_interval::gettod(&now); 574 | if (wave_interval.passed(&now)) { 575 | if (no_wave_limit || wave_limit-- > 0) { 576 | open_connections(num, addrs, current_url); 577 | } 578 | wave_interval.mark(&now); 579 | } 580 | 581 | if (report_interval.passed(&now)) { 582 | report_connections(&start); 583 | report_interval.mark(&now); 584 | } 585 | 586 | if (nfds == 0) { 587 | continue; 588 | } 589 | 590 | for (int i = 0; i < fds_len; ++i) { 591 | // Presumably a non-blocking connect error? 592 | if (fds[i].revents & POLLERR) { 593 | fds[i].revents &= ~POLLERR; 594 | 595 | int err = get_sock_error(i); 596 | wave_stats.connect[err]++; 597 | close_connection(i); 598 | if (debug) 599 | printf("fd %d err: %d\n", fds[i].fd, err); 600 | } 601 | 602 | // Non-blocking connect succeeded or failed. 603 | if (fds[i].revents & POLLOUT) { 604 | fds[i].revents &= ~POLLOUT; 605 | 606 | int err = get_sock_error(i); 607 | if (err == 0) { 608 | // Connect succeeded. Stop polling for write. 609 | fds[i].events &= ~POLLOUT; 610 | wave_stats.connected++; 611 | conn_info[i].state = CONN_ESTABLISHED; 612 | time_interval::gettod(&conn_info[i].connected); 613 | 614 | // For now, use blocking IO. 615 | setblocking(fds[i].fd); 616 | if (debug) 617 | printf("fd %d: connect\n", fds[i].fd); 618 | 619 | struct timeval diff; 620 | time_interval::timeval_subtract(&diff, &conn_info[i].connected, &conn_info[i].connecting); 621 | time_t delta = diff.tv_sec*1000000+diff.tv_usec; 622 | if (delta > 1000000) { 623 | printf("%d connect time: %lu\n", conn_info[i].request_number, delta); 624 | } 625 | 626 | // Build the URL and request headers. 627 | char request[8192]; 628 | int found_ua = 0, found_host = 0; 629 | sprintf(request, "GET %s", urls[conn_info[i].url_number].request().c_str()); 630 | if (unique) { 631 | sprintf(request+strlen(request), "&cnt=%d", conn_info[i].request_number); 632 | } 633 | strcat(request, " HTTP/1.0\r\n"); 634 | for (strvec::iterator it = headers.begin(); it != headers.end(); it++) { 635 | strcat(request, it->c_str()); 636 | strcat(request, "\r\n"); 637 | if (strcasestr(it->c_str(), "host:") != NULL) { 638 | found_host = 1; 639 | } 640 | if (strcasestr(it->c_str(), "user-agent:") != NULL) { 641 | found_ua = 1; 642 | } 643 | } 644 | if (! found_host) { 645 | strcat(request, "Host: "); 646 | strcat(request, urls[conn_info[i].url_number].host().c_str()); 647 | strcat(request, "\r\n"); 648 | } 649 | if (! found_ua) { 650 | strcat(request, "User-Agent: Goofy 0.0\r\n"); 651 | } 652 | strcat(request, "\r\n"); 653 | if (debug) 654 | printf("%s", request); 655 | 656 | int request_len = strlen(request); 657 | 658 | // Send the request. 659 | if (write(fds[i].fd, request, request_len) != request_len) { 660 | // We can't write the request to the socket, give up. 661 | wave_stats.write[errno]++; 662 | close_connection(i); 663 | if (debug) 664 | printf("fd %d: write err: %d\n", fds[i].fd, err); 665 | } 666 | } 667 | else { 668 | // Connect failed. 669 | wave_stats.connect[err]++; 670 | close_connection(i); 671 | if (debug) 672 | printf("fd %d: connect err: %d\n", fds[i].fd, err); 673 | } 674 | } 675 | 676 | // Data available. Read just once so we never block. If there is 677 | // more data, we'll get it next time. 678 | if (fds[i].revents & POLLIN) { 679 | fds[i].revents &= ~POLLIN; 680 | 681 | char buf[8192]; 682 | int n = read(fds[i].fd, buf, sizeof(buf)); 683 | if (n < 0) { 684 | wave_stats.read[errno]++; 685 | close_connection(i); 686 | if (debug) 687 | printf("fd %d read err: %d\n", fds[i].fd, errno); 688 | continue; 689 | } 690 | else if (n == 0) { 691 | // No data means peer closed the connection. 692 | if (debug) 693 | printf("fd %d empty read\n", fds[i].fd); 694 | close_connection(i); 695 | } 696 | else { 697 | buf[n-1] = 0; 698 | if (debug > 1) 699 | printf("fd %d read: %s\n", fds[i].fd, buf); 700 | if (strstr(buf, "HTTP/1.") == buf) { 701 | wave_stats.http_code[atoi(buf+9)]++; 702 | } 703 | } 704 | } 705 | 706 | // Peer closed the connection. 707 | if (fds[i].revents & POLLHUP) { 708 | fds[i].revents &= ~POLLHUP; 709 | if (debug) 710 | printf("fd %d closed\n", fds[i].fd); 711 | close_connection(i); 712 | } 713 | 714 | // Anything left in revents is unexpected. 715 | if (fds[i].revents) { 716 | printf("fd %d: 0x%x\n", fds[i].fd, fds[i].revents); 717 | } 718 | } 719 | } 720 | 721 | return 0; 722 | } 723 | --------------------------------------------------------------------------------