Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion tcmalloc/internal/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,22 @@ cc_library(
visibility = [
"//tcmalloc:__subpackages__",
],
deps = [":config"],
deps = [
":config",
"@com_google_absl//absl/base",
"@com_google_absl//absl/time",
],
)

cc_test(
name = "clock_test",
srcs = ["clock_test.cc"],
copts = TCMALLOC_DEFAULT_COPTS,
deps = [
":clock",
"@com_google_absl//absl/time",
"@com_google_googletest//:gtest_main",
],
)

cc_library(
Expand Down
78 changes: 76 additions & 2 deletions tcmalloc/internal/clock.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@

#include <stdint.h>

#include <atomic>

#include "absl/base/internal/cycleclock.h"
#include "absl/time/time.h"
#include "tcmalloc/internal/config.h"

GOOGLE_MALLOC_SECTION_BEGIN
Expand All @@ -28,10 +32,80 @@ namespace tcmalloc_internal {
// used implementations. Tests can use this interface to mock out the clock.
struct Clock {
// Returns the current time in ticks (relative to an arbitrary time base).
int64_t (*now)();
int64_t (*now)() = absl::base_internal::CycleClock::Now;

// Returns the number of ticks per second.
double (*freq)();
double (*freq)() = absl::base_internal::CycleClock::Frequency;

struct Snapshot {
int64_t now;
double freq;
};

Snapshot GetSnapshot() const { return Snapshot{now(), freq()}; }
};

// Encapsulates a 32-bit cycle timestamp by right-shifting clock.now() by
// kShift bits.
//
// Granularity and Epoch:
// - At 2 GHz: 1 tick = ~0.524 ms; 32-bit epoch = ~26.0 days before wraparound.
// - At 3 GHz: 1 tick = ~0.349 ms; 32-bit epoch = ~17.3 days before wraparound.
// - At 4 GHz: 1 tick = ~0.262 ms; 32-bit epoch = ~13.0 days before wraparound.
class Cycles32 {
public:
static constexpr int kShift = 20;

constexpr Cycles32() = default;
explicit constexpr Cycles32(uint32_t val) : val_(val) {}

// Updates the timestamp using clock.now() >> kShift.
void Update(Clock clock = Clock{}) {
uint32_t now_32 = static_cast<uint32_t>(clock.now() >> kShift);
if (now_32 == 0) now_32 = 1; // Reserve 0 as the uninitialized sentinel.
val_.store(now_32, std::memory_order_relaxed);
}

// Resets the timestamp to 0 (uninitialized sentinel).
void Reset() { val_.store(0, std::memory_order_relaxed); }

// Returns true if the timestamp is initialized (val_ != 0).
explicit operator bool() const {
return val_.load(std::memory_order_relaxed) != 0;
}

// Returns elapsed time since the recorded tick using a pre-taken snapshot.
// Returns absl::InfiniteDuration() if val_ == 0 (uninitialized).
absl::Duration AsDuration(Clock::Snapshot snap) const {
const uint32_t last = val_.load(std::memory_order_relaxed);
if (last == 0) return absl::InfiniteDuration();
const uint32_t now_32 = static_cast<uint32_t>(snap.now >> kShift);
// Unsigned 32-bit modular subtraction across wraparound is safe.
const uint32_t elapsed_ticks = now_32 - last;
const double elapsed_cycles =
static_cast<double>(elapsed_ticks) * (1 << kShift);
return absl::Seconds(elapsed_cycles / snap.freq);
}

// Convenience overload: takes a fresh clock snapshot.
absl::Duration AsDuration(Clock clock = Clock{}) const {
return AsDuration(clock.GetSnapshot());
}

// Returns true if this timestamp occurred after or at the same time as other
// across 32-bit wraparound boundaries (valid within half the epoch).
bool TimeAfterOrEqual(const Cycles32& other) const {
return TimeAfterOrEqual(raw(), other.raw());
}

uint32_t raw() const { return val_.load(std::memory_order_relaxed); }

private:
static bool TimeAfterOrEqual(uint32_t a, uint32_t b) {
return static_cast<int32_t>(a - b) >= 0;
}

std::atomic<uint32_t> val_{0};
};

} // namespace tcmalloc_internal
Expand Down
154 changes: 154 additions & 0 deletions tcmalloc/internal/clock_test.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// Copyright 2026 The TCMalloc Authors
//
// 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
//
// https://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 "tcmalloc/internal/clock.h"

#include <stdint.h>

#include "gtest/gtest.h"
#include "absl/time/time.h"

namespace tcmalloc {
namespace tcmalloc_internal {
namespace {

class Cycles32Test : public ::testing::Test {
protected:
void SetUp() override {
g_ticks_ = 0;
g_freq_ = 1e9; // 1 GHz -> 1 ns per cycle.
}

void TearDown() override { g_ticks_ = 0; }

static int64_t MockNow() { return g_ticks_; }
static double MockFreq() { return g_freq_; }

Clock mock_clock_{.now = MockNow, .freq = MockFreq};

static inline int64_t g_ticks_ = 0;
static inline double g_freq_ = 1e9;
};

TEST_F(Cycles32Test, Uninitialized) {
Cycles32 c;
EXPECT_EQ(c.raw(), 0);
EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration());
}

TEST_F(Cycles32Test, UpdateAndAsDuration) {
g_ticks_ = (100LL << Cycles32::kShift);

Cycles32 c;
c.Update(mock_clock_);
EXPECT_EQ(c.raw(), 100);

g_ticks_ = (250LL << Cycles32::kShift);
EXPECT_EQ(c.AsDuration(mock_clock_),
absl::Nanoseconds(150LL << Cycles32::kShift));
}

TEST_F(Cycles32Test, WraparoundModularSubtraction) {
Cycles32 c(0xFFFFFFF0u);

g_ticks_ = (10LL << Cycles32::kShift);
EXPECT_EQ(c.AsDuration(mock_clock_),
absl::Nanoseconds(26LL << Cycles32::kShift));
}

TEST_F(Cycles32Test, TimeAfterOrEqual) {
Cycles32 c10(10);
Cycles32 c20(20);
EXPECT_TRUE(c10.TimeAfterOrEqual(c10));
EXPECT_TRUE(c20.TimeAfterOrEqual(c10));
EXPECT_FALSE(c10.TimeAfterOrEqual(c20));

Cycles32 c_before(0xFFFFFFF0u);
Cycles32 c_after(10);
EXPECT_TRUE(c_after.TimeAfterOrEqual(c_before));
EXPECT_FALSE(c_before.TimeAfterOrEqual(c_after));
}

TEST_F(Cycles32Test, SentinelAliasingAtTickZero) {
g_ticks_ = 0; // Tick 0 right-shifts to 0.
Cycles32 c;
c.Update(mock_clock_);
EXPECT_EQ(c.raw(),
1); // Clamped to 1 to reserve 0 as uninitialized sentinel.
EXPECT_NE(c.AsDuration(mock_clock_), absl::InfiniteDuration());
}

TEST_F(Cycles32Test, ExactHalfEpochSignInversion) {
Cycles32 base(0);
Cycles32 max_forward(0x7FFFFFFFu);
Cycles32 sign_flip(0x80000000u);

EXPECT_TRUE(max_forward.TimeAfterOrEqual(base));
EXPECT_FALSE(sign_flip.TimeAfterOrEqual(base));

Cycles32 w_base(10);
Cycles32 w_forward(10 + 0x7FFFFFFFu);
Cycles32 w_flip(10 + 0x80000000u);
EXPECT_TRUE(w_forward.TimeAfterOrEqual(w_base));
EXPECT_FALSE(w_flip.TimeAfterOrEqual(w_base));
}

TEST_F(Cycles32Test, MaximumElapsedDurationExactness) {
Cycles32 c(1);
g_ticks_ = (0xFFFFFFFFLL << Cycles32::kShift);
// 0xFFFFFFFFu - 1 = 0xFFFFFFFEu ticks elapsed.
EXPECT_EQ(c.AsDuration(mock_clock_),
absl::Nanoseconds(0xFFFFFFFELL << Cycles32::kShift));
}

TEST_F(Cycles32Test, ZeroElapsedDuration) {
g_ticks_ = (500LL << Cycles32::kShift);
Cycles32 c;
c.Update(mock_clock_);
EXPECT_EQ(c.AsDuration(mock_clock_), absl::ZeroDuration());
}

TEST_F(Cycles32Test, SnapshotImmuneToDrift) {
g_ticks_ = (100LL << Cycles32::kShift);
Cycles32 c;
c.Update(mock_clock_);

g_ticks_ = (200LL << Cycles32::kShift);
auto snap = mock_clock_.GetSnapshot();

// Advance clock after snapshot was taken.
g_ticks_ = (500LL << Cycles32::kShift);

// AsDuration(snap) must use the snapshot (100 ticks elapsed), not g_ticks_
// (400 ticks).
EXPECT_EQ(c.AsDuration(snap), absl::Nanoseconds(100LL << Cycles32::kShift));
}

TEST_F(Cycles32Test, ResetAndBoolOperator) {
Cycles32 c;
EXPECT_FALSE(static_cast<bool>(c));
EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration());

c.Update(mock_clock_);
EXPECT_TRUE(static_cast<bool>(c));
EXPECT_NE(c.AsDuration(mock_clock_), absl::InfiniteDuration());

c.Reset();
EXPECT_FALSE(static_cast<bool>(c));
EXPECT_EQ(c.AsDuration(mock_clock_), absl::InfiniteDuration());
}

} // namespace
} // namespace tcmalloc_internal
} // namespace tcmalloc
Loading