bblanchon / ArduinoJson

📟 JSON library for Arduino and embedded C++. Simple and efficient.
https://arduinojson.org
MIT License
6.71k stars 1.12k forks source link

`SerializeJson` sends many 1 bytes packets with `EthernetClient` (Massive Slowdown!) #1476

Closed EmperorArthur closed 3 years ago

EmperorArthur commented 3 years ago

No Buffering

The issue is that does not buffer writes. This may be an issue on there end, but was possibly a deliberate decision to decrease latency, or simplify code. This means that even though the Wiznet chip has a 2K buffer, the example code below sends 101 packets!

While some of these issues are hard to solve (floats and dubles are sent as two packets), at the least it should be possible to send C strings as single packets.

This matters both from an overhead perspective, and because I have observed that large packet counts can significantly increase response time.

How to measure

Target platform and Compiler information:

Same issue on the latest Arduino IDE.

Minimal Example:

#include <ArduinoJson.h>
#include <Ethernet.h>

#define IP_ADDRESS IPAddress(192,168,1,250)
byte MAC_ADDRESS[] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
#define DNS_ADDRESS IPAddress(1,1,1,1)
#define GATEWAY_ADDRESS IPAddress(192,168,1,1)

auto http_server = EthernetServer(80);

void setup() {
  EthernetClass::begin(MAC_ADDRESS, IP_ADDRESS);
  http_server.begin();
}

void loop() {
  EthernetClient client = http_server.available();
    if (!client) {
      return;
    }
    StaticJsonDocument<500> document;
    document["C String"] = "This is a C String.";
    document["String"] = String("This is a String.");
    document["int"] = static_cast<int>(123);
    document["float"] = static_cast<float>(0.456);
    document["double"] = static_cast<float>(0.789);

    auto out = String(F("HTTP/1.0 200 OK\n"
                             "Content-Type: application/json\n"
                             "Connection: close\n"
                             "Content-Length: "));
    out += measureJsonPretty(document);
    out += "\n\n";
    //serializeJsonPretty(document, out);
    client.print(out);

    serializeJsonPretty(document, client);
    client.stop();
}
EmperorArthur commented 3 years ago

More Information:

Switching to the commented serializeJsonPretty drops the packet count to 1, and reduces the response time from 33ms to 7ms!

This is obviously not practical for anything that does not have the 8k of RAM the Controllino does, but demonstrates just what that overhead means.

EmperorArthur commented 3 years ago

For those who are also in the position of having lots of ram here's a work around.

It first determines if there is enough ram to buffer everything, then does so if possible.

    bool use_buffer = false;
    auto buffer = String();
    void *tmp = malloc(measureJsonPretty(document) + 128);  //Allow some overhead
    if (tmp != nullptr) {
        use_buffer = true;
    }
    free(tmp);
    if (use_buffer) {
        serializeJsonPretty(document, buffer);
        client.print(buffer);
    } else {
        serializeJsonPretty(document, client);
    }

In more complex structures, using this code, I am seeing a reduction from 327ms to 37ms! With a few API calls that's the difference between barely noticeable and horribly slow.

bblanchon commented 3 years ago

Hi @EmperorArthur,

Thank you for this detailed report.

This is a well-known problem that is already covered in the documentation. Please see: How to improve (de)serialization speed?

Best regards, Benoit

EmperorArthur commented 3 years ago

Okay. I see the problem. Part of it is that with such a powerful processor I am able to just ignore or brute force my way through some of the problems that would be issues, like RAM usage. That 327ms JSON is over 1024kB and, everything else is so small that tens of ms are not noticeable. Especially when the sensors take that long to obtain a reading anyways.

What caught me is that I checked the FAQ when speed became an issue, but the issue was in the HowTo section and the problems page. I would really recommend merging the problems and FAQ into one page. Going through a bit more research it looks like the slow question was on the V5 FAQ, but when you moved to V6 it was split.

In addition, I found this project through PlatformIO's library search, which provides basic examples right there.* The basic example and your serialization howto make no mention of buffered streams. Your howto does mention buffers, but only static buffers and does not warn about the dangers of streams.

TL;DR: I opened the issue because I guess I wasn't looking in the right place.

Ah well, if anyone else is in the same boat I am, I have found that malloc pattern above works really well. I'll often use it with a while loop that repeatedly cuts the buffer size in half until it succeeds so the fallback isn't as terrible as what I've written here.

* I did check the site out and read the part about possible memory issues.

bblanchon commented 3 years ago

Indeed, I recently moved many questions out of the FAQ because it became messy. I'm sorry you missed the page you were looking for.

I removed even more questions out of the FAQ and added links to the "How-To's" and "Common errors and problems" pages; hopefully, it will help future readers find the answers more easily.

@EmperorArthur, I wish more people would do like you and tell me how they couldn't find the information. Thank you very much for helping me improve the documentation.

EmperorArthur commented 3 years ago

@bblanchon It's hard to get why people wouldn't do that.

Even from a purely selfish perspective, the better the software is, the more likely people are going to use it. The more people who use the software the more likely it is to get better and not be abandoned. It's a win-win.