Skip to content

Commit 19860cf

Browse files
authored
[Issue #1266] Implementing a high-performance visibility management solution (#1267)
This PR introduces a high-performance visibility management scheme based on __Epoch + COW + Lock-Free List__. __Key Components:__ 1. __EpochManager:__ - Implements an epoch-based memory reclamation mechanism. - Query threads declare active status via `enterEpoch()` and `exitEpoch()`. - GC threads advance the Epoch after retiring old data, only releasing memory safely once all threads in the old Epoch have exited. 2. __VersionedData & COW:__ - Encapsulates the base bitmap and deletion record list head within `VersionedData`. - GC operations create new `VersionedData` copies via Copy-on-Write. - Utilises atomic compare-and-swap (CAS) on `std::atomic<VersionedData*> currentVersion` to ensure query threads consistently observe a coherent snapshot.
1 parent e7b0477 commit 19860cf

File tree

8 files changed

+1001
-139
lines changed

8 files changed

+1001
-139
lines changed

cpp/pixels-retina/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ include_directories(${CMAKE_SOURCE_DIR}/include)
3838

3939
# Source files
4040
set(SOURCES
41+
lib/EpochManager.cpp
4142
lib/TileVisibility.cpp
4243
lib/RGVisibility.cpp
4344
lib/RGVisibilityJni.cpp
@@ -63,6 +64,7 @@ install(TARGETS pixels-retina
6364
# Add the test executable
6465
add_executable(tile_visibility_tests test/TileVisibilityTest.cpp)
6566
add_executable(rg_visibility_tests test/RGVisibilityTest.cpp)
67+
add_executable(visibility_perf_tests test/VisibilityPerformanceTest.cpp)
6668

6769
# Link the test executable with the library
6870
target_link_libraries(tile_visibility_tests
@@ -73,10 +75,15 @@ target_link_libraries(rg_visibility_tests
7375
pixels-retina
7476
GTest::gtest_main
7577
)
78+
target_link_libraries(visibility_perf_tests
79+
pixels-retina
80+
GTest::gtest_main
81+
)
7682

7783
include(GoogleTest)
7884
gtest_discover_tests(tile_visibility_tests)
7985
gtest_discover_tests(rg_visibility_tests)
86+
gtest_discover_tests(visibility_perf_tests)
8087

8188
# Set build type to Debug if not specified
8289
# if (NOT CMAKE_BUILD_TYPE)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2025 PixelsDB.
3+
*
4+
* This file is part of Pixels.
5+
*
6+
* Pixels is free software: you can redistribute it and/or modify
7+
* it under the terms of the Affero GNU General Public License as
8+
* published by the Free Software Foundation, either version 3 of
9+
* the License, or (at your option) any later version.
10+
*
11+
* Pixels is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* Affero GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the Affero GNU General Public
17+
* License along with Pixels. If not, see
18+
* <https://www.gnu.org/licenses/>.
19+
*/
20+
#ifndef PIXELS_RETINA_EPOCH_MANAGER_H
21+
#define PIXELS_RETINA_EPOCH_MANAGER_H
22+
23+
#include <atomic>
24+
#include <thread>
25+
#include <mutex>
26+
27+
/**
28+
* EpochManager - A global epoch-based memory reclamation system
29+
*
30+
* This manager allows safe deferred deletion of objects in a concurrent
31+
* environment using epoch-based reclamation. Threads announce their presence
32+
* in an epoch, and objects can only be reclaimed when all threads have
33+
* advanced past the epoch in which the object was retired.
34+
*/
35+
class EpochManager {
36+
public:
37+
static EpochManager& getInstance() {
38+
static EpochManager instance;
39+
return instance;
40+
}
41+
42+
/**
43+
* Enter the critical section. Thread announces it's participating in
44+
* the current epoch.
45+
*/
46+
void enterEpoch();
47+
48+
/**
49+
* Exit the critical section. Thread leaves the epoch.
50+
*/
51+
void exitEpoch();
52+
53+
/**
54+
* Advance the global epoch counter. Should be called by GC operations.
55+
* Returns the new epoch value.
56+
*/
57+
uint64_t advanceEpoch();
58+
59+
/**
60+
* Check if an object retired at the given epoch can be safely reclaimed.
61+
* An object can be reclaimed if all threads have advanced past its retire epoch.
62+
*/
63+
bool canReclaim(uint64_t retireEpoch) const;
64+
65+
/**
66+
* Get the current global epoch.
67+
*/
68+
uint64_t getCurrentEpoch() const {
69+
return globalEpoch.load(std::memory_order_acquire);
70+
}
71+
72+
private:
73+
EpochManager() : globalEpoch(1) {}
74+
~EpochManager();
75+
76+
EpochManager(const EpochManager&) = delete;
77+
EpochManager& operator=(const EpochManager&) = delete;
78+
79+
struct ThreadInfo {
80+
std::atomic<uint64_t> localEpoch{0}; // 0 means not in critical section
81+
std::atomic<bool> active{true};
82+
std::thread::id threadId;
83+
ThreadInfo* next{nullptr};
84+
};
85+
86+
std::atomic<uint64_t> globalEpoch;
87+
std::atomic<ThreadInfo*> threadListHead{nullptr};
88+
std::mutex threadListMutex; // Protects thread list modifications
89+
90+
ThreadInfo* getOrCreateThreadInfo();
91+
static thread_local ThreadInfo* tlsThreadInfo;
92+
};
93+
94+
/**
95+
* RAII helper for epoch protection
96+
*/
97+
class EpochGuard {
98+
public:
99+
EpochGuard() {
100+
EpochManager::getInstance().enterEpoch();
101+
}
102+
103+
~EpochGuard() {
104+
EpochManager::getInstance().exitEpoch();
105+
}
106+
107+
EpochGuard(const EpochGuard&) = delete;
108+
EpochGuard& operator=(const EpochGuard&) = delete;
109+
};
110+
111+
#endif // PIXELS_RETINA_EPOCH_MANAGER_H

cpp/pixels-retina/include/RGVisibility.h

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,12 @@ class RGVisibility {
4040

4141
private:
4242
static constexpr uint32_t VISIBILITY_RECORD_CAPACITY = 256;
43-
static constexpr uint32_t MAX_ACCESS_COUNT = 0x007FFFFF;
44-
static constexpr uint32_t GC_MASK = 0xFF000000;
45-
static constexpr uint32_t ACCESS_MASK = 0x00FFFFFF;
46-
static constexpr uint32_t ACCESS_INC = 0x00000001;
4743
static constexpr uint32_t BITMAP_SIZE_PER_TILE_VISIBILITY = 4;
48-
static constexpr uint32_t RG_READ_LEASE_MS = 100;
4944

5045
TileVisibility* tileVisibilities;
5146
const uint64_t tileCount;
52-
std::atomic<uint32_t> flag; // high 1 byte is the gc flag, low 3 bytes are the access count
5347

5448
TileVisibility* getTileVisibility(uint32_t rowId) const;
55-
void beginRGAccess();
56-
void endRGAccess();
5749
};
5850

5951
#endif //RG_VISIBILITY_H

cpp/pixels-retina/include/TileVisibility.h

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <cassert>
3030
#include <cstddef>
3131
#include <cstdint>
32+
#include <vector>
3233

3334
inline uint64_t makeDeleteIndex(uint8_t rowId, uint64_t ts) {
3435
return (static_cast<uint64_t>(rowId) << 56 | (ts & 0x00FFFFFFFFFFFFFFULL));
@@ -48,6 +49,41 @@ struct DeleteIndexBlock {
4849
std::atomic<DeleteIndexBlock *> next{nullptr};
4950
};
5051

52+
/**
53+
* VersionedData - A versioned snapshot of the base state
54+
* Used for Copy-on-Write during garbage collection
55+
* IMPORTANT: head is part of the version to ensure atomic visibility
56+
*/
57+
struct VersionedData {
58+
uint64_t baseBitmap[4];
59+
uint64_t baseTimestamp;
60+
DeleteIndexBlock* head; // Delete chain head, part of the version
61+
62+
VersionedData() : baseTimestamp(0), head(nullptr) {
63+
baseBitmap[0] = baseBitmap[1] = baseBitmap[2] = baseBitmap[3] = 0;
64+
}
65+
66+
VersionedData(uint64_t ts, const uint64_t bitmap[4], DeleteIndexBlock* h)
67+
: baseTimestamp(ts), head(h) {
68+
baseBitmap[0] = bitmap[0];
69+
baseBitmap[1] = bitmap[1];
70+
baseBitmap[2] = bitmap[2];
71+
baseBitmap[3] = bitmap[3];
72+
}
73+
};
74+
75+
/**
76+
* RetiredVersion - Tracks a retired version for epoch-based reclamation
77+
*/
78+
struct RetiredVersion {
79+
VersionedData* data;
80+
DeleteIndexBlock* blocksToDelete; // Head of the chain to delete
81+
uint64_t retireEpoch;
82+
83+
RetiredVersion(VersionedData* d, DeleteIndexBlock* b, uint64_t e)
84+
: data(d), blocksToDelete(b), retireEpoch(e) {}
85+
};
86+
5187
class TileVisibility {
5288
public:
5389
TileVisibility();
@@ -61,11 +97,12 @@ class TileVisibility {
6197
TileVisibility(const TileVisibility &) = delete;
6298
TileVisibility &operator=(const TileVisibility &) = delete;
6399

64-
uint64_t baseBitmap[4];
65-
uint64_t baseTimestamp;
66-
std::atomic<DeleteIndexBlock *> head;
100+
void reclaimRetiredVersions();
101+
102+
std::atomic<VersionedData*> currentVersion;
67103
std::atomic<DeleteIndexBlock *> tail;
68104
std::atomic<size_t> tailUsed;
105+
std::vector<RetiredVersion> retired; // Protected by GC (single writer)
69106
};
70107

71108
#endif // PIXELS_RETINA_TILE_VISIBILITY_H
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2025 PixelsDB.
3+
*
4+
* This file is part of Pixels.
5+
*
6+
* Pixels is free software: you can redistribute it and/or modify
7+
* it under the terms of the Affero GNU General Public License as
8+
* published by the Free Software Foundation, either version 3 of
9+
* the License, or (at your option) any later version.
10+
*
11+
* Pixels is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* Affero GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the Affero GNU General Public
17+
* License along with Pixels. If not, see
18+
* <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
#include "EpochManager.h"
22+
23+
// Thread-local storage for thread info
24+
thread_local EpochManager::ThreadInfo* EpochManager::tlsThreadInfo = nullptr;
25+
26+
EpochManager::~EpochManager() {
27+
ThreadInfo* current = threadListHead.load(std::memory_order_acquire);
28+
while (current) {
29+
ThreadInfo* next = current->next;
30+
delete current;
31+
current = next;
32+
}
33+
}
34+
35+
EpochManager::ThreadInfo* EpochManager::getOrCreateThreadInfo() {
36+
if (tlsThreadInfo) {
37+
return tlsThreadInfo;
38+
}
39+
40+
// Create new thread info
41+
ThreadInfo* newInfo = new ThreadInfo();
42+
newInfo->threadId = std::this_thread::get_id();
43+
44+
// Add to global list
45+
std::lock_guard<std::mutex> lock(threadListMutex);
46+
newInfo->next = threadListHead.load(std::memory_order_relaxed);
47+
threadListHead.store(newInfo, std::memory_order_release);
48+
49+
tlsThreadInfo = newInfo;
50+
return newInfo;
51+
}
52+
53+
void EpochManager::enterEpoch() {
54+
ThreadInfo* info = getOrCreateThreadInfo();
55+
uint64_t currentEpoch = globalEpoch.load(std::memory_order_acquire);
56+
info->localEpoch.store(currentEpoch, std::memory_order_release);
57+
}
58+
59+
void EpochManager::exitEpoch() {
60+
if (tlsThreadInfo) {
61+
tlsThreadInfo->localEpoch.store(0, std::memory_order_release);
62+
}
63+
}
64+
65+
uint64_t EpochManager::advanceEpoch() {
66+
return globalEpoch.fetch_add(1, std::memory_order_acq_rel) + 1;
67+
}
68+
69+
bool EpochManager::canReclaim(uint64_t retireEpoch) const {
70+
// Scan all threads to find the minimum active epoch
71+
ThreadInfo* current = threadListHead.load(std::memory_order_acquire);
72+
73+
while (current) {
74+
if (!current->active.load(std::memory_order_acquire)) {
75+
current = current->next;
76+
continue;
77+
}
78+
79+
uint64_t localEpoch = current->localEpoch.load(std::memory_order_acquire);
80+
81+
// localEpoch == 0 means the thread is not in critical section, skip it
82+
if (localEpoch != 0 && localEpoch <= retireEpoch) {
83+
// Found a thread still in or before the retire epoch
84+
return false;
85+
}
86+
87+
current = current->next;
88+
}
89+
90+
// All threads have advanced past the retire epoch
91+
return true;
92+
}

0 commit comments

Comments
 (0)