mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Capture Shardok CPU profiles with Linux perf (#8791)
This commit is contained in:
@@ -10,6 +10,8 @@ on:
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'src/main/resources/net/eagle0/shardok/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/shardok_perf.lock.json'
|
||||
- 'ci/shardok_perf.yaml'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
@@ -283,6 +285,12 @@ jobs:
|
||||
echo "Loaded image ID: $(docker image inspect --format '{{.Id}}' "$SHARDOK_IMAGE")"
|
||||
echo "Shardok binary SHA256:"
|
||||
docker run --rm --entrypoint sha256sum "$SHARDOK_IMAGE" /app/shardok-server
|
||||
echo "Verifying perf capture permissions:"
|
||||
docker run --rm \
|
||||
--cap-add PERFMON \
|
||||
--entrypoint perf \
|
||||
"$SHARDOK_IMAGE" \
|
||||
stat --event cpu-clock:u -- perf version
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
@@ -293,6 +301,7 @@ jobs:
|
||||
docker run -d \
|
||||
--name shardok-ai \
|
||||
--restart unless-stopped \
|
||||
--cap-add PERFMON \
|
||||
-p 40042:40042 \
|
||||
-v /opt/eagle0/data:/data \
|
||||
-v /etc/shardok:/etc/shardok:ro \
|
||||
|
||||
+9
-1
@@ -70,7 +70,6 @@ scala_deps.scala_proto()
|
||||
# Language Support - C++
|
||||
#
|
||||
|
||||
bazel_dep(name = "gperftools", version = "2.18.1")
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.8.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
@@ -264,9 +263,18 @@ bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.3.0")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
bazel_dep(name = "rules_distroless", version = "0.6.1")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
apt = use_extension("@rules_distroless//apt:extensions.bzl", "apt")
|
||||
apt.install(
|
||||
lock = "//ci:shardok_perf.lock.json",
|
||||
name = "shardok_perf",
|
||||
manifest = "//ci:shardok_perf.yaml",
|
||||
)
|
||||
use_repo(apt, "shardok_perf")
|
||||
|
||||
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
|
||||
# Digest pinned for Bazel repository cache hit; update with:
|
||||
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
|
||||
Generated
+7
-3
@@ -69,6 +69,7 @@
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.20.0/MODULE.bazel": "8b85300b9c8594752e0721a37210e34879d23adc219ed9dc8f4104a4a1750920",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.25.0/MODULE.bazel": "e2e60a10a6da64bbf533f15ca652bf61a033e41c2ed734d79a9a08ba87f68c1a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
@@ -92,6 +93,7 @@
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.11.bcr.1/MODULE.bazel": "bb319e2fbb19e5223e5d4235f56fc46674b7b7c765cde3096ca80e7faafb539d",
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.15/MODULE.bazel": "422c026647ead49d81271fe55cfe6fe4a4c90011d1ae0b747efdd7e0b30ec367",
|
||||
"https://bcr.bazel.build/modules/bazel_jar_jar/0.1.15/source.json": "9ce5c5bc2b5fe84f66757cf418325fd59948dc0e98f3c9ba59d2efb1b5b31150",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0-beta.1/MODULE.bazel": "407729e232f611c3270005b016b437005daa7b1505826798ea584169a476e878",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0-rc.0/MODULE.bazel": "d6e00979a98ac14ada5e31c8794708b41434d461e7e7ca39b59b765e6d233b18",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
|
||||
"https://bcr.bazel.build/modules/bazel_lib/3.1.0/MODULE.bazel": "6809765c14e3c766a9b9286c7b0ec56ed87a73326e48fe01749f0c0fdcfe3287",
|
||||
@@ -200,8 +202,6 @@
|
||||
"https://bcr.bazel.build/modules/googletest/1.17.0.bcr.2/MODULE.bazel": "827f54f492a3ce549c940106d73de332c2b30cebd0c20c0bc5d786aba7f116cb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.17.0.bcr.2/source.json": "3664514073a819992320ffbce5825e4238459df344d8b01748af2208f8d2e1eb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46",
|
||||
"https://bcr.bazel.build/modules/gperftools/2.18.1/MODULE.bazel": "2bfde64989ae531d9fd9da9c2ccfff1212eafcfcb26b0594b59667d0190b6252",
|
||||
"https://bcr.bazel.build/modules/gperftools/2.18.1/source.json": "3425124f51a743ab03b3145f55bff4c0d255b5311b4ca54886db79ea461ebedb",
|
||||
"https://bcr.bazel.build/modules/grpc-java/1.62.2/MODULE.bazel": "99b8771e8c7cacb130170fed2a10c9e8fed26334a93e73b42d2953250885a158",
|
||||
"https://bcr.bazel.build/modules/grpc-java/1.66.0/MODULE.bazel": "86ff26209fac846adb89db11f3714b3dc0090fb2fb81575673cc74880cda4e7e",
|
||||
"https://bcr.bazel.build/modules/grpc-java/1.69.0/MODULE.bazel": "53887af6a00b3b406d70175d3d07e84ea9362016ff55ea90b9185f0227bfaf98",
|
||||
@@ -343,6 +343,8 @@
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.2.9/MODULE.bazel": "34263f1dca62ea664265438cef714d7db124c03e1ed55ebb4f1dc860164308d1",
|
||||
"https://bcr.bazel.build/modules/rules_distroless/0.6.1/MODULE.bazel": "ce96d59d70695eadf53f45d752fef38f7ac59c117f5db32bb61767a732fe090b",
|
||||
"https://bcr.bazel.build/modules/rules_distroless/0.6.1/source.json": "bdea3dd4883fed8478564675d7bcc15eb3bd366245ec51c6d317d100c6af59b5",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.20.5/MODULE.bazel": "0ca90c23f5c8e7b7c353bd673062f8716eca7c525e4fd28da16247672251bd77",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.22.1/MODULE.bazel": "0faeb8532d178e34ad0e461d7ec1a9d08f3e5c6c81b8791d5fff5aee356adcb2",
|
||||
"https://bcr.bazel.build/modules/rules_dotnet/0.22.1/source.json": "a1d2ad8cd886f716d7707925f3628178830c7df644a47fce711732d9e03fde0c",
|
||||
@@ -388,6 +390,7 @@
|
||||
"https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.6.3/MODULE.bazel": "e90505b7a931d194245ffcfb6ff4ca8ef9d46b4e830d12e64817752e0198e2ed",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.7.1/MODULE.bazel": "123a57f84c7f80d6f66b0c2486db3460ed8c4389f788ccbd35bb489b1ab23634",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.8.0/MODULE.bazel": "de589d0880911ac007abd521b9f0ddcd8b0dbd05c8553e6f8124a050b83acf7d",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.9.0/MODULE.bazel": "e17c876cb53dcd817b7b7f0d2985b710610169729e8c371b2221cacdcd3dce4a",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d",
|
||||
"https://bcr.bazel.build/modules/rules_java/9.3.0/MODULE.bazel": "f657c72d65ac449caae9abf2e68e66c0d36f9416848c4c4903d0b3234229e7f2",
|
||||
@@ -523,6 +526,7 @@
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20251210-ee656c7/MODULE.bazel": "03e1c50b16fc2403de5dd98ff8e2dc0b404865740434b8705c7c0853241f943d",
|
||||
"https://bcr.bazel.build/modules/xds/0.0.0-20251210-ee656c7/source.json": "69b87f3bff38ba29399c012f3e3fc0f239ac4e5c1ae66ebd9481be6d8461da55",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.1.1/MODULE.bazel": "9039681f9bcb8958ee2c87ffc74bdafba9f4369096a2b5634b88abc0eaefa072",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.1/MODULE.bazel": "9bcb7151b3cd4681b89d350530eaf7b45e32a44dda94843b8932b0cb1cd4594a",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.2/MODULE.bazel": "0384efa70e8033d842ea73aa4b7199fa099709e236a7264345c03937166670b6",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/MODULE.bazel": "d3a270662f5d766cd7229732d65a5a5bc485240c3007343dd279edfb60c9ae27",
|
||||
"https://bcr.bazel.build/modules/yq.bzl/0.3.4/source.json": "786dafdc2843722da3416e4343ee1a05237227f068590779a6e8496a2064c0f9",
|
||||
@@ -1281,7 +1285,7 @@
|
||||
"@@yq.bzl+//yq:extensions.bzl%yq": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "tDqk+ntWTdxNAWPDjRY1uITgHbti2jcXR5ZdinltBs0=",
|
||||
"usagesDigest": "XGLRpNcVs4WD/zog6U0sXbBo2OlR5O4mc58OU1L3EVc=",
|
||||
"usagesDigest": "yQVwBIghFyxVWUtjYshAwqLz0INeV/zVcsVAbV6ghbE=",
|
||||
"recordedInputs": [],
|
||||
"generatedRepoSpecs": {
|
||||
"yq_darwin_amd64": {
|
||||
|
||||
+1
-8
@@ -230,19 +230,12 @@ oci_image(
|
||||
name = "shardok_server_image_arm64",
|
||||
base = "@ubuntu_24_04_linux_arm64_v8",
|
||||
entrypoint = ["/app/shardok-server"],
|
||||
env = {
|
||||
# The default libgcc unwinder does not use the interrupted thread's ARM64 signal context,
|
||||
# producing correct leaf samples with unrelated parent frames. The production binary is
|
||||
# built with frame pointers, so select gperftools' context-aware frame-pointer unwinder
|
||||
# before its static initializer chooses an implementation.
|
||||
"TCMALLOC_STACKTRACE_METHOD": "generic_fp",
|
||||
"TCMALLOC_STACKTRACE_METHOD_VERBOSE": "1",
|
||||
},
|
||||
exposed_ports = [
|
||||
"40042/tcp",
|
||||
"40052/tcp",
|
||||
],
|
||||
tars = [
|
||||
"@shardok_perf//:flat",
|
||||
# Note: busybox_layer_arm64 omitted - busybox.net has SSL issues
|
||||
# Health checks can use the shardok-server binary itself or be added later
|
||||
":shardok_binary_layer_arm64",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
version: 1
|
||||
|
||||
sources:
|
||||
- channel: resolute main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-security main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
- channel: resolute-updates main universe
|
||||
url: https://ports.ubuntu.com/ubuntu-ports
|
||||
|
||||
archs:
|
||||
- "arm64"
|
||||
|
||||
packages:
|
||||
- "linux-perf"
|
||||
@@ -10,7 +10,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok:__subpackages__",
|
||||
"//src/test/cpp/net/eagle0/shardok:__subpackages__",
|
||||
],
|
||||
deps = ["@gperftools//:cpu_profiler"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
#include "CpuProfiler.hpp"
|
||||
|
||||
#include <gperftools/profiler.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace shardok {
|
||||
namespace {
|
||||
@@ -26,19 +37,151 @@ auto FileSize(const std::string& path) -> int64_t {
|
||||
return static_cast<int64_t>(size);
|
||||
}
|
||||
|
||||
auto ReadFile(const std::string& path) -> std::string {
|
||||
std::ifstream input(path);
|
||||
if (!input.is_open()) { return {}; }
|
||||
std::ostringstream contents;
|
||||
contents << input.rdbuf();
|
||||
return contents.str();
|
||||
}
|
||||
|
||||
auto PerfExecutable() -> std::string {
|
||||
if (const char* configured = std::getenv("SHARDOK_PERF_EXECUTABLE");
|
||||
configured != nullptr && *configured != '\0') {
|
||||
return configured;
|
||||
}
|
||||
return "perf";
|
||||
}
|
||||
|
||||
class PerfCpuProfileProcess final : public CpuProfileProcess {
|
||||
private:
|
||||
pid_t pid_ = -1;
|
||||
std::string errorPath_;
|
||||
|
||||
void Reap() {
|
||||
if (pid_ <= 0) { return; }
|
||||
int status = 0;
|
||||
while (waitpid(pid_, &status, 0) < 0 && errno == EINTR) {}
|
||||
pid_ = -1;
|
||||
}
|
||||
|
||||
public:
|
||||
auto Start(const std::string& profilePath, const int32_t sampleFrequencyHz)
|
||||
-> std::optional<std::string> override {
|
||||
errorPath_ = profilePath + ".stderr";
|
||||
std::error_code removeError;
|
||||
std::filesystem::remove(errorPath_, removeError);
|
||||
|
||||
const std::string executable = PerfExecutable();
|
||||
std::vector<std::string> arguments{
|
||||
executable,
|
||||
"record",
|
||||
"--output",
|
||||
profilePath,
|
||||
"--freq",
|
||||
std::to_string(sampleFrequencyHz),
|
||||
"--event",
|
||||
"cpu-clock:u",
|
||||
"--inherit",
|
||||
"--call-graph",
|
||||
"dwarf,8192",
|
||||
"--pid",
|
||||
std::to_string(getpid()),
|
||||
};
|
||||
std::vector<char*> argumentPointers;
|
||||
argumentPointers.reserve(arguments.size() + 1);
|
||||
for (auto& argument : arguments) { argumentPointers.push_back(argument.data()); }
|
||||
argumentPointers.push_back(nullptr);
|
||||
|
||||
posix_spawn_file_actions_t fileActions;
|
||||
int result = posix_spawn_file_actions_init(&fileActions);
|
||||
if (result != 0) {
|
||||
return "Unable to initialize perf output capture: " + std::string(strerror(result));
|
||||
}
|
||||
result = posix_spawn_file_actions_addopen(
|
||||
&fileActions,
|
||||
STDERR_FILENO,
|
||||
errorPath_.c_str(),
|
||||
O_WRONLY | O_CREAT | O_TRUNC,
|
||||
0600);
|
||||
if (result == 0) {
|
||||
result = posix_spawnp(
|
||||
&pid_,
|
||||
executable.c_str(),
|
||||
&fileActions,
|
||||
nullptr,
|
||||
argumentPointers.data(),
|
||||
environ);
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&fileActions);
|
||||
if (result != 0) {
|
||||
pid_ = -1;
|
||||
return "Unable to start perf: " + std::string(strerror(result));
|
||||
}
|
||||
|
||||
// Permission and unsupported-event failures happen immediately. Give perf enough time to
|
||||
// report them so the start RPC returns a useful error instead of a false recording state.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
int status = 0;
|
||||
const pid_t waitResult = waitpid(pid_, &status, WNOHANG);
|
||||
if (waitResult == pid_) {
|
||||
pid_ = -1;
|
||||
std::string detail = ReadFile(errorPath_);
|
||||
if (detail.empty()) { detail = "perf exited before recording began"; }
|
||||
return "perf failed to start the Shardok CPU profile: " + detail;
|
||||
}
|
||||
if (waitResult < 0) {
|
||||
const std::string error = strerror(errno);
|
||||
pid_ = -1;
|
||||
return "Unable to observe perf after startup: " + error;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Stop() override {
|
||||
if (pid_ <= 0) { return; }
|
||||
if (kill(pid_, SIGINT) < 0 && errno != ESRCH) {
|
||||
// The child belongs to this process, so this is only a last-resort cleanup path.
|
||||
(void)kill(pid_, SIGKILL);
|
||||
}
|
||||
Reap();
|
||||
}
|
||||
|
||||
auto IsRunning() -> bool override {
|
||||
if (pid_ <= 0) { return false; }
|
||||
int status = 0;
|
||||
pid_t waitResult = 0;
|
||||
do {
|
||||
waitResult = waitpid(pid_, &status, WNOHANG);
|
||||
} while (waitResult < 0 && errno == EINTR);
|
||||
if (waitResult == 0) { return true; }
|
||||
pid_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
~PerfCpuProfileProcess() override { Stop(); }
|
||||
};
|
||||
|
||||
auto DefaultProcessFactory() -> std::unique_ptr<CpuProfileProcess> {
|
||||
return std::make_unique<PerfCpuProfileProcess>();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
CpuProfiler::CpuProfiler(
|
||||
std::string profilePath,
|
||||
std::string executablePath,
|
||||
const std::chrono::milliseconds maxDuration,
|
||||
const int64_t maxProfileSizeBytes)
|
||||
const int64_t maxProfileSizeBytes,
|
||||
CpuProfileProcessFactory processFactory)
|
||||
: state_(std::make_shared<State>()),
|
||||
executablePath_(std::move(executablePath)) {
|
||||
state_->profilePath = std::move(profilePath);
|
||||
state_->maxDuration = maxDuration;
|
||||
state_->maxProfileSizeBytes = maxProfileSizeBytes;
|
||||
setenv("CPUPROFILE_FREQUENCY", "100", 1);
|
||||
state_->processFactory = processFactory ? std::move(processFactory)
|
||||
: CpuProfileProcessFactory(DefaultProcessFactory);
|
||||
}
|
||||
|
||||
CpuProfiler::~CpuProfiler() { Stop(); }
|
||||
@@ -54,8 +197,10 @@ auto CpuProfiler::Start() -> std::optional<std::string> {
|
||||
return "Unable to remove the previous Shardok CPU profile: " + removeError.message();
|
||||
}
|
||||
|
||||
if (ProfilerStart(state->profilePath.c_str()) == 0) {
|
||||
return "gperftools failed to start the Shardok CPU profile";
|
||||
state->process = state->processFactory();
|
||||
if (const auto error = state->process->Start(state->profilePath, kSampleFrequencyHz)) {
|
||||
state->process.reset();
|
||||
return error;
|
||||
}
|
||||
|
||||
state->recording = true;
|
||||
@@ -70,6 +215,11 @@ auto CpuProfiler::Start() -> std::optional<std::string> {
|
||||
std::lock_guard monitorLock(state->mutex);
|
||||
if (!state->recording || state->generation != generation) { return; }
|
||||
|
||||
if (!state->process->IsRunning()) {
|
||||
state->recording = false;
|
||||
state->process.reset();
|
||||
return;
|
||||
}
|
||||
if (std::chrono::steady_clock::now() >= deadline ||
|
||||
FileSize(state->profilePath) >= state->maxProfileSizeBytes) {
|
||||
CpuProfiler::StopLocked(*state);
|
||||
@@ -83,7 +233,8 @@ auto CpuProfiler::Start() -> std::optional<std::string> {
|
||||
|
||||
void CpuProfiler::StopLocked(State& state) {
|
||||
if (!state.recording) { return; }
|
||||
ProfilerStop();
|
||||
state.process->Stop();
|
||||
state.process.reset();
|
||||
state.recording = false;
|
||||
}
|
||||
|
||||
@@ -92,17 +243,12 @@ void CpuProfiler::Stop() {
|
||||
StopLocked(*state_);
|
||||
}
|
||||
|
||||
void CpuProfiler::Flush() {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
if (state_->recording) { ProfilerFlush(); }
|
||||
}
|
||||
|
||||
auto CpuProfiler::Snapshot() const -> CpuProfileSnapshot {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
const int64_t profileSize = FileSize(state_->profilePath);
|
||||
return CpuProfileSnapshot{
|
||||
.recording = state_->recording,
|
||||
.profileAvailable = profileSize > 0,
|
||||
.profileAvailable = !state_->recording && profileSize > 0,
|
||||
.startedAtEpochMillis = state_->startedAtEpochMillis,
|
||||
.profileSizeBytes = profileSize,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
@@ -10,6 +11,18 @@
|
||||
|
||||
namespace shardok {
|
||||
|
||||
class CpuProfileProcess {
|
||||
public:
|
||||
virtual ~CpuProfileProcess() = default;
|
||||
|
||||
virtual auto Start(const std::string& profilePath, int32_t sampleFrequencyHz)
|
||||
-> std::optional<std::string> = 0;
|
||||
virtual void Stop() = 0;
|
||||
[[nodiscard]] virtual auto IsRunning() -> bool = 0;
|
||||
};
|
||||
|
||||
using CpuProfileProcessFactory = std::function<std::unique_ptr<CpuProfileProcess>()>;
|
||||
|
||||
struct CpuProfileSnapshot {
|
||||
bool recording;
|
||||
bool profileAvailable;
|
||||
@@ -27,6 +40,8 @@ private:
|
||||
std::string profilePath;
|
||||
std::chrono::milliseconds maxDuration;
|
||||
int64_t maxProfileSizeBytes;
|
||||
CpuProfileProcessFactory processFactory;
|
||||
std::unique_ptr<CpuProfileProcess> process;
|
||||
};
|
||||
|
||||
std::shared_ptr<State> state_;
|
||||
@@ -35,15 +50,16 @@ private:
|
||||
static void StopLocked(State& state);
|
||||
|
||||
public:
|
||||
static constexpr int32_t kSampleFrequencyHz = 100;
|
||||
static constexpr int32_t kSampleFrequencyHz = 99;
|
||||
static constexpr std::chrono::milliseconds kMaxDuration = std::chrono::hours(1);
|
||||
static constexpr int64_t kMaxProfileSizeBytes = 100 * 1024 * 1024;
|
||||
static constexpr int64_t kMaxProfileSizeBytes = 500 * 1024 * 1024;
|
||||
|
||||
explicit CpuProfiler(
|
||||
std::string profilePath = "/tmp/shardok-admin.prof",
|
||||
std::string profilePath = "/tmp/shardok-admin.data",
|
||||
std::string executablePath = "/proc/self/exe",
|
||||
std::chrono::milliseconds maxDuration = kMaxDuration,
|
||||
int64_t maxProfileSizeBytes = kMaxProfileSizeBytes);
|
||||
int64_t maxProfileSizeBytes = kMaxProfileSizeBytes,
|
||||
CpuProfileProcessFactory processFactory = {});
|
||||
~CpuProfiler();
|
||||
|
||||
CpuProfiler(const CpuProfiler&) = delete;
|
||||
@@ -51,7 +67,6 @@ public:
|
||||
|
||||
auto Start() -> std::optional<std::string>;
|
||||
void Stop();
|
||||
void Flush();
|
||||
[[nodiscard]] auto Snapshot() const -> CpuProfileSnapshot;
|
||||
[[nodiscard]] auto ProfilePath() const -> const std::string&;
|
||||
[[nodiscard]] auto ExecutablePath() const -> const std::string&;
|
||||
|
||||
@@ -178,7 +178,6 @@ auto EagleInterfaceImpl::DownloadCpuProfile(
|
||||
std::string path;
|
||||
switch (request->artifact_type()) {
|
||||
case CPU_PROFILE_ARTIFACT_TYPE_PROFILE:
|
||||
cpuProfiler_.Flush();
|
||||
if (!cpuProfiler_.Snapshot().profileAvailable) {
|
||||
return Status(
|
||||
StatusCode::FAILED_PRECONDITION,
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -12,29 +15,64 @@
|
||||
namespace shardok {
|
||||
namespace {
|
||||
|
||||
#if defined(__has_feature)
|
||||
#if __has_feature(address_sanitizer)
|
||||
#define EAGLE0_ADDRESS_SANITIZER_ENABLED 1
|
||||
#endif
|
||||
#endif
|
||||
#if defined(__SANITIZE_ADDRESS__)
|
||||
#define EAGLE0_ADDRESS_SANITIZER_ENABLED 1
|
||||
#endif
|
||||
struct FakeProcessState {
|
||||
bool running = false;
|
||||
int startCount = 0;
|
||||
int stopCount = 0;
|
||||
size_t initialProfileBytes = 16;
|
||||
std::optional<std::string> startError;
|
||||
};
|
||||
|
||||
class FakeCpuProfileProcess final : public CpuProfileProcess {
|
||||
private:
|
||||
std::shared_ptr<FakeProcessState> state_;
|
||||
|
||||
public:
|
||||
explicit FakeCpuProfileProcess(std::shared_ptr<FakeProcessState> state)
|
||||
: state_(std::move(state)) {}
|
||||
|
||||
auto Start(const std::string& profilePath, int32_t /*sampleFrequencyHz*/)
|
||||
-> std::optional<std::string> override {
|
||||
state_->startCount++;
|
||||
if (state_->startError) { return state_->startError; }
|
||||
std::ofstream profile(profilePath, std::ios::binary);
|
||||
profile << std::string(state_->initialProfileBytes, 'p');
|
||||
profile.close();
|
||||
state_->running = true;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Stop() override {
|
||||
if (!state_->running) { return; }
|
||||
state_->running = false;
|
||||
state_->stopCount++;
|
||||
}
|
||||
|
||||
auto IsRunning() -> bool override { return state_->running; }
|
||||
};
|
||||
|
||||
auto FakeProcessFactory(const std::shared_ptr<FakeProcessState>& state)
|
||||
-> CpuProfileProcessFactory {
|
||||
return [state]() { return std::make_unique<FakeCpuProfileProcess>(state); };
|
||||
}
|
||||
|
||||
TEST(CpuProfilerTest, RecordsUntilStoppedAndRejectsConcurrentStart) {
|
||||
#if defined(EAGLE0_ADDRESS_SANITIZER_ENABLED)
|
||||
GTEST_SKIP() << "gperftools stack sampling is incompatible with ASan stack redzones";
|
||||
#endif
|
||||
|
||||
const std::string profilePath =
|
||||
(std::filesystem::temp_directory_path() /
|
||||
("shardok-cpu-profiler-test-" + std::to_string(getpid()) + ".prof"))
|
||||
.string();
|
||||
std::filesystem::remove(profilePath);
|
||||
|
||||
CpuProfiler profiler(profilePath, "/proc/self/exe");
|
||||
const auto processState = std::make_shared<FakeProcessState>();
|
||||
CpuProfiler profiler(
|
||||
profilePath,
|
||||
"/proc/self/exe",
|
||||
CpuProfiler::kMaxDuration,
|
||||
CpuProfiler::kMaxProfileSizeBytes,
|
||||
FakeProcessFactory(processState));
|
||||
ASSERT_FALSE(profiler.Start().has_value());
|
||||
EXPECT_TRUE(profiler.Snapshot().recording);
|
||||
EXPECT_FALSE(profiler.Snapshot().profileAvailable);
|
||||
EXPECT_TRUE(profiler.Start().has_value());
|
||||
|
||||
std::atomic<uint64_t> work = 0;
|
||||
@@ -47,26 +85,26 @@ TEST(CpuProfilerTest, RecordsUntilStoppedAndRejectsConcurrentStart) {
|
||||
EXPECT_TRUE(snapshot.profileAvailable);
|
||||
EXPECT_GT(snapshot.profileSizeBytes, 0);
|
||||
EXPECT_GT(work.load(), 0);
|
||||
EXPECT_EQ(processState->startCount, 1);
|
||||
EXPECT_EQ(processState->stopCount, 1);
|
||||
|
||||
std::filesystem::remove(profilePath);
|
||||
}
|
||||
|
||||
TEST(CpuProfilerTest, AutomaticallyStopsAtDurationLimit) {
|
||||
#if defined(EAGLE0_ADDRESS_SANITIZER_ENABLED)
|
||||
GTEST_SKIP() << "gperftools stack sampling is incompatible with ASan stack redzones";
|
||||
#endif
|
||||
|
||||
const std::string profilePath =
|
||||
(std::filesystem::temp_directory_path() /
|
||||
("shardok-cpu-profiler-duration-test-" + std::to_string(getpid()) + ".prof"))
|
||||
.string();
|
||||
std::filesystem::remove(profilePath);
|
||||
|
||||
const auto processState = std::make_shared<FakeProcessState>();
|
||||
CpuProfiler profiler(
|
||||
profilePath,
|
||||
"/proc/self/exe",
|
||||
std::chrono::milliseconds(50),
|
||||
CpuProfiler::kMaxProfileSizeBytes);
|
||||
CpuProfiler::kMaxProfileSizeBytes,
|
||||
FakeProcessFactory(processState));
|
||||
ASSERT_FALSE(profiler.Start().has_value());
|
||||
|
||||
const auto waitUntil = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
@@ -78,28 +116,30 @@ TEST(CpuProfilerTest, AutomaticallyStopsAtDurationLimit) {
|
||||
EXPECT_FALSE(snapshot.recording);
|
||||
EXPECT_TRUE(snapshot.profileAvailable);
|
||||
EXPECT_GT(snapshot.profileSizeBytes, 0);
|
||||
EXPECT_EQ(processState->stopCount, 1);
|
||||
|
||||
std::filesystem::remove(profilePath);
|
||||
}
|
||||
|
||||
TEST(CpuProfilerTest, AutomaticallyStopsAtProfileSizeLimit) {
|
||||
#if defined(EAGLE0_ADDRESS_SANITIZER_ENABLED)
|
||||
GTEST_SKIP() << "gperftools stack sampling is incompatible with ASan stack redzones";
|
||||
#endif
|
||||
|
||||
const std::string profilePath =
|
||||
(std::filesystem::temp_directory_path() /
|
||||
("shardok-cpu-profiler-size-test-" + std::to_string(getpid()) + ".prof"))
|
||||
.string();
|
||||
std::filesystem::remove(profilePath);
|
||||
|
||||
CpuProfiler profiler(profilePath, "/proc/self/exe", std::chrono::seconds(10), 1);
|
||||
const auto processState = std::make_shared<FakeProcessState>();
|
||||
CpuProfiler profiler(
|
||||
profilePath,
|
||||
"/proc/self/exe",
|
||||
std::chrono::seconds(10),
|
||||
1,
|
||||
FakeProcessFactory(processState));
|
||||
ASSERT_FALSE(profiler.Start().has_value());
|
||||
|
||||
std::atomic<uint64_t> work = 0;
|
||||
const auto workUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(50);
|
||||
while (std::chrono::steady_clock::now() < workUntil) { work.fetch_add(1); }
|
||||
profiler.Flush();
|
||||
|
||||
const auto waitUntil = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (profiler.Snapshot().recording && std::chrono::steady_clock::now() < waitUntil) {
|
||||
@@ -111,6 +151,30 @@ TEST(CpuProfilerTest, AutomaticallyStopsAtProfileSizeLimit) {
|
||||
EXPECT_TRUE(snapshot.profileAvailable);
|
||||
EXPECT_GT(snapshot.profileSizeBytes, 1);
|
||||
EXPECT_GT(work.load(), 0);
|
||||
EXPECT_EQ(processState->stopCount, 1);
|
||||
|
||||
std::filesystem::remove(profilePath);
|
||||
}
|
||||
|
||||
TEST(CpuProfilerTest, ReportsProcessStartupFailure) {
|
||||
const std::string profilePath =
|
||||
(std::filesystem::temp_directory_path() /
|
||||
("shardok-cpu-profiler-failure-test-" + std::to_string(getpid()) + ".prof"))
|
||||
.string();
|
||||
std::filesystem::remove(profilePath);
|
||||
|
||||
const auto processState = std::make_shared<FakeProcessState>();
|
||||
processState->startError = "perf_event_open is not permitted";
|
||||
CpuProfiler profiler(
|
||||
profilePath,
|
||||
"/proc/self/exe",
|
||||
CpuProfiler::kMaxDuration,
|
||||
CpuProfiler::kMaxProfileSizeBytes,
|
||||
FakeProcessFactory(processState));
|
||||
|
||||
EXPECT_EQ(profiler.Start(), processState->startError);
|
||||
EXPECT_FALSE(profiler.Snapshot().recording);
|
||||
EXPECT_FALSE(profiler.Snapshot().profileAvailable);
|
||||
|
||||
std::filesystem::remove(profilePath);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user