This is an archive of the discontinued LLVM Phabricator instance.

Reduce extraneous temp strings in debugserver, free objects when they're not longer needed
ClosedPublic

Authored by jasonmolenda on Mar 31 2022, 1:23 PM.

Details

Summary

Looking at debugserver's memory use a bit, I noticed that the SendPacket method which takes a std::string& argument, is often called with a std::string object that the caller calls c_str() on, so a temporary copy of the string is created. This patch removes those c_str() method calls.

We also have a common pattern where we have/create a JSONGenerator::ObjectSP, dump the string representation of the object into a ostringstream, and then binary escape the ostringstream. We end up with the original ObjectSP dictionary, the ostringstream print of it, and the escaped std::string copy of it. Then we pass that to SendPacket which may create a compressed std::string of it. This patch frees those objects as soon as they are no longer needed, so our peak memory use is limited to a narrower window.

In the future I want to add a JSONGenerator::DumpEscaped() method that formats its output following the binary escape protocol. Every packet that sends JSON responses must escape it before sending, so there's no point in even generating the un-escaped version in the first place. It forces us to always have two copies of the string in heap for at least a little while.

Diff Detail

Event Timeline

jasonmolenda created this revision.Mar 31 2022, 1:23 PM
Herald added a project: Restricted Project. · View Herald TranscriptMar 31 2022, 1:23 PM
jasonmolenda requested review of this revision.Mar 31 2022, 1:23 PM

If debugserver linked against libSupport we could have saved the additional copy altogether by using llvm::raw_string_ostream:

std::string str;
llvm::raw_string_ostream stream(str);
stream.str() // Flushes and returns a reference to the stack allocated str

Barring that, I wonder if if a little wrapper around std::ostringstream could improve readability and avoid bugs where someone forgets to call stream.str("").

class AggressiveStream {
public:
  AggressiveStream() : m_stream(std::make_unique<std::ostringstream>()) {}

  std::ostringstream &operator*() {
    assert(m_stream && "cannot use stream after having called str()");
    return *m_stream;
  }

  std::string str() {
    std::string s = m_stream->str();
    m_stream.reset();
    return std::move(s);
  }

private:
  std::unique_ptr<std::ostringstream> m_stream;
};

WDYT?

lldb/tools/debugserver/source/RNBRemote.cpp
588

clear doesn't do what you think it does, it modifies the state flag which isn't relevant here.

jasonmolenda added inline comments.Mar 31 2022, 2:51 PM
lldb/tools/debugserver/source/RNBRemote.cpp
588

Yeah I know, the common idiom for clearing an ostringstream is to .str("") an then .clear() to clear flag state, I was only clear'ing to be proper, even though we don't use the object past this point.

If debugserver linked against libSupport we could have saved the additional copy altogether by using llvm::raw_string_ostream:

std::string str;
llvm::raw_string_ostream stream(str);
stream.str() // Flushes and returns a reference to the stack allocated str

Barring that, I wonder if if a little wrapper around std::ostringstream could improve readability and avoid bugs where someone forgets to call stream.str("").

class AggressiveStream {
public:
  AggressiveStream() : m_stream(std::make_unique<std::ostringstream>()) {}

  std::ostringstream &operator*() {
    assert(m_stream && "cannot use stream after having called str()");
    return *m_stream;
  }

  std::string str() {
    std::string s = m_stream->str();
    m_stream.reset();
    return std::move(s);
  }

private:
  std::unique_ptr<std::ostringstream> m_stream;
};

WDYT?

That's a cool idea, and certainly less error prone than this by-hand method I did, but I think I'd rather fix JSONGenerator::Dump to return quoted strings so we don't need to immediately copy the entire string into another string for quoting. I don't feel strongly about it tho. I might be too optimistic that I'm going to fix Dump and it would be good to adopt a safer wrapper around ostringsream.

I was chatting with Jonas about this a bit. The part of this patch which fixes all of the SendPacket(outbuf.c_str()) calls, to avoid a temporary copy of the std::string, is clearly necessary and correct. The changes to clear the ostringstreams eagerly after we've escaped the data is noisy, and I'll remove those and look at updating JSONGenerator to print escaped string representations directly, to eliminate the unescaped copy & the escaped copy duplication. I'll update the patch in a bit.

Address Jonas' feedback by removing the parts of the patch that freed the ostringstream's after binary-escaped versions of them had been generated. Instead I'll add DumpEscaped methods to the JSONGenerator class.

This revision was not accepted when it landed; it landed in state Needs Review.Mar 31 2022, 11:46 PM
This revision was automatically updated to reflect the committed changes.