1

Currently, I am using websockets to send an image to my server, process it, and then send it back. Specifically, I am using Ruby with Sinatra and sinatra-websocket.

On my development server, it takes ~2 seconds to send an image to the server and retrieving the exact image without processing.

On an AWS-EC2 instance, this takes ~15 seconds. The image file I am sending is ~500kb. My upload and download speeds are well above that.

How can I speed up this process? Is this a naive way of sending images back and forth?

Edit: To replicate my issue, you can clone and run my repo in an AWS-EC2 free tier instance.

Mark
  • 11
  • 2
  • I'm not sure which servers you're using... what are the differences between your development server and the EC2 instance? ... what is the routing stack (before the app server) ? ... also ... I suspect that even if you used Ruby with a native C Websocket server (such as the `iodine` gem)... you would still experience slowdowns that relate to your application logic and the amount of processing it requires for routing, concurrent loads, etc'. – Myst Apr 15 '17 at 03:18
  • My development server is local. So, the speed is not a surprise since network latency doesn't exist. As far as my application logic goes, this isolated case simply does the following: client -> sends ~400kb 64base encoded img to server -> thin server receives the image -> server sends it right back without any processing. – Mark Apr 15 '17 at 08:37

1 Answers1

0

This is more or a testing helper than an answer, since the question doesn't provide enough details to narrow the issue down.


I wrote a simple test application using Plezi. Since I'm plezi's author it was easier for me than to learn your stack.

It works perfectly on my computer, 79ms for a ~3Mb file.

The ~3Mb roundtrip to Heroku took my system 3 seconds and went down to ~2.5 seconds after the TCP/IP warmup was complete...

...but my internet speeds are probably effecting the test (my reception is low at the moment, so I might be slow).

I'm not sure I can replicate the issue, but you can use the code in this answer to test your server.

If the roundtrip still takes longer than 10 seconds, it might be the EC2 stack. I don't think 10 seconds would be reasonable for ~500Kb.

On the other hand, if it's a shorter roundtrip, it might be either the way you tested your application or your Ruby stack... in which case, maybe the solution is to switch to plezi (or the iodine native websocket design).

You can paste the following code into config.ru (remember you'll also need a gem file with the plezi gem and possibly a Gemfile.lock):

# The roundtrip html client
ROUNDTRIP_CLIENT = <<CLIENT_EFO
<html>
  <head>
    <script src = '/client.js'></script>
  </head>
  <body>
  <input type='file' id='test_f' lable='file to upload'></input>
  <button id='test_b'>run test</button>
  <div id='output'></div>
  <script>
  var client;
  window.onload = function (e) {
    client = new PleziClient();
    client.autoreconnect = true;
    client.roundtrip = (e) => {
      var d = new Date();
      e.completed_at = d.getTime();
      console.log(e);
      document.getElementById('output').innerHTML += "<p>Test for " +
        "<a href='" + e.data + "' target='_blank'>" + Math.round(e.data.length / 1024)+ "Kb encoded file</a>" +
        " completed in " + (e.completed_at - e.time) + "ms</p>";
    }
    client.onopen = (e) => console.log("Websocket client open", e);
  }

  function run_test(e) {
    console.log("File submitted.");
    reader = new FileReader();
    reader.onloadend = function(e)
    {
      console.log("File loaded, " + e.target.result.length + "bytes... starting test.")
      var d = new Date();
      client.emit({event: "roundtrip", data: e.target.result, time: d.getTime() });
    }
    reader.readAsDataURL(document.getElementById('test_f').files[0]);
    return false;
  }
  document.getElementById('test_b').onclick = run_test;
  </script>
  </body>
</html>
CLIENT_EFO

# require plezi
require 'plezi'
# For security, Iodine limists websocket messages.
# We update the default limit from ~250Kb to ~4Mb.
# This replaces the commandline option: iodine -v -maxms 4194304
Iodine::Rack.max_msg_size = 4194304

# the roundtrip controller... quite simple.
class RoundTrip
  # return the roundtrip client.
  def index
    ROUNDTRIP_CLIENT
  end
  # echo back the websocket message - we're just testing the round trip.
  def on_message data
    write data
  end
end
# Set the plezi root route to the RoundTrip controller
Plezi.route '/', RoundTrip
# Set the client javascript route - I'm using it as a heler.
Plezi.route '/client.js', :client
# Set Rack to run the Plezi application
run Plezi.app

To run the code from the terminal, use the iodine command (it will start the iodine server, which Plezi requires.


EDIT

From the link to the git-repo (in the comments), I realized that the JSON is parsed by the server and then it is re-emitted.

To emulate this, I updated the example code.

This should be similar to what the code in the repo seems to do and it adds some time to the roundtrip, since the JSON parsing and re-formatting create copies of the data, which requires memory allocation as well as CPU time.

The only change in the code is in the RoundTrip controller class, but I'm pasting the whole thing for your copy+paste convenience.

Place the following code in your app.rb file (remember to edit install.sh to install the plezi gem):

# The roundtrip html client
ROUNDTRIP_CLIENT = <<CLIENT_EFO
<html>
  <head>
    <script src = '/client.js'></script>
  </head>
  <body>
  <input type='file' id='test_f' lable='file to upload'></input>
  <button id='test_b'>run test</button>
  <div id='output'></div>
  <script>
  var client;
  window.onload = function (e) {
    client = new PleziClient();
    client.autoreconnect = true;
    client.roundtrip = (e) => {
      var d = new Date();
      e.completed_at = d.getTime();
      console.log(e);
      document.getElementById('output').innerHTML += "<p>Test for " +
        "<a href='" + e.data + "' target='_blank'>" + Math.round(e.data.length / 1024)+ "Kb encoded file</a>" +
        " completed in " + (e.completed_at - e.time) + "ms</p>";
    }
    client.onopen = (e) => console.log("Websocket client open", e);
  }

  function run_test(e) {
    console.log("File submitted.");
    reader = new FileReader();
    reader.onloadend = function(e)
    {
      console.log("File loaded, " + e.target.result.length + "bytes... starting test.")
      var d = new Date();
      client.emit({event: "roundtrip", data: e.target.result, time: d.getTime() });
    }
    reader.readAsDataURL(document.getElementById('test_f').files[0]);
    return false;
  }
  document.getElementById('test_b').onclick = run_test;
  </script>
  </body>
</html>
CLIENT_EFO

# require plezi
require 'plezi'
# For security, Iodine limists websocket messages.
# We update the default limit from ~250Kb to ~4Mb.
# This replaces the commandline option: iodine -v -maxms 4194304
Iodine::Rack.max_msg_size = 4194304

# the roundtirp controller... quite simple.
class RoundTrip
  @auto_dispatch = true
  # return the roundtrip client.
  def index
    ROUNDTRIP_CLIENT
  end
  # Using Auto-Dispatch, the JSON is parsed and this event is invoked.
  def roundtrip msg
    # Hash results are automatically converted into JSON and emitted
    msg
  end
end
# Set the plezi root route to the RoundTrip controller
Plezi.route '/', RoundTrip
# Set the client javascript route - I'm using it as a heler.
Plezi.route '/client.js', :client
# Plezi will start automatically when the script exits

To run the code from the terminal, use the ruby app.rb command, same as your repo does for the existing app.

While the old code simply offered and "echo" response, the new code (which looks almost the same) has a few more steps:

  • Using Auto-Dispatch, the Plezi framework now automatically parses the JSON and routes the event ("roundtrip") the the controller's method (roundtrip).

  • The method receives a Hash with the parsed data and returns that Hash back to Plezi.

  • The framework collects the Hash, formats a JSON object and emits back the result (non String or Hash results are ignored)...

... this is similar to the repo's behavior.

Myst
  • 18,516
  • 2
  • 45
  • 67
  • Hey Myst, I'll definitely check Plezi out and test your code. If you're interested in replicating my issue, you can create an AWS-EC2 (free tier) and clone my [repo](https://github.com/markediez/ecs193ab). – Mark Apr 18 '17 at 17:42
  • @Mark , I noticed the code in the repo parses and re-formats the JSON object. I edited my answer to offer JSON parsing as part of the flow. It slows things down by ~20ms on my machine. – Myst Apr 19 '17 at 03:25