mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-28 20:55:44 +00:00
Run Shardok profiles until stopped (#8770)
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <gperftools/profiler.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
@@ -27,16 +28,22 @@ auto FileSize(const std::string& path) -> int64_t {
|
||||
|
||||
} // namespace
|
||||
|
||||
CpuProfiler::CpuProfiler(std::string profilePath, std::string executablePath)
|
||||
CpuProfiler::CpuProfiler(
|
||||
std::string profilePath,
|
||||
std::string executablePath,
|
||||
const std::chrono::milliseconds maxDuration,
|
||||
const int64_t maxProfileSizeBytes)
|
||||
: 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);
|
||||
}
|
||||
|
||||
CpuProfiler::~CpuProfiler() { Stop(); }
|
||||
|
||||
auto CpuProfiler::Start(const int32_t durationSeconds) -> std::optional<std::string> {
|
||||
auto CpuProfiler::Start() -> std::optional<std::string> {
|
||||
auto state = state_;
|
||||
std::lock_guard lock(state->mutex);
|
||||
if (state->recording) { return "A Shardok CPU profile is already recording"; }
|
||||
@@ -52,15 +59,22 @@ auto CpuProfiler::Start(const int32_t durationSeconds) -> std::optional<std::str
|
||||
}
|
||||
|
||||
state->recording = true;
|
||||
state->durationSeconds = durationSeconds;
|
||||
state->startedAtEpochMillis = EpochMillisNow();
|
||||
const uint64_t generation = ++state->generation;
|
||||
const auto deadline = std::chrono::steady_clock::now() + state->maxDuration;
|
||||
const auto monitorInterval = std::min(std::chrono::milliseconds(1000), state->maxDuration);
|
||||
|
||||
std::thread([state = std::move(state), generation, durationSeconds]() {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(durationSeconds));
|
||||
std::lock_guard autoStopLock(state->mutex);
|
||||
if (state->recording && state->generation == generation) {
|
||||
CpuProfiler::StopLocked(*state);
|
||||
std::thread([state = std::move(state), generation, deadline, monitorInterval]() {
|
||||
while (true) {
|
||||
std::this_thread::sleep_for(monitorInterval);
|
||||
std::lock_guard monitorLock(state->mutex);
|
||||
if (!state->recording || state->generation != generation) { return; }
|
||||
|
||||
if (std::chrono::steady_clock::now() >= deadline ||
|
||||
FileSize(state->profilePath) >= state->maxProfileSizeBytes) {
|
||||
CpuProfiler::StopLocked(*state);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}).detach();
|
||||
|
||||
@@ -89,7 +103,6 @@ auto CpuProfiler::Snapshot() const -> CpuProfileSnapshot {
|
||||
return CpuProfileSnapshot{
|
||||
.recording = state_->recording,
|
||||
.profileAvailable = profileSize > 0,
|
||||
.durationSeconds = state_->durationSeconds,
|
||||
.startedAtEpochMillis = state_->startedAtEpochMillis,
|
||||
.profileSizeBytes = profileSize,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
|
||||
#define EAGLE0_SHARDOK_SERVER_CPU_PROFILER_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -12,7 +13,6 @@ namespace shardok {
|
||||
struct CpuProfileSnapshot {
|
||||
bool recording;
|
||||
bool profileAvailable;
|
||||
int32_t durationSeconds;
|
||||
int64_t startedAtEpochMillis;
|
||||
int64_t profileSizeBytes;
|
||||
};
|
||||
@@ -22,10 +22,11 @@ private:
|
||||
struct State {
|
||||
std::mutex mutex;
|
||||
bool recording = false;
|
||||
int32_t durationSeconds = 0;
|
||||
int64_t startedAtEpochMillis = 0;
|
||||
uint64_t generation = 0;
|
||||
std::string profilePath;
|
||||
std::chrono::milliseconds maxDuration;
|
||||
int64_t maxProfileSizeBytes;
|
||||
};
|
||||
|
||||
std::shared_ptr<State> state_;
|
||||
@@ -35,16 +36,20 @@ private:
|
||||
|
||||
public:
|
||||
static constexpr int32_t kSampleFrequencyHz = 100;
|
||||
static constexpr std::chrono::milliseconds kMaxDuration = std::chrono::hours(1);
|
||||
static constexpr int64_t kMaxProfileSizeBytes = 100 * 1024 * 1024;
|
||||
|
||||
explicit CpuProfiler(
|
||||
std::string profilePath = "/tmp/shardok-admin.prof",
|
||||
std::string executablePath = "/proc/self/exe");
|
||||
std::string executablePath = "/proc/self/exe",
|
||||
std::chrono::milliseconds maxDuration = kMaxDuration,
|
||||
int64_t maxProfileSizeBytes = kMaxProfileSizeBytes);
|
||||
~CpuProfiler();
|
||||
|
||||
CpuProfiler(const CpuProfiler&) = delete;
|
||||
auto operator=(const CpuProfiler&) -> CpuProfiler& = delete;
|
||||
|
||||
auto Start(int32_t durationSeconds) -> std::optional<std::string>;
|
||||
auto Start() -> std::optional<std::string>;
|
||||
void Stop();
|
||||
void Flush();
|
||||
[[nodiscard]] auto Snapshot() const -> CpuProfileSnapshot;
|
||||
|
||||
@@ -81,7 +81,6 @@ void PopulateCpuProfileStatus(
|
||||
CpuProfileStatusResponse *response) {
|
||||
response->set_recording(snapshot.recording);
|
||||
response->set_profile_available(snapshot.profileAvailable);
|
||||
response->set_duration_seconds(snapshot.durationSeconds);
|
||||
response->set_started_at_epoch_millis(snapshot.startedAtEpochMillis);
|
||||
response->set_profile_size_bytes(snapshot.profileSizeBytes);
|
||||
response->set_sample_frequency_hz(CpuProfiler::kSampleFrequencyHz);
|
||||
@@ -148,17 +147,11 @@ auto EagleInterfaceImpl::GetCpuProfileStatus(
|
||||
|
||||
auto EagleInterfaceImpl::StartCpuProfile(
|
||||
ServerContext *context,
|
||||
const StartCpuProfileRequest *request,
|
||||
const StartCpuProfileRequest * /*request*/,
|
||||
CpuProfileStatusResponse *response) -> Status {
|
||||
if (auto authStatus = tokenValidator_.ValidateOrStatus(context)) { return *authStatus; }
|
||||
|
||||
const int32_t durationSeconds = request->duration_seconds();
|
||||
if (durationSeconds != 30 && durationSeconds != 60 && durationSeconds != 120) {
|
||||
return Status(
|
||||
StatusCode::INVALID_ARGUMENT,
|
||||
"Profile duration must be 30, 60, or 120 seconds");
|
||||
}
|
||||
if (const auto error = cpuProfiler_.Start(durationSeconds)) {
|
||||
if (const auto error = cpuProfiler_.Start()) {
|
||||
return Status(StatusCode::FAILED_PRECONDITION, *error);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
shardokpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/common/shardok_internal_interface"
|
||||
@@ -29,7 +28,6 @@ var (
|
||||
type shardokProfileStatusResponse struct {
|
||||
Recording bool `json:"recording"`
|
||||
ProfileAvailable bool `json:"profile_available"`
|
||||
DurationSeconds int32 `json:"duration_seconds,omitempty"`
|
||||
StartedAtEpochMs int64 `json:"started_at_epoch_millis,omitempty"`
|
||||
ProfileSizeBytes int64 `json:"profile_size_bytes,omitempty"`
|
||||
SampleFrequencyHz int32 `json:"sample_frequency_hz,omitempty"`
|
||||
@@ -107,7 +105,6 @@ func profileStatusResponse(status *shardokpb.CpuProfileStatusResponse) shardokPr
|
||||
return shardokProfileStatusResponse{
|
||||
Recording: status.GetRecording(),
|
||||
ProfileAvailable: status.GetProfileAvailable(),
|
||||
DurationSeconds: status.GetDurationSeconds(),
|
||||
StartedAtEpochMs: status.GetStartedAtEpochMillis(),
|
||||
ProfileSizeBytes: status.GetProfileSizeBytes(),
|
||||
SampleFrequencyHz: status.GetSampleFrequencyHz(),
|
||||
@@ -146,19 +143,9 @@ func handleShardokProfileStart(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
duration, err := strconv.Atoi(r.URL.Query().Get("duration"))
|
||||
if err != nil || (duration != 30 && duration != 60 && duration != 120) {
|
||||
writeShardokProfileJSON(w, http.StatusBadRequest, shardokProfileStatusResponse{
|
||||
Error: "Profile duration must be 30, 60, or 120 seconds",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := shardokProfileContext(r.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
status, err := shardokProfileClient.StartCpuProfile(ctx, &shardokpb.StartCpuProfileRequest{
|
||||
DurationSeconds: int32(duration),
|
||||
})
|
||||
status, err := shardokProfileClient.StartCpuProfile(ctx, &shardokpb.StartCpuProfileRequest{})
|
||||
if err != nil {
|
||||
writeShardokProfileJSON(w, http.StatusBadGateway, shardokProfileStatusResponse{Error: err.Error()})
|
||||
return
|
||||
|
||||
@@ -15,7 +15,6 @@ func TestShardokProfileStatusResponse(t *testing.T) {
|
||||
response := profileStatusResponse(&shardokpb.CpuProfileStatusResponse{
|
||||
Recording: true,
|
||||
ProfileAvailable: true,
|
||||
DurationSeconds: 60,
|
||||
StartedAtEpochMillis: 1234,
|
||||
ProfileSizeBytes: 5678,
|
||||
SampleFrequencyHz: 100,
|
||||
@@ -24,7 +23,7 @@ func TestShardokProfileStatusResponse(t *testing.T) {
|
||||
if !response.Recording || !response.ProfileAvailable {
|
||||
t.Fatalf("unexpected status flags: %+v", response)
|
||||
}
|
||||
if response.DurationSeconds != 60 || response.SampleFrequencyHz != 100 {
|
||||
if response.SampleFrequencyHz != 100 {
|
||||
t.Fatalf("unexpected profile settings: %+v", response)
|
||||
}
|
||||
if response.StartedAtEpochMs != 1234 || response.ProfileSizeBytes != 5678 {
|
||||
|
||||
@@ -54,12 +54,10 @@
|
||||
<dialog id="shardok-profile-dialog">
|
||||
<article style="max-width: 440px;">
|
||||
<h3>Record Shardok CPU Profile</h3>
|
||||
<p>Choose a bounded recording duration. The recording stops automatically.</p>
|
||||
<p>The recording runs until you stop it, or automatically stops after one hour or 100 MB.</p>
|
||||
<footer style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button class="secondary outline" onclick="document.getElementById('shardok-profile-dialog').close()">Cancel</button>
|
||||
<button class="secondary" onclick="startShardokProfile(30)">30 sec</button>
|
||||
<button class="secondary" onclick="startShardokProfile(60)">60 sec</button>
|
||||
<button onclick="startShardokProfile(120)">120 sec</button>
|
||||
<button onclick="startShardokProfile()">Start</button>
|
||||
</footer>
|
||||
</article>
|
||||
</dialog>
|
||||
@@ -179,7 +177,7 @@
|
||||
|
||||
if (data.recording) {
|
||||
container.innerHTML = `
|
||||
<span class="jfr-status jfr-on">Shardok CPU: ON (${data.duration_seconds}s)</span>
|
||||
<span class="jfr-status jfr-on">Shardok CPU: ON</span>
|
||||
<button onclick="stopShardokProfile()" class="btn-small secondary">Stop</button>
|
||||
`;
|
||||
} else {
|
||||
@@ -204,9 +202,9 @@
|
||||
document.getElementById('shardok-profile-dialog').showModal();
|
||||
}
|
||||
|
||||
function startShardokProfile(durationSeconds) {
|
||||
function startShardokProfile() {
|
||||
document.getElementById('shardok-profile-dialog').close();
|
||||
htmx.ajax('POST', `/shardok-profile/start?duration=${durationSeconds}`, {
|
||||
htmx.ajax('POST', '/shardok-profile/start', {
|
||||
target: '#shardok-profile-controls',
|
||||
swap: 'none'
|
||||
});
|
||||
|
||||
@@ -40,7 +40,7 @@ service ShardokInternalInterface {
|
||||
message CpuProfileStatusRequest {}
|
||||
|
||||
message StartCpuProfileRequest {
|
||||
int32 duration_seconds = 1;
|
||||
reserved 1;
|
||||
}
|
||||
|
||||
message StopCpuProfileRequest {}
|
||||
@@ -48,7 +48,7 @@ message StopCpuProfileRequest {}
|
||||
message CpuProfileStatusResponse {
|
||||
bool recording = 1;
|
||||
bool profile_available = 2;
|
||||
int32 duration_seconds = 3;
|
||||
reserved 3;
|
||||
int64 started_at_epoch_millis = 4;
|
||||
int64 profile_size_bytes = 5;
|
||||
int32 sample_frequency_hz = 6;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/server/CpuProfiler.hpp"
|
||||
|
||||
@@ -20,7 +21,7 @@ namespace {
|
||||
#define EAGLE0_ADDRESS_SANITIZER_ENABLED 1
|
||||
#endif
|
||||
|
||||
TEST(CpuProfilerTest, RecordsBoundedProfileAndRejectsConcurrentStart) {
|
||||
TEST(CpuProfilerTest, RecordsUntilStoppedAndRejectsConcurrentStart) {
|
||||
#if defined(EAGLE0_ADDRESS_SANITIZER_ENABLED)
|
||||
GTEST_SKIP() << "gperftools stack sampling is incompatible with ASan stack redzones";
|
||||
#endif
|
||||
@@ -32,9 +33,9 @@ TEST(CpuProfilerTest, RecordsBoundedProfileAndRejectsConcurrentStart) {
|
||||
std::filesystem::remove(profilePath);
|
||||
|
||||
CpuProfiler profiler(profilePath, "/proc/self/exe");
|
||||
ASSERT_FALSE(profiler.Start(30).has_value());
|
||||
ASSERT_FALSE(profiler.Start().has_value());
|
||||
EXPECT_TRUE(profiler.Snapshot().recording);
|
||||
EXPECT_TRUE(profiler.Start(30).has_value());
|
||||
EXPECT_TRUE(profiler.Start().has_value());
|
||||
|
||||
std::atomic<uint64_t> work = 0;
|
||||
const auto workUntil = std::chrono::steady_clock::now() + std::chrono::milliseconds(250);
|
||||
@@ -50,5 +51,69 @@ TEST(CpuProfilerTest, RecordsBoundedProfileAndRejectsConcurrentStart) {
|
||||
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);
|
||||
|
||||
CpuProfiler profiler(
|
||||
profilePath,
|
||||
"/proc/self/exe",
|
||||
std::chrono::milliseconds(50),
|
||||
CpuProfiler::kMaxProfileSizeBytes);
|
||||
ASSERT_FALSE(profiler.Start().has_value());
|
||||
|
||||
const auto waitUntil = std::chrono::steady_clock::now() + std::chrono::seconds(2);
|
||||
while (profiler.Snapshot().recording && std::chrono::steady_clock::now() < waitUntil) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
const CpuProfileSnapshot snapshot = profiler.Snapshot();
|
||||
EXPECT_FALSE(snapshot.recording);
|
||||
EXPECT_TRUE(snapshot.profileAvailable);
|
||||
EXPECT_GT(snapshot.profileSizeBytes, 0);
|
||||
|
||||
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);
|
||||
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) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
const CpuProfileSnapshot snapshot = profiler.Snapshot();
|
||||
EXPECT_FALSE(snapshot.recording);
|
||||
EXPECT_TRUE(snapshot.profileAvailable);
|
||||
EXPECT_GT(snapshot.profileSizeBytes, 1);
|
||||
EXPECT_GT(work.load(), 0);
|
||||
|
||||
std::filesystem::remove(profilePath);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace shardok
|
||||
|
||||
Reference in New Issue
Block a user