Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fuel is ~22x slower than java.net.http.HttpClient #667

Closed
jsyeo opened this issue Aug 6, 2019 · 5 comments
Closed

fuel is ~22x slower than java.net.http.HttpClient #667

jsyeo opened this issue Aug 6, 2019 · 5 comments
Labels
🐛 bug Something isn't working

Comments

@jsyeo
Copy link

jsyeo commented Aug 6, 2019

Fuel seems to be very slow when sending a sending a request in its body. This is even more pronounced when the payload gets bigger (> 1MB). When benchmarked against the java.net.http.HttpClient, I noticed that fuel is 24 times slower than the http client that came with Java 11. This is the code I used to do a simple benchmark.

import com.github.kittinunf.fuel.httpPost
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.nio.file.Files
import java.nio.file.Paths

fun main() {
    val payload = Files.readString(Paths.get("large.json"))
    val url = "http://localhost:4567/lol"

    // fuel client
    val t = dumbBenchmark {
      url.httpPost().body(payload).response()
    }
    println("fuel client took $t ms on average")

    // java.net.http.HttpClient
    val client = HttpClient.newHttpClient()
    val req = HttpRequest.newBuilder()
        .uri(URI(url))
        .POST(HttpRequest.BodyPublishers.ofString(payload))
        .setHeader("Content-Type", "application/json")
        .build()

    val t2 = dumbBenchmark {
        client.send(req, HttpResponse.BodyHandlers.ofString())
    }
    println("httpclient took $t2 ms on average")
}

// Runs the lambda five times, time each invocation, and return the average  
fun dumbBenchmark(f: () -> Unit): Double = (1..5).map {
    val start = System.currentTimeMillis()
    f.invoke()
    val end = System.currentTimeMillis()
    end - start
}.average()

Here I am running a local sinatra server at localhost:4567, the server simply returns 200 OK for any kind of input. When I send a payload of ~2.2MB I get these results:

fuel client took 7488.2 ms on average
httpclient took 330.2 ms on average

I suspect this is due to the bufferring of the ProgressOutputStream in HttpClient.kt. I find it odd that even on a request that do not need progress reporting, fuel would still buffer the output stream. We should probably not buffer when a progress callback is not given to the request.

jsyeo added a commit to jsyeo/fuel that referenced this issue Aug 6, 2019
@SleeplessByte SleeplessByte added the 🐛 bug Something isn't working label Aug 6, 2019
@SleeplessByte
Copy link
Collaborator

What are the results of jsyeo@04d6b25?

@jsyeo
Copy link
Author

jsyeo commented Aug 7, 2019

fuel client took 528.2 ms on average
httpclient took 427.8 ms on average

Here you go!

@tfactor2
Copy link

@jsyeo I've experienced similar issue on v.2.2.0 but didn't notice this bug report - therefore did my own investigation.
The original problem stated in description will not be solved by the fix - it's a workaround - if I still need a progress callback the performance drop will be huge.

The problem is located in the ProgressOutputStream that is inherited from java.io.FilterOutputStream. The FilterOutputStream has very inefficient implementation of write(byte[] b, int off, int len) that instead of calling the wrapped OutputStream write(byte[] b, int off, int len) calls write(int b) multiple times (actually number of bytes of the body, for 1M file it will be 1M times!).
SocketOutputStream used by HttpUrlConnection in turn supports batch writing.

So the fix could be replacing the call of super.write(b, off, len) with out.write(b, off, len) in ProgressOutputStream:

class ProgressOutputStream(stream: OutputStream, val onProgress: WriteProgress) : FilterOutputStream(stream) {
    override fun write(b: ByteArray?, off: Int, len: Int) {
        out.write(b, off, len) // 'out' instead of 'super'
...

@SleeplessByte , wdyt?

@SleeplessByte
Copy link
Collaborator

SleeplessByte commented Aug 13, 2019 via email

@tfactor2
Copy link

could it be done (pull request created) in scope of this issue?

tfactor2 pushed a commit to tfactor2/fuel that referenced this issue Aug 13, 2019
SleeplessByte pushed a commit that referenced this issue Aug 13, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛 bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants