1

I am trying to time how long it takes to make concurrent requests. My timing results are about four times slower than what ab2 reports.

I've tried timing requests in two different ways, both of which lead to similar results (which are far off ab2's results). To give an example, ab2 will report the longest request to last 2 milliseconds on a local server, whereas this code will report up to 4.5 milliseconds. By the way, the whole codebase is available here.

How do I properly time a request in go?

Method 1: timing includes more than just the request

From this commit.

// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
    go func(no int, greq goreq.Request) {
        r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
        startTime := time.Now()
        greq.Uri = r.URL.Generate()
        gresp, err := greq.Do()
        if err != nil {
            log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
        } else if no < r.Repeat {
            // We're always using the last response for the next batch of requests.
            gresp.Body.Close()
        }
        <-r.ongoingReqs // We're done, let's make room for the next request.
        resp := Response{Response: gresp, duration: time.Now().Sub(startTime)}
        // Let's add that request to the list of completed requests.
        r.doneChan <- &resp
        runtime.Gosched()
    }(rno, greq)
}

Method 2: using an internal function with a defer statement

From this commit.

// Let's spawn all the requests, with their respective concurrency.
wg.Add(r.Repeat)
r.doneWg.Add(r.Repeat)
for rno := 1; rno <= r.Repeat; rno++ {
    go func(no int, greq goreq.Request) {
        r.ongoingReqs <- struct{}{} // Adding sentinel value to limit concurrency.
        greq.Uri = r.URL.Generate()
        var duration time.Duration
        gresp, err := func(dur *time.Duration) (gresp *goreq.Response, err error) {
            defer func(startTime time.Time) { *dur = time.Now().Sub(startTime) }(time.Now())
            return greq.Do()
        }(&duration)

        if err != nil {
            log.Critical("could not send request to #%d %s: %s", no, r.URL, err)
        } else if no < r.Repeat {
            // We're always using the last response for the next batch of requests.
            gresp.Body.Close()
        }
        <-r.ongoingReqs // We're done, let's make room for the next request.
        resp := Response{Response: gresp, duration: duration}
        // Let's add that request to the list of completed requests.
        r.doneChan <- &resp
        runtime.Gosched()
    }(rno, greq)
}

I had a look at this question, which did not help.

Community
  • 1
  • 1
ChrisR
  • 966
  • 14
  • 20
  • If you're trying to create a benchmark, adding another layer over the http package with "goreq" seems counterproductive. Try with the standard http package first, then profile if there's still time unaccounted for. A number of http load generators have been written in Go with good results. – JimB Sep 25 '15 at 12:46
  • Does the server return *any* content at all in the response body? – JimB Sep 25 '15 at 14:03
  • @JimB, yes the server may return content. And this load generator can actually make use of returned data for subsequent requests. – ChrisR Sep 25 '15 at 14:05
  • I'm not seeing where you're reading the Body, but if you don't read that content and close the body as soon as possible, that connection is blocked from making more requests, requiring you to create new connections. (this is obscured somewhat by the extra goreq package too) – JimB Sep 25 '15 at 14:15
  • To be clear, reading the body is part of the response time (unless you're defining response time to only be the receipt of headers). – JimB Sep 25 '15 at 14:19
  • Okay, that makes sense, I had no idea. I'll definitely have to look at that. Right now, the code will assign the responses to a list of responses, and for the subsequent request, it will grab the last body value and read that. Now that you mention that more connections are needed, I understand why the number of open file handlers available for the program exhaust so quickly. – ChrisR Sep 25 '15 at 14:40
  • @JimB, if I understand correctly, I should not close the body but instead somehow discard it (if I'm not reading anything from it), is that correct? If so, should I store all the data I need from the response prior to moving onto the next one, and let the GC discard the actual goreq response? – ChrisR Sep 25 '15 at 16:59
  • 1
    You need to read the full response *and* close the body as quickly as possible. The request isn't complete until you read the entire response. I'm not sure to answer the second part, if you want the body later you need to store it somewhere. – JimB Sep 25 '15 at 19:45

1 Answers1

1

As your goroutines enter syscall (writing to a socket), they are preempted. This means that they are interrupted and another goroutine will run in their place. Eventually, your preempted goroutine will be scheduled again and it will continue running where it left off. This doesn't necessarily happen exactly after the syscall is done, though.

Timing in a goroutine is difficult, because even if you did everything serially, Go 1.5's garbage collector will run occasionally, interrupting your theoretical serial loop.

The only real solution is a bit complex:

  1. Use RawSyscall directly.
  2. Annotate your function with //go:nosplit to prevent it from being preempted.
  3. Disable garbage collector.

Even then, I might be forgetting something.

thwd
  • 23,956
  • 8
  • 74
  • 108
  • Thanks for your quick answer. I can't find much doc on `//go:nosplit`, where should I put that? I get a link time stack overflow error if I put it above the `go func` instructions and even above the `func (r *Request) Spawn` function. Any thoughts? – ChrisR Sep 25 '15 at 12:08
  • 1
    It's documented [here](https://golang.org/cmd/compile/). I think it only works for named functions. I've requested a peer-review for this answer on golang-nuts. Maybe someone comes up with something smarter. – thwd Sep 25 '15 at 12:24
  • What do you intend to use RawSyscall for? Rewriting all the network IO, which is an area where a lot of performance tuning has already been done? This is trying to make http requests, which is a fairly complex process, not a single syscall. – JimB Sep 25 '15 at 12:51
  • @JimB based on [this question](http://stackoverflow.com/questions/16977988/details-of-syscall-rawsyscall-syscall-syscall-in-go) it seems to make sense. – thwd Sep 25 '15 at 13:24
  • I still don't see how that applies. If you rewrite the net package to somehow only use RawSyscall, the runtime won't be able to schedule concurrent IO in the same thread. Your timing may be more deterministic, but you would have to pin every request to it's own thread to prevent blocking. You might as well just use `runtime.LockOSThread` and and use blocking IO directly on your own custom net.Conn. – JimB Sep 25 '15 at 13:33
  • @JimB: Doing blocking IO can still preempt the goroutine, unless you use RawSyscall, AFAIK. Locking on to a thread makes sense. – thwd Sep 25 '15 at 13:37
  • I think this is still jumping the gun, since there's no indication that the original problem has anything to do with the runtime. It's more likely something like unnecessary tcp dialing, request rate spikes at the server, pressure on the keepalive pool, not reading a response body, etc. I've had no problem timing sub millisecond events in highly concurrent Go network systems. – JimB Sep 25 '15 at 13:44