Skip to content

Commit 51a78a3

Browse files
authored
feat(server): using memory defrag with per shard mem info (#616)
Signed-off-by: Boaz Sade <boaz@dragonflydb.io>
1 parent 1286bac commit 51a78a3

File tree

6 files changed

+252
-59
lines changed

6 files changed

+252
-59
lines changed

src/core/compact_object_test.cc

+140
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,146 @@ class CompactObjectTest : public ::testing::Test {
108108
string tmp_;
109109
};
110110

111+
TEST_F(CompactObjectTest, WastedMemoryDetection) {
112+
mi_option_set(mi_option_decommit_delay, 0);
113+
114+
size_t allocated = 0, commited = 0, wasted = 0;
115+
// By setting the threshold to high value we are expecting
116+
// To find locations where we have wasted memory
117+
float ratio = 0.8;
118+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
119+
EXPECT_EQ(allocated, 0);
120+
EXPECT_EQ(commited, 0);
121+
EXPECT_EQ(wasted, (commited - allocated));
122+
123+
std::size_t allocated_mem = 64;
124+
auto* myheap = mi_heap_get_backing();
125+
126+
void* p1 = mi_heap_malloc(myheap, 64);
127+
128+
void* ptrs_end[50];
129+
for (size_t i = 0; i < 50; ++i) {
130+
ptrs_end[i] = mi_heap_malloc(myheap, 128);
131+
allocated_mem += 128;
132+
}
133+
134+
allocated = commited = wasted = 0;
135+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
136+
EXPECT_EQ(allocated, allocated_mem);
137+
EXPECT_GT(commited, allocated_mem);
138+
EXPECT_EQ(wasted, (commited - allocated));
139+
void* ptr[50];
140+
// allocate 50
141+
for (size_t i = 0; i < 50; ++i) {
142+
ptr[i] = mi_heap_malloc(myheap, 256);
143+
allocated_mem += 256;
144+
}
145+
146+
// At this point all the blocks has committed > 0 and used > 0
147+
// and since we expecting to find these locations, the size of
148+
// wasted == commited memory - allocated memory.
149+
allocated = commited = wasted = 0;
150+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
151+
EXPECT_EQ(allocated, allocated_mem);
152+
EXPECT_GT(commited, allocated_mem);
153+
EXPECT_EQ(wasted, (commited - allocated));
154+
155+
// free 50/50 -
156+
for (size_t i = 0; i < 50; ++i) {
157+
mi_free(ptr[i]);
158+
allocated_mem -= 256;
159+
}
160+
161+
// After all the memory at block size 256 is free, we would have commited there
162+
// but the used is expected to be 0, so the number now is different from the
163+
// case above
164+
allocated = commited = wasted = 0;
165+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
166+
EXPECT_EQ(allocated, allocated_mem);
167+
EXPECT_GT(commited, allocated_mem);
168+
// since we release all 256 memory block, it should not be counted
169+
EXPECT_EQ(wasted, (commited - allocated));
170+
for (size_t i = 0; i < 50; ++i) {
171+
mi_free(ptrs_end[i]);
172+
}
173+
mi_free(p1);
174+
175+
// Now that its all freed, we are not expecting to have any wasted memory any more
176+
allocated = commited = wasted = 0;
177+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
178+
EXPECT_EQ(allocated, 0);
179+
EXPECT_GT(commited, allocated);
180+
EXPECT_EQ(wasted, (commited - allocated));
181+
182+
mi_collect(false);
183+
}
184+
185+
TEST_F(CompactObjectTest, WastedMemoryDontCount) {
186+
// The commited memory per blocks are:
187+
// 64bit => 4K
188+
// 128bit => 8k
189+
// 256 => 16k
190+
// and so on, which mean every n * sizeof(ptr) ^ 2 == 2^11*2*(n-1) (where n starts with 1)
191+
constexpr std::size_t kExpectedFor256MemWasted = 0x4000; // memory block 256
192+
mi_option_set(mi_option_decommit_delay, 0);
193+
auto* myheap = mi_heap_get_backing();
194+
195+
size_t allocated = 0, commited = 0, wasted = 0;
196+
// By setting the threshold to a very low number
197+
// we don't expect to find and locations where memory is wasted
198+
float ratio = 0.01;
199+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
200+
EXPECT_EQ(allocated, 0);
201+
EXPECT_EQ(commited, 0);
202+
EXPECT_EQ(wasted, (commited - allocated));
203+
204+
std::size_t allocated_mem = 64;
205+
206+
void* p1 = mi_heap_malloc(myheap, 64);
207+
208+
void* ptrs_end[50];
209+
for (size_t i = 0; i < 50; ++i) {
210+
ptrs_end[i] = mi_heap_malloc(myheap, 128);
211+
(void)p1;
212+
allocated_mem += 128;
213+
}
214+
215+
void* ptr[50];
216+
217+
// allocate 50
218+
for (size_t i = 0; i < 50; ++i) {
219+
ptr[i] = mi_heap_malloc(myheap, 256);
220+
allocated_mem += 256;
221+
}
222+
allocated = commited = wasted = 0;
223+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
224+
// Threshold is low so we are not expecting any wasted memory to be found.
225+
EXPECT_EQ(allocated, allocated_mem);
226+
EXPECT_GT(commited, allocated_mem);
227+
EXPECT_EQ(wasted, 0);
228+
229+
// free 50/50 -
230+
for (size_t i = 0; i < 50; ++i) {
231+
mi_free(ptr[i]);
232+
allocated_mem -= 256;
233+
}
234+
allocated = commited = wasted = 0;
235+
zmalloc_get_allocator_wasted_blocks(ratio, &allocated, &commited, &wasted);
236+
237+
EXPECT_EQ(allocated, allocated_mem);
238+
EXPECT_GT(commited, allocated_mem);
239+
// We will detect only wasted memory for block size of
240+
// 256 - and all of it is wasted.
241+
EXPECT_EQ(wasted, kExpectedFor256MemWasted);
242+
// Threshold is low so we are not expecting any wasted memory to be found.
243+
for (size_t i = 0; i < 50; ++i) {
244+
mi_free(ptrs_end[i]);
245+
}
246+
mi_free(p1);
247+
248+
mi_collect(false);
249+
}
250+
111251
TEST_F(CompactObjectTest, Basic) {
112252
robj* rv = createRawStringObject("foo", 3);
113253
cobj_.ImportRObj(rv);

src/redis/zmalloc.h

+7
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ size_t zmalloc_get_smap_bytes_by_field(char *field, long pid);
111111
size_t zmalloc_get_memory_size(void);
112112
size_t zmalloc_usable_size(const void* p);
113113

114+
/* get the memory usage + the number of wasted locations of memory
115+
Based on a given threshold (ratio < 1).
116+
Note that if a block is not used, it would not counted as wasted
117+
*/
118+
int zmalloc_get_allocator_wasted_blocks(float ratio, size_t* allocated, size_t* commited,
119+
size_t* wasted);
120+
114121
/*
115122
* checks whether a page that the pointer ptr located at is underutilized.
116123
* This uses the current local thread heap.

src/redis/zmalloc_mi.c

+35
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,13 @@ typedef struct Sum_s {
108108
size_t comitted;
109109
} Sum_t;
110110

111+
typedef struct {
112+
size_t allocated;
113+
size_t comitted;
114+
size_t wasted;
115+
float ratio;
116+
} MemUtilized_t;
117+
111118
bool heap_visit_cb(const mi_heap_t* heap, const mi_heap_area_t* area, void* block,
112119
size_t block_size, void* arg) {
113120
assert(area->used < (1u << 31));
@@ -117,7 +124,23 @@ bool heap_visit_cb(const mi_heap_t* heap, const mi_heap_area_t* area, void* bloc
117124
// mimalloc mistakenly exports used in blocks instead of bytes.
118125
sum->allocated += block_size * area->used;
119126
sum->comitted += area->committed;
127+
return true; // continue iteration
128+
};
120129

130+
bool heap_count_wasted_blocks(const mi_heap_t* heap, const mi_heap_area_t* area, void* block,
131+
size_t block_size, void* arg) {
132+
assert(area->used < (1u << 31));
133+
134+
MemUtilized_t* sum = (MemUtilized_t*)arg;
135+
136+
// mimalloc mistakenly exports used in blocks instead of bytes.
137+
size_t used = block_size * area->used;
138+
sum->allocated += used;
139+
sum->comitted += area->committed;
140+
141+
if (used < area->committed * sum->ratio) {
142+
sum->wasted += (area->committed - used);
143+
}
121144
return true; // continue iteration
122145
};
123146

@@ -132,6 +155,18 @@ int zmalloc_get_allocator_info(size_t* allocated, size_t* active, size_t* reside
132155
return 1;
133156
}
134157

158+
int zmalloc_get_allocator_wasted_blocks(float ratio, size_t* allocated, size_t* commited,
159+
size_t* wasted) {
160+
MemUtilized_t sum = {.allocated = 0, .comitted = 0, .wasted = 0, .ratio = ratio};
161+
162+
mi_heap_visit_blocks(zmalloc_heap, false /* visit all blocks*/, heap_count_wasted_blocks, &sum);
163+
*allocated = sum.allocated;
164+
*commited = sum.comitted;
165+
*wasted = sum.wasted;
166+
167+
return 1;
168+
}
169+
135170
void init_zmalloc_threadlocal(void* heap) {
136171
if (zmalloc_heap)
137172
return;

src/server/dragonfly_test.cc

+16-28
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ extern "C" {
2020
#include "server/main_service.h"
2121
#include "server/test_utils.h"
2222

23-
ABSL_DECLARE_FLAG(float, commit_use_threshold);
23+
ABSL_DECLARE_FLAG(float, mem_defrag_threshold);
2424

2525
namespace dfly {
2626

@@ -788,32 +788,22 @@ TEST_F(DflyEngineTest, Issue706) {
788788
}
789789

790790
TEST_F(DefragDflyEngineTest, TestDefragOption) {
791-
absl::SetFlag(&FLAGS_commit_use_threshold, 1.1);
792-
// Fill data into dragonfly and then check if we have
793-
// any location in memory to defrag. See issue #448 for details about this.
791+
absl::SetFlag(&FLAGS_mem_defrag_threshold, 0.02);
792+
// Fill data into dragonfly and then check if we have
793+
// any location in memory to defrag. See issue #448 for details about this.
794794
constexpr size_t kMaxMemoryForTest = 1'100'000;
795795
constexpr int kNumberOfKeys = 1'000; // this fill the memory
796796
constexpr int kKeySize = 637;
797-
constexpr int kMaxDefragTriesForTests = 10;
798-
constexpr int kFactor = 10;
799-
constexpr int kMaxNumKeysToDelete = 100;
797+
constexpr int kMaxDefragTriesForTests = 30;
798+
constexpr int kFactor = 4;
800799

801800
max_memory_limit = kMaxMemoryForTest; // control memory size so no need for too many keys
802-
shard_set->TEST_EnableHeartBeat(); // enable use memory update (used_mem_current)
803-
804801
std::vector<std::string> keys2delete;
805802
keys2delete.push_back("del");
806803

807-
// Generate a list of keys that would be deleted
808-
// The keys that we will delete are all in the form of "key-name:1<other digits>"
809-
// This is because we are populating keys that has this format, but we don't want
810-
// to delete all keys, only some random keys so we deleting those that start with 1
811-
int current_step = kFactor;
812-
for (int i = 1; i < kMaxNumKeysToDelete; current_step *= kFactor) {
813-
for (; i < current_step; i++) {
814-
int j = i - 1 + current_step;
815-
keys2delete.push_back("key-name:" + std::to_string(j));
816-
}
804+
// create keys that we would like to remove, try to make it none adjusting locations
805+
for (int i = 0; i < kNumberOfKeys; i += kFactor) {
806+
keys2delete.push_back("key-name:" + std::to_string(i));
817807
}
818808

819809
std::vector<std::string_view> keys(keys2delete.begin(), keys2delete.end());
@@ -829,32 +819,30 @@ TEST_F(DefragDflyEngineTest, TestDefragOption) {
829819
EngineShard* shard = EngineShard::tlocal();
830820
ASSERT_FALSE(shard == nullptr); // we only have one and its should not be empty!
831821
fibers_ext::SleepFor(100ms);
832-
auto mem_used = 0;
822+
833823
// make sure that the task that collect memory usage from all shard ran
834824
// for at least once, and that no defrag was done yet.
835-
for (int i = 0; i < 3 && mem_used == 0; i++) {
825+
auto stats = shard->stats();
826+
for (int i = 0; i < 3; i++) {
836827
fibers_ext::SleepFor(100ms);
837-
EXPECT_EQ(shard->stats().defrag_realloc_total, 0);
838-
mem_used = used_mem_current.load(memory_order_relaxed);
828+
EXPECT_EQ(stats.defrag_realloc_total, 0);
839829
}
840830
});
841831

842832
ArgSlice delete_cmd(keys);
843833
r = CheckedInt(delete_cmd);
834+
LOG(WARNING) << "finish deleting memory entries " << r;
844835
// the first element in this is the command del so size is one less
845-
ASSERT_EQ(r, kMaxNumKeysToDelete - 1);
836+
ASSERT_EQ(r, keys2delete.size() - 1);
846837
// At this point we need to see whether we did running the task and whether the task did something
847838
shard_set->pool()->AwaitFiberOnAll([&](unsigned index, ProactorBase* base) {
848839
EngineShard* shard = EngineShard::tlocal();
849840
ASSERT_FALSE(shard == nullptr); // we only have one and its should not be empty!
850841
// a "busy wait" to ensure that memory defragmentations was successful:
851842
// the task ran and did it work
852843
auto stats = shard->stats();
853-
for (int i = 0; i < kMaxDefragTriesForTests; i++) {
844+
for (int i = 0; i < kMaxDefragTriesForTests && stats.defrag_realloc_total == 0; i++) {
854845
stats = shard->stats();
855-
if (stats.defrag_realloc_total > 0) {
856-
break;
857-
}
858846
fibers_ext::SleepFor(220ms);
859847
}
860848
// make sure that we successfully found places to defrag in memory

0 commit comments

Comments
 (0)