[clangd] Trim memory periodically when using glibc malloc

This diff addresses the issue of the ever increasing memory usage of clangd. The key to understand what happens is to use `malloc_stats()`: malloc arenas keep getting bigger, although the actual memory used does not. It seems some operations while bulding the indices (both dynamic and background) create this problem. Specifically, 'FileSymbols::update' and 'FileSymbols::buildIndex' seem especially affected.

This diff adds a call to `malloc_trim()` periodically in
ClangdLSPServer.

Fixes: https://github.com/clangd/clangd/issues/251
Fixes: https://github.com/clangd/clangd/issues/115

Reviewed By: sammccall

Differential Revision: https://reviews.llvm.org/D93452
This commit is contained in:
Quentin Chateau 2020-12-22 08:44:20 +01:00 committed by Sam McCall
parent f72c384b5b
commit b8c37153d5
5 changed files with 73 additions and 2 deletions

View File

@ -14,9 +14,12 @@ if (NOT DEFINED CLANGD_BUILD_XPC)
unset(CLANGD_BUILD_XPC_DEFAULT)
endif ()
option(CLANGD_MALLOC_TRIM "Call malloc_trim(3) periodically in Clangd. (only takes effect when using glibc)" ON)
llvm_canonicalize_cmake_booleans(
CLANGD_BUILD_XPC
CLANGD_ENABLE_REMOTE
CLANGD_MALLOC_TRIM
LLVM_ENABLE_ZLIB
)

View File

@ -178,6 +178,7 @@ public:
} else if (auto Handler = Notifications.lookup(Method)) {
Handler(std::move(Params));
Server.maybeExportMemoryProfile();
Server.maybeCleanupMemory();
} else {
log("unhandled notification {0}", Method);
}
@ -453,6 +454,7 @@ void ClangdLSPServer::callRaw(StringRef Method, llvm::json::Value Params,
void ClangdLSPServer::notify(llvm::StringRef Method, llvm::json::Value Params) {
log("--> {0}", Method);
maybeCleanupMemory();
std::lock_guard<std::mutex> Lock(TranspWriter);
Transp.notify(Method, std::move(Params));
}
@ -1301,6 +1303,27 @@ void ClangdLSPServer::maybeExportMemoryProfile() {
NextProfileTime = Now + ProfileInterval;
}
void ClangdLSPServer::maybeCleanupMemory() {
// Memory cleanup is probably expensive, throttle it
static constexpr auto MemoryCleanupInterval = std::chrono::minutes(1);
if (!Opts.MemoryCleanup)
return;
// FIXME: this can probably be done without a mutex
// and the logic could be shared with maybeExportMemoryProfile
{
auto Now = std::chrono::steady_clock::now();
std::lock_guard<std::mutex> Lock(NextMemoryCleanupTimeMutex);
if (Now < NextMemoryCleanupTime)
return;
NextMemoryCleanupTime = Now + MemoryCleanupInterval;
}
vlog("Calling memory cleanup callback");
Opts.MemoryCleanup();
}
// FIXME: This function needs to be properly tested.
void ClangdLSPServer::onChangeConfiguration(
const DidChangeConfigurationParams &Params) {
@ -1507,8 +1530,9 @@ ClangdLSPServer::ClangdLSPServer(class Transport &Transp,
MsgHandler->bind("textDocument/foldingRange", &ClangdLSPServer::onFoldingRange);
// clang-format on
// Delay first profile until we've finished warming up.
NextProfileTime = std::chrono::steady_clock::now() + std::chrono::minutes(1);
// Delay first profile and memory cleanup until we've finished warming up.
NextMemoryCleanupTime = NextProfileTime =
std::chrono::steady_clock::now() + std::chrono::minutes(1);
}
ClangdLSPServer::~ClangdLSPServer() {
@ -1621,6 +1645,10 @@ void ClangdLSPServer::onDiagnosticsReady(PathRef File, llvm::StringRef Version,
void ClangdLSPServer::onBackgroundIndexProgress(
const BackgroundQueue::Stats &Stats) {
static const char ProgressToken[] = "backgroundIndexProgress";
// The background index did some work, maybe we need to cleanup
maybeCleanupMemory();
std::lock_guard<std::mutex> Lock(BackgroundIndexProgressMutex);
auto NotifyProgress = [this](const BackgroundQueue::Stats &Stats) {

View File

@ -48,6 +48,9 @@ public:
llvm::Optional<Path> CompileCommandsDir;
/// The offset-encoding to use, or None to negotiate it over LSP.
llvm::Optional<OffsetEncoding> Encoding;
/// If set, periodically called to release memory.
/// Consider malloc_trim(3)
std::function<void()> MemoryCleanup = nullptr;
/// Per-feature options. Generally ClangdServer lets these vary
/// per-request, but LSP allows limited/no customizations.
@ -184,10 +187,18 @@ private:
/// profiling hasn't happened recently.
void maybeExportMemoryProfile();
/// Run the MemoryCleanup callback if it's time.
/// This method is thread safe.
void maybeCleanupMemory();
/// Timepoint until which profiling is off. It is used to throttle profiling
/// requests.
std::chrono::steady_clock::time_point NextProfileTime;
/// Next time we want to call the MemoryCleanup callback.
std::mutex NextMemoryCleanupTimeMutex;
std::chrono::steady_clock::time_point NextMemoryCleanupTime;
/// Since initialization of CDBs and ClangdServer is done lazily, the
/// following context captures the one used while creating ClangdLSPServer and
/// passes it to above mentioned object instances to make sure they share the

View File

@ -1,2 +1,3 @@
#define CLANGD_BUILD_XPC @CLANGD_BUILD_XPC@
#define CLANGD_ENABLE_REMOTE @CLANGD_ENABLE_REMOTE@
#define CLANGD_MALLOC_TRIM @CLANGD_MALLOC_TRIM@

View File

@ -50,6 +50,10 @@
#include <unistd.h>
#endif
#ifdef __GLIBC__
#include <malloc.h>
#endif
namespace clang {
namespace clangd {
@ -497,6 +501,29 @@ opt<bool> CollectMainFileRefs{
init(ClangdServer::Options().CollectMainFileRefs),
};
#if defined(__GLIBC__) && CLANGD_MALLOC_TRIM
opt<bool> EnableMallocTrim{
"malloc-trim",
cat(Misc),
desc("Release memory periodically via malloc_trim(3)."),
init(true),
};
std::function<void()> getMemoryCleanupFunction() {
if (!EnableMallocTrim)
return nullptr;
// Leave a few MB at the top of the heap: it is insignificant
// and will most likely be needed by the main thread
constexpr size_t MallocTrimPad = 20'000'000;
return []() {
if (malloc_trim(MallocTrimPad))
vlog("Released memory via malloc_trim");
};
}
#else
std::function<void()> getMemoryCleanupFunction() { return nullptr; }
#endif
#if CLANGD_ENABLE_REMOTE
opt<std::string> RemoteIndexAddress{
"remote-index-address",
@ -797,6 +824,7 @@ clangd accepts flags on the commandline, and in the CLANGD_FLAGS environment var
Opts.BuildRecoveryAST = RecoveryAST;
Opts.PreserveRecoveryASTType = RecoveryASTType;
Opts.FoldingRanges = FoldingRanges;
Opts.MemoryCleanup = getMemoryCleanupFunction();
Opts.CodeComplete.IncludeIneligibleResults = IncludeIneligibleResults;
Opts.CodeComplete.Limit = LimitResults;