blob: a4c6be656841a2697cd8ee1c0216382abe7b1b43 [file] [log] [blame]
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
#include "memory_profiler.hpp"
#include <chrono>
#include <fstream>
#include <sstream>
#include <string>
#include <utility>
#include <process/delay.hpp>
#include <process/future.hpp>
#include <process/help.hpp>
#include <process/http.hpp>
#include <stout/assert.hpp>
#include <stout/format.hpp>
#include <stout/json.hpp>
#include <stout/option.hpp>
#include <stout/os.hpp>
#include <stout/path.hpp>
#include <stout/os/write.hpp>
#include <glog/logging.h>
using process::Future;
using process::HELP;
using process::TLDR;
using process::DESCRIPTION;
using process::AUTHENTICATION;
using std::string;
// The main workflow to generate and download a heap profile
// goes through the sequence of endpoints
//
// `/start?duration=T` -> `/download/{raw,graph,text}`
//
// A started profiling run will be stopped automatically after the
// given duration has passed, but can be ended prematurely by accessing
//
// `/stop`
//
// Any started run has an associated unique id, which is intended to make
// it easier for scripts to reliably download only those profiles that
// they themselves generated. Human operators will mostly ignore it and
// use the provided default value.
//
// The generated files are typically stored under the directory
// `/tmp/libprocess.XXXXXX/jemalloc.{txt,svg,dump}`, where XXXXXX
// stands for a random combination of letters. This directory, as well
// as the files contained therein, is created lazily the first time it
// is accessed.
//
// To avoid running out of disk space, every time a new file is
// generated, the previous one is overwritten. The members `rawId`,
// `graphId` and `textId` track which version, if any, of the
// corresponding artifact is currently available on disk.
//
// Since this class, being a part of libprocess, will end up in
// `libmesos.so` and thus possibly in applications that use their own
// memory allocator, we carefully avoid actually linking this class
// against `libjemalloc.so`. Instead, we use weak symbols to detect the
// presence of jemalloc at runtime, and use a macro to hide these symbols
// when building on platforms that don't support weak symbols.
#ifdef LIBPROCESS_ALLOW_JEMALLOC
extern "C" __attribute__((weak)) void malloc_stats_print(
void (*writecb)(void*, const char*),
void* opaque,
const char* opts);
extern "C" __attribute__((weak)) int mallctl(
const char* opt, void* oldp, size_t* oldsz, void* newp, size_t newsz);
#endif // LIBPROCESS_ALLOW_JEMALLOC
namespace {
constexpr char LIBPROCESS_DEFAULT_TMPDIR[] = "/tmp";
constexpr char RAW_PROFILE_FILENAME[] = "profile.dump";
constexpr char SYMBOLIZED_PROFILE_FILENAME[] = "symbolized-profile.dump";
constexpr char GRAPH_FILENAME[] = "profile.svg";
constexpr Duration MINIMUM_COLLECTION_TIME = Seconds(1);
constexpr Duration DEFAULT_COLLECTION_TIME = Minutes(5);
constexpr Duration MAXIMUM_COLLECTION_TIME = Hours(24);
constexpr char JEMALLOC_NOT_DETECTED_MESSAGE[] = R"_(
The current binary doesn't seem to be linked against jemalloc,
or the currently used jemalloc library was compiled without
support for statistics collection.
If the current binary was not compiled against jemalloc,
consider adding the path to libjemalloc to the LD_PRELOAD
environment variable, for example LD_PRELOAD=/usr/lib/libjemalloc.so
If you're running a mesos binary and want to have it linked
against jemalloc by default, consider using the
--enable-jemalloc-allocator configuration option)_";
constexpr char JEMALLOC_PROFILING_NOT_ENABLED_MESSAGE[] = R"_(
The current process seems to be using jemalloc, but profiling
couldn't be enabled.
If you're using a custom version of libjemalloc, make sure that
MALLOC_CONF="prof:true" is part of the environment. (The '/state'
endpoint can be used to double-check the current malloc
configuration).
If the environment looks correct, make sure jemalloc was built
with the --enable-stats and --enable-prof options enabled.
If you're running a mesos binary that was built with the
--enable-memory-profiling option enabled and you're still seeing
this message, please consider filing a bug report)_";
// Size in bytes of the dummy file that gets written when hitting `/start`.
constexpr int DUMMY_FILE_SIZE = 64 * 1024; // 64 KiB
// The `detectJemalloc()` function below was taken from the folly library
// (called `usingJEMalloc()` there), originally distributed by Facebook, Inc
// under the Apache License.
//
// It checks whether jemalloc is used as the current malloc implementation by
// allocating one byte and checking if the threads allocation counter increases.
// This requires jemalloc to have been compiled with `--enable-stats`.
bool detectJemalloc() noexcept {
#ifndef LIBPROCESS_ALLOW_JEMALLOC
return false;
#else
static const bool result = [] () noexcept {
// Some platforms (*cough* OSX *cough*) require weak symbol checks to be
// in the form if (mallctl != nullptr). Not if (mallctl) or if (!mallctl)
// (!!). http://goo.gl/xpmctm
if (mallctl == nullptr || malloc_stats_print == nullptr) {
return false;
}
// "volatile" because gcc optimizes out the reads from *counter, because
// it "knows" malloc doesn't modify global state...
volatile uint64_t* counter;
size_t counterLen = sizeof(uint64_t*);
if (mallctl("thread.allocatedp", static_cast<void*>(&counter), &counterLen,
nullptr, 0) != 0) {
return false;
}
if (counterLen != sizeof(uint64_t*)) {
return false;
}
uint64_t origAllocated = *counter;
// Static because otherwise clever compilers will find out that
// the ptr is not used and does not escape the scope, so they will
// just optimize away the malloc.
static const void* ptr = malloc(1);
if (!ptr) {
// wtf, failing to allocate 1 byte
return false;
}
return (origAllocated != *counter);
}();
return result;
#endif
}
template<typename T>
Try<T> readJemallocSetting(const char* name)
{
#ifdef LIBPROCESS_ALLOW_JEMALLOC
if (!detectJemalloc()) {
return Error(JEMALLOC_NOT_DETECTED_MESSAGE);
}
T value;
size_t size = sizeof(value);
int error = ::mallctl(name, &value, &size, nullptr, 0);
if (error) {
return Error(strings::format(
"Couldn't read option %s: %s", name, ::strerror(error)).get());
}
return value;
#else
UNREACHABLE();
#endif
}
// Returns an error on failure or the previous value on success.
template<typename T>
Try<T> updateJemallocSetting(const char* name, const T& value)
{
#ifdef LIBPROCESS_ALLOW_JEMALLOC
if (!detectJemalloc()) {
return Error(JEMALLOC_NOT_DETECTED_MESSAGE);
}
T previous;
size_t size = sizeof(previous);
int error = ::mallctl(
name, &previous, &size, const_cast<T*>(&value), sizeof(value));
if (error) {
return Error(strings::format(
"Couldn't write value %s for option %s: %s",
stringify(value), name, ::strerror(error)).get());
}
return previous;
#else
UNREACHABLE();
#endif
}
// Sadly, we cannot just use `updateJemallocSetting()` and ignore the result,
// because some settings, in particular `prof.dump`, don't have previous value
// to return.
template<typename T>
Try<Nothing> writeJemallocSetting(const char* name, const T& value)
{
#ifdef LIBPROCESS_ALLOW_JEMALLOC
if (!detectJemalloc()) {
return Error(JEMALLOC_NOT_DETECTED_MESSAGE);
}
int error = mallctl(
name, nullptr, nullptr, const_cast<T*>(&value), sizeof(value));
if (error) {
return Error(strings::format(
"Couldn't write value %s for option %s: %s",
stringify(value), name, ::strerror(error)).get());
}
return Nothing();
#else
UNREACHABLE();
#endif
}
// All generated disk artifacts, i.e. profiles and graph files, are stored
// in this directory. It is generated lazily on the first call to
// `getTemporaryDirectoryPath()` and is never changed afterwards.
// Locking is not needed because all accesses go through the same instance of
// the `MemoryProfiler` process and hence are always serialized with respect
// to each other.
//
// TODO(bevers): This should be made available libprocess-global eventually,
// but right now this is the only class that has a use for it.
Option<Path> temporaryDirectory;
Try<Path> getTemporaryDirectoryPath() {
if (temporaryDirectory.isSome()) {
return temporaryDirectory.get();
}
// TODO(bevers): Add a libprocess-specific override for the system-wide
// `TMPDIR`, for example `LIBPROCESS_TMPDIR`.
const string tmpdir =
os::getenv("TMPDIR").getOrElse(LIBPROCESS_DEFAULT_TMPDIR);
const string pathTemplate = path::join(tmpdir, "libprocess.XXXXXX");
// TODO(bevers): Add an atexit-handler that cleans up the directory.
Try<string> dir = os::mkdtemp(pathTemplate);
if (dir.isError()) {
return Error(dir.error());
}
temporaryDirectory = dir.get();
VLOG(1) << "Using path " << dir.get() << " to store temporary files";
return temporaryDirectory.get();
}
// TODO(alexr): Consider making this asynchronous.
Try<Nothing> generateJeprofFile(
const string& inputPath,
const string& options,
const string& outputPath)
{
// As jeprof doesn't have an option to specify an output file, we actually
// cannot use `os::spawn()` here.
// Note that the three parameters *MUST NOT* be controllable by the user
// accessing the HTTP endpoints, otherwise arbitrary shell commands could be
// trivially injected.
// Apart from that, we dont need to be as careful here as with the actual
// heap profile dump, because a failure will not crash the whole process.
Option<int> result = os::system(strings::format(
"jeprof %s /proc/self/exe %s > %s",
options,
inputPath,
outputPath).get());
if (result != 0) {
return Error(
"Error trying to run jeprof. Please make sure that jeprof is installed"
" and that the input file contains data. For more information, please"
" consult the log files of this process");
}
return Nothing();
}
// TODO(bevers): Implement `http::Request::extractFromRequest<T>(string key)`
// instead of having this here.
Result<time_t> extractIdFromRequest(const process::http::Request& request)
{
Option<string> idParameter = request.url.query.get("id");
if (idParameter.isNone()) {
return None();
}
// Since `strtoll()` can legitimately return any value, we have to detect
// errors by checking if `errno` was set during the call.
errno = 0;
char* endptr;
int base = 10;
long long parsed = std::strtoll(idParameter->c_str(), &endptr, base);
if (errno) {
return Error(strerror(errno));
}
if (endptr != idParameter->c_str() + idParameter->size()) {
return Error("Garbage after parsed id");
}
return parsed;
}
} // namespace {
namespace process {
// Interface to interact with the `jemalloc` library.
namespace jemalloc {
Try<bool> startProfiling()
{
return updateJemallocSetting("prof.active", true);
}
Try<bool> stopProfiling()
{
return updateJemallocSetting("prof.active", false);
}
Try<bool> profilingActive()
{
return readJemallocSetting<bool>("prof.active");
}
Try<Nothing> dump(const string& path)
{
// A profile is dumped every time the 'prof.dump' setting is written to.
return writeJemallocSetting("prof.dump", path.c_str());
}
} // namespace jemalloc {
const string MemoryProfiler::START_HELP()
{
return HELP(
TLDR(
"Starts collection of stack traces."),
DESCRIPTION(
"Activates memory profiling.",
"The profiling works by statistically sampling the backtraces of",
"calls to 'malloc()'. This requires some additional memory to store",
"the collected data. The required additional space is expected to",
"grow logarithmically.",
"",
"Query parameters:",
"",
"> duration=VALUE How long to collect data before",
"> stopping. (default: 5mins)"),
AUTHENTICATION(true));
}
const string MemoryProfiler::STOP_HELP()
{
return HELP(
TLDR(
"Stops memory profiling and dumps collected data."),
DESCRIPTION(
"Instructs the memory profiler to stop collecting data"
"and dumps a file containing the collected data to disk,"
"clearing that data from memory. Does nothing if profiling",
"has not been started before."),
AUTHENTICATION(true));
}
const string MemoryProfiler::DOWNLOAD_RAW_HELP()
{
return HELP(
TLDR(
"Returns a raw memory profile."),
DESCRIPTION(
"Returns a file that was generated when the '/stop' endpoint",
"was last accessed. See the jemalloc [manual page][manpage] for",
"information about the file format.",
"",
"Query parameters:",
"",
"> id=VALUE Optional parameter to request a specific",
"> version of the profile."),
AUTHENTICATION(true),
None(),
REFERENCES("[manpage]: http://jemalloc.net/jemalloc.3.html"));
}
const string MemoryProfiler::DOWNLOAD_TEXT_HELP()
{
return HELP(
TLDR(
"Generates and returns a symbolized memory profile."),
DESCRIPTION(
"Generates a symbolized profile.",
"Requires that the running binary was built with symbols and that",
"jeprof is installed on the host machine.",
"",
"**NOTE:** Generating the returned file might take several minutes.",
"",
"Query parameters:",
"> id=VALUE Optional parameter to request a specific",
"> version of the generated profile."),
AUTHENTICATION(true));
}
const string MemoryProfiler::DOWNLOAD_GRAPH_HELP()
{
return HELP(
TLDR(
"Generates and returns a graph visualization."),
DESCRIPTION(
"Generates a graphical representation of the raw profile in SVG.",
"Using this endpoint requires that that jeprof and dot are installed",
"on the host machine.",
"",
"**NOTE:** Generating the returned file might take several minutes.",
"",
"Query parameters:",
"",
"> id=VALUE Optional parameter to request a specific",
"> version of the generated graph."),
AUTHENTICATION(true));
}
const string MemoryProfiler::STATISTICS_HELP()
{
return HELP(
TLDR(
"Shows memory allocation statistics."),
DESCRIPTION(
"Memory allocation statistics as returned by 'malloc_stats_print()'.",
"These track e.g. the total number of bytes allocated by the current",
"process and the bin-size of these allocations.",
"These statistics are unrelated to the profiling mechanism managed",
"by the '/start' and '/stop' endpoints, and are always accurate.",
"",
"Returns a JSON object."),
AUTHENTICATION(true));
}
const string MemoryProfiler::STATE_HELP()
{
return HELP(
TLDR(
"Shows the configuration of the memory profiler process."),
DESCRIPTION(
"Current memory profiler state. This shows, for example, whether",
"jemalloc was detected, whether profiling is currently active and",
"the directory used to store temporary files.",
"",
"Returns a JSON object."),
AUTHENTICATION(true));
}
void MemoryProfiler::initialize()
{
route("/start",
authenticationRealm,
START_HELP(),
&MemoryProfiler::start);
route("/stop",
authenticationRealm,
STOP_HELP(),
&MemoryProfiler::stop);
route("/download/raw",
authenticationRealm,
DOWNLOAD_RAW_HELP(),
&MemoryProfiler::downloadRawProfile);
route("/download/text",
authenticationRealm,
DOWNLOAD_TEXT_HELP(),
&MemoryProfiler::downloadSymbolizedProfile);
route("/download/graph",
authenticationRealm,
DOWNLOAD_GRAPH_HELP(),
&MemoryProfiler::downloadGraphProfile);
route("/statistics",
authenticationRealm,
STATISTICS_HELP(),
&MemoryProfiler::statistics);
route("/state",
authenticationRealm,
STATE_HELP(),
&MemoryProfiler::state);
}
MemoryProfiler::ProfilingRun::ProfilingRun(
MemoryProfiler* profiler,
time_t id,
const Duration& duration)
: id(id),
timer(delay(
duration,
profiler,
&MemoryProfiler::stopAndGenerateRawProfile))
{}
void MemoryProfiler::ProfilingRun::extend(
MemoryProfiler* profiler,
const Duration& duration)
{
Duration remaining = timer.timeout().remaining();
Clock::cancel(timer);
timer = delay(
remaining + duration,
profiler,
&MemoryProfiler::stopAndGenerateRawProfile);
}
MemoryProfiler::DiskArtifact::DiskArtifact(
const std::string& path,
time_t id)
: path(path),
id(id)
{}
const time_t MemoryProfiler::DiskArtifact::getId() const
{
return id;
}
string MemoryProfiler::DiskArtifact::getPath() const
{
return path;
}
http::Response MemoryProfiler::DiskArtifact::asHttp() const
{
// If we get here, we want to serve the file that *should* be on disk.
// Verify that it still exists before attempting to serve it.
//
// TODO(bevers): Store a checksum and verify that it matches.
if (!os::stat::isfile(path)) {
return http::BadRequest("Requested file was deleted from local disk.\n");
}
process::http::OK response;
response.type = response.PATH;
response.path = path;
response.headers["Content-Type"] = "application/octet-stream";
response.headers["Content-Disposition"] =
strings::format("attachment; filename=%s", path).get();
return std::move(response);
}
Try<MemoryProfiler::DiskArtifact> MemoryProfiler::DiskArtifact::create(
const string& filename,
time_t timestamp,
std::function<Try<Nothing>(const string&)> generator)
{
Try<Path> tmpdir = getTemporaryDirectoryPath();
if (tmpdir.isError()) {
return Error("Could not determine target path: " + tmpdir.error());
}
const string path = path::join(tmpdir.get(), filename);
Try<Nothing> result = generator(path);
if (result.isError()) {
// The old file might still be fine on disk, but there's no good way to
// verify this hence we assume that the error rendered it unusable.
return Error("Failed to create artifact: " + result.error());
}
return MemoryProfiler::DiskArtifact(path, timestamp);
}
MemoryProfiler::MemoryProfiler(const Option<string>& _authenticationRealm)
: ProcessBase("memory-profiler"),
authenticationRealm(_authenticationRealm)
{}
// TODO(bevers): Add a query parameter to select json or html format.
// TODO(bevers): Add a query parameter to configure the sampling interval.
Future<http::Response> MemoryProfiler::start(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
if (!detectJemalloc()) {
return http::BadRequest(string(JEMALLOC_NOT_DETECTED_MESSAGE) + ".\n");
}
Duration duration = DEFAULT_COLLECTION_TIME;
// TODO(bevers): Introduce `http::Request::extractQueryParameter<T>(string)`
// instead of doing it ad-hoc here.
Option<string> durationParameter = request.url.query.get("duration");
if (durationParameter.isSome()) {
Try<Duration> parsed = Duration::parse(durationParameter.get());
if (parsed.isError()) {
return http::BadRequest(
"Could not parse parameter 'duration': " + parsed.error() + ".\n");
}
duration = parsed.get();
}
if (duration < MINIMUM_COLLECTION_TIME ||
duration > MAXIMUM_COLLECTION_TIME) {
return http::BadRequest(
"Duration '" + stringify(duration) + "' must be between "
+ stringify(MINIMUM_COLLECTION_TIME) + " and "
+ stringify(MAXIMUM_COLLECTION_TIME) + ".\n");
}
Try<bool> wasActive = jemalloc::startProfiling();
if (wasActive.isError()) {
return http::BadRequest(
string(JEMALLOC_PROFILING_NOT_ENABLED_MESSAGE) + ".\n");
}
if (!wasActive.get()) {
time_t id = std::chrono::system_clock::to_time_t(
std::chrono::system_clock::now());
currentRun = ProfilingRun(this, id, duration);
}
JSON::Object response;
// This can happen when jemalloc was configured e.g. via the `MALLOC_CONF`
// environment variable. We don't touch it in this case.
if (!currentRun.isSome()) {
return http::Conflict("Heap profiling was started externally.\n");
}
string message = wasActive.get() ?
"Heap profiling is already active." :
"Successfully started new heap profiling run.";
message +=
" After the remaining time elapses, download the generated profile at '/" +
this->self().id + "/download/raw?id=" + stringify(currentRun->id) + "'." +
" Visit '/" + this->self().id + "/stop' to stop collection earlier.";
// Adding 0.5 to round to nearest integer value.
response.values["remaining_seconds"] = stringify(static_cast<int>(
currentRun->timer.timeout().remaining().secs() + 0.5));
response.values["message"] = message;
response.values["id"] = currentRun->id;
return http::OK(response);
}
// TODO(bevers): Add a way to dump an intermediate profile without
// stopping the data collection.
Future<http::Response> MemoryProfiler::stop(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
if (!detectJemalloc()) {
return http::BadRequest(string(JEMALLOC_NOT_DETECTED_MESSAGE) + ".\n");
}
Try<bool> active = jemalloc::profilingActive();
if (active.isError()) {
return http::BadRequest(
"Error interfacing with jemalloc: " + active.error() + ".\n");
}
if (!currentRun.isSome() && active.get()) {
// TODO(bevers): Allow stopping even in this case.
return http::BadRequest(
"Profiling is active, but was not started by libprocess. Accessing the"
" raw profile through libprocess is currently not supported.\n");
}
// If stop is successful or a no-op, `jemallocRawProfile` will be `Some`.
stopAndGenerateRawProfile();
if (rawProfile.isError()) {
return http::BadRequest(rawProfile.error() + ".\n");
}
Try<bool> stillActive = jemalloc::profilingActive();
CHECK(stillActive.isError() || !stillActive.get());
const string message =
"Successfully stopped memory profiling run."
" Use one of the provided URLs to download results."
" Note that in order to generate graphs or symbolized profiles,"
" jeprof must be installed on the host machine and generation of"
" these files can take several minutes.";
const string id = stringify(rawProfile->getId());
JSON::Object result;
result.values["id"] = id;
result.values["message"] = message;
result.values["url_raw_profile"] =
"/" + this->self().id + "/download/raw?id=" + id;
result.values["url_graph_profile"] =
"/" + this->self().id + "/download/graph?id=" + id;
result.values["url_symbolized_profile"] =
"/" + this->self().id + "/download/text?id=" + id;
return http::OK(result);
}
Future<http::Response> MemoryProfiler::downloadRawProfile(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
Result<time_t> requestedId = extractIdFromRequest(request);
// Verify that `id` has the correct version if it was explicitly passed.
if (requestedId.isError()) {
return http::BadRequest(
"Invalid parameter 'id': " + requestedId.error() + ".\n");
}
if (currentRun.isSome() && !requestedId.isSome()) {
return http::BadRequest(
"A profiling run is currently in progress. To download results of the"
" previous run, please pass an 'id' explicitly.\n");
}
if (rawProfile.isError()) {
return http::BadRequest(
"Cannot access raw profile: " + rawProfile.error() + ".\n");
}
// Only requests for the latest available version are allowed.
if (requestedId.isSome() &&
(requestedId.get() != rawProfile->getId())) {
return http::BadRequest(
"Cannot serve requested id #" + stringify(requestedId.get()) + ".\n");
}
return rawProfile->asHttp();
}
Future<http::Response> MemoryProfiler::downloadSymbolizedProfile(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
Result<time_t> requestedId = extractIdFromRequest(request);
// Verify that `id` has the correct version if it was explicitly passed.
if (requestedId.isError()) {
return http::BadRequest(
"Invalid parameter 'id': " + requestedId.error() + ".\n");
}
if (currentRun.isSome() && !requestedId.isSome()) {
return http::BadRequest(
"A profiling run is currently in progress. To download results of the"
" previous run, please pass an 'id' explicitly.\n");
}
if (rawProfile.isError()) {
return http::BadRequest(
"No source profile exists: " + rawProfile.error() + ".\n");
}
const string rawProfilePath = rawProfile->getPath();
const time_t rawProfileId = rawProfile->getId();
// Only requests for the latest available version are allowed.
if (requestedId.isSome() && (requestedId.get() != rawProfileId)) {
return http::BadRequest(
"Cannot serve requested id #" + stringify(requestedId.get()) + ".\n");
}
// Generate the profile for the latest available version
// or return the cached file on disk.
if (symbolizedProfile.isError() ||
(symbolizedProfile->getId() != rawProfileId)) {
symbolizedProfile = DiskArtifact::create(
SYMBOLIZED_PROFILE_FILENAME,
rawProfileId,
[rawProfilePath](const string& outputPath) -> Try<Nothing> {
return generateJeprofFile(
rawProfilePath,
"--text",
outputPath);
});
}
if (symbolizedProfile.isSome()) {
return symbolizedProfile->asHttp();
} else {
const string message = "Cannot generate file: " + symbolizedProfile.error();
LOG(WARNING) << message;
return http::BadRequest(message + ".\n");
}
}
Future<http::Response> MemoryProfiler::downloadGraphProfile(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
Result<time_t> requestedId = extractIdFromRequest(request);
// Verify that `id` has the correct version if it was explicitly passed.
if (requestedId.isError()) {
return http::BadRequest(
"Invalid parameter 'id': " + requestedId.error() + ".\n");
}
if (currentRun.isSome() && !requestedId.isSome()) {
return http::BadRequest(
"A profiling run is currently in progress. To download results of the"
" previous run, please pass an 'id' explicitly.\n");
}
if (rawProfile.isError()) {
return http::BadRequest(
"No source profile exists: " + rawProfile.error() + ".\n");
}
const string rawProfilePath = rawProfile->getPath();
const time_t rawProfileId = rawProfile->getId();
// Only requests for the latest available version are allowed.
if (requestedId.isSome() && (requestedId.get() != rawProfileId)) {
return http::BadRequest(
"Cannot serve requested id #" + stringify(requestedId.get()) + ".\n");
}
// Generate the profile for the latest available version
// or return the cached file on disk.
if (graphProfile.isError() || (graphProfile->getId() != rawProfileId)) {
graphProfile = DiskArtifact::create(
GRAPH_FILENAME,
rawProfileId,
[rawProfilePath](const string& outputPath) -> Try<Nothing> {
return generateJeprofFile(
rawProfilePath,
"--svg",
outputPath);
});
}
if (graphProfile.isSome()) {
return graphProfile->asHttp();
} else {
const string message = "Cannot generate file: " + graphProfile.error();
LOG(WARNING) << message;
return http::BadRequest(message + ".\n");
}
}
// TODO(bevers): Allow passing custom options via query parameters.
Future<http::Response> MemoryProfiler::statistics(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
if (!detectJemalloc()) {
return http::BadRequest(string(JEMALLOC_NOT_DETECTED_MESSAGE) + ".\n");
}
const string options = "J"; // 'J' selects JSON output format.
string statistics;
#ifdef LIBPROCESS_ALLOW_JEMALLOC
::malloc_stats_print([](void* opaque, const char* msg) {
string* statistics = static_cast<string*>(opaque);
*statistics += msg;
}, &statistics, options.c_str());
#endif
return http::OK(statistics, "application/json; charset=utf-8");
}
Future<http::Response> MemoryProfiler::state(
const http::Request& request,
const Option<http::authentication::Principal>&)
{
bool detected = detectJemalloc();
JSON::Object state;
{
// State unrelated to jemalloc.
JSON::Object profilerState;
profilerState.values["jemalloc_detected"] = detected;
profilerState.values["tmp_dir"] = stringify(
temporaryDirectory.getOrElse("Not yet generated"));
{
JSON::Object runInformation;
if (currentRun.isSome()) {
runInformation.values["id"] = currentRun->id;
runInformation.values["remaining_seconds"] =
currentRun->timer.timeout().remaining().secs();
} else if (rawProfile.isSome()) {
runInformation.values["id"] = rawProfile->getId();
runInformation.values["remaining_seconds"] = 0;
} else {
runInformation.values["id"] = JSON::Null();
}
profilerState.values["current_run"] = std::move(runInformation);
}
state.values["memory_profiler"] = std::move(profilerState);
}
if (!detected) {
return http::OK(state);
}
{
// Holds relevant parts of the current jemalloc state.
JSON::Object jemallocState;
{
// Holds malloc configuration from various sources.
JSON::Object mallocConf;
// User-specified malloc configuration that was added via
// the `MALLOC_CONF` environment variable.
mallocConf.values["environment"] =
os::getenv("MALLOC_CONF").getOrElse("");
// Compile-time malloc configuration that was added at build time via
// the `--with-malloc-conf` flag.
Try<const char*> builtinMallocConf = readJemallocSetting<const char*>(
"config.malloc_conf");
if (builtinMallocConf.isError()) {
mallocConf.values["build_options"] = builtinMallocConf.error();
} else {
mallocConf.values["build_options"] = builtinMallocConf.get();
}
// TODO(bevers): System-wide jemalloc settings can be specified by
// creating a symlink at /etc/malloc.conf whose pointed-to value is read
// as an option string.
// Application-specific jemalloc settings can be specified by creating
// an externally visible symbol called `malloc_conf`.
// We should also display both of these here.
jemallocState.values["malloc_conf"] = std::move(mallocConf);
}
// Whether jemalloc was compiled with support for heap profiling.
Try<bool> profilingSupported = readJemallocSetting<bool>("config.prof");
if (profilingSupported.isError()) {
jemallocState.values["profiling_enabled"] = profilingSupported.error();
} else {
jemallocState.values["profiling_enabled"] = profilingSupported.get();
}
// Whether profiling is currently active.
Try<bool> profilingActive = readJemallocSetting<bool>("prof.active");
if (profilingActive.isError()) {
jemallocState.values["profiling_active"] = profilingActive.error();
} else {
jemallocState.values["profiling_active"] = profilingActive.get();
}
state.values["jemalloc"] = std::move(jemallocState);
}
return http::OK(state);
}
void MemoryProfiler::stopAndGenerateRawProfile()
{
ASSERT(detectJemalloc());
VLOG(1) << "Attempting to stop current profiling run";
// If there is no current profiling run, there is nothing to do.
if (!currentRun.isSome()) {
return;
}
Try<bool> stopped = jemalloc::stopProfiling();
if (stopped.isError()) {
LOG(WARNING) << "Failed to stop memory profiling: " << stopped.error();
// Don't give up. Probably it will fail again in the future, but at least
// the problem will be clearly visible in the logs.
currentRun->extend(this, Seconds(5));
return;
}
// Heap profiling should not be active any more.
// We won't retry stopping and generating a profile after this point:
// We're not actively sampling any more, and if the user still cares
// about this profile they will get the data with the next run.
Try<bool> stillActive = jemalloc::profilingActive();
CHECK(stillActive.isError() || !stillActive.get());
time_t runId = currentRun->id;
Clock::cancel(currentRun->timer);
currentRun = None();
if (!stopped.get()) {
// This is a weird state to end up in, apparently something else in this
// process stopped profiling independently of us.
// If there was some valuable, un-dumped data it is still possible to get
// it by starting a new run.
LOG(WARNING)
<< "Memory profiling unexpectedly inactive; not dumping profile. Ensure"
<< " nothing else is interfacing with jemalloc in this process";
return;
}
// We store the new artifact even in case of error to surface it to the user.
rawProfile = DiskArtifact::create(
RAW_PROFILE_FILENAME,
runId,
[](const string& outputPath) -> Try<Nothing> {
// Make sure we actually have permissions to write to the file and that
// there is at least a little bit space left on the device.
const string data(DUMMY_FILE_SIZE, '\0');
Try<Nothing> written = os::write(outputPath, data);
if (written.isError()) {
return Error(written.error());
}
// Verify independently that the file was actually written.
Try<Bytes> size = os::stat::size(outputPath);
if (size.isError() || size.get() != DUMMY_FILE_SIZE) {
return Error(strings::format(
"Couldn't verify integrity of dump file %s", outputPath).get());
}
// Finally, do the real dump.
return jemalloc::dump(outputPath);
});
if (rawProfile.isError()) {
LOG(WARNING) << "Cannot dump profile: " + rawProfile.error();
}
}
} // namespace process {