Skip to content

Commit 767ed14

Browse files
authored
feat(core): Add defrag support for json objects (#6023)
* core: Add utility to reallocate a JSON object Taking an existing JSON object as input, it is serialized and then deserialized, allocating a new object which is a copy of the old one, but now exists in the page reserved for malloc. It can be used to defragment a JSON object. * core: Add defrag support for json objects * core: Add tests for json defrag Signed-off-by: Abhijat Malviya <[email protected]>
1 parent 0a9ef95 commit 767ed14

File tree

5 files changed

+101
-2
lines changed

5 files changed

+101
-2
lines changed

src/core/compact_object.cc

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ void RobjWrapper::MakeInnerRoom(size_t current_cap, size_t desired, MemoryResour
723723
} // namespace detail
724724

725725
uint32_t JsonEnconding() {
726-
static thread_local uint32_t json_enc =
726+
thread_local uint32_t json_enc =
727727
absl::GetFlag(FLAGS_experimental_flat_json) ? kEncodingJsonFlat : kEncodingJsonCons;
728728
return json_enc;
729729
}
@@ -1094,6 +1094,8 @@ bool CompactObj::DefragIfNeeded(PageUsage* page_usage) {
10941094
return false;
10951095
case SMALL_TAG:
10961096
return u_.small_str.DefragIfNeeded(page_usage);
1097+
case JSON_TAG:
1098+
return u_.json_obj.DefragIfNeeded(page_usage);
10971099
case INT_TAG:
10981100
page_usage->RecordNotRequired();
10991101
// this is not relevant in this case
@@ -1620,6 +1622,44 @@ MemoryResource* CompactObj::memory_resource() {
16201622
return tl.local_mr;
16211623
}
16221624

1625+
bool CompactObj::JsonConsT::DefragIfNeeded(PageUsage* page_usage) {
1626+
if (JsonType* old = json_ptr; ShouldDefragment(page_usage)) {
1627+
json_ptr = AllocateMR<JsonType>(DeepCopyJSON(old, memory_resource()));
1628+
DeleteMR<JsonType>(old);
1629+
return true;
1630+
}
1631+
return false;
1632+
}
1633+
1634+
bool CompactObj::JsonConsT::ShouldDefragment(PageUsage* page_usage) const {
1635+
bool should_defragment = false;
1636+
json_ptr->compute_memory_size([&page_usage, &should_defragment](const void* p) {
1637+
should_defragment |= page_usage->IsPageForObjectUnderUtilized(const_cast<void*>(p));
1638+
return 0;
1639+
});
1640+
return should_defragment;
1641+
}
1642+
1643+
bool CompactObj::FlatJsonT::DefragIfNeeded(PageUsage* page_usage) {
1644+
if (uint8_t* old = flat_ptr; page_usage->IsPageForObjectUnderUtilized(old)) {
1645+
const uint32_t size = json_len;
1646+
flat_ptr = static_cast<uint8_t*>(tl.local_mr->allocate(size, kAlignSize));
1647+
memcpy(flat_ptr, old, size);
1648+
tl.local_mr->deallocate(old, size, kAlignSize);
1649+
return true;
1650+
}
1651+
1652+
return false;
1653+
}
1654+
1655+
bool CompactObj::JsonWrapper::DefragIfNeeded(PageUsage* page_usage) {
1656+
if (JsonEnconding() == kEncodingJsonCons) {
1657+
return cons.DefragIfNeeded(page_usage);
1658+
}
1659+
1660+
return flat.DefragIfNeeded(page_usage);
1661+
}
1662+
16231663
constexpr std::pair<CompactObjType, std::string_view> kObjTypeToString[8] = {
16241664
{OBJ_STRING, "string"sv}, {OBJ_LIST, "list"sv}, {OBJ_SET, "set"sv},
16251665
{OBJ_ZSET, "zset"sv}, {OBJ_HASH, "hash"sv}, {OBJ_STREAM, "stream"sv},

src/core/compact_object.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,18 +492,28 @@ class CompactObj {
492492
struct JsonConsT {
493493
JsonType* json_ptr;
494494
size_t bytes_used;
495+
496+
bool DefragIfNeeded(PageUsage* page_usage);
497+
498+
// Computes if the contained object should be defragmented, by examining pointers within it and
499+
// returning true if any of them reside in an underutilized page.
500+
bool ShouldDefragment(PageUsage* page_usage) const;
495501
};
496502

497503
struct FlatJsonT {
498504
uint32_t json_len;
499505
uint8_t* flat_ptr;
506+
507+
bool DefragIfNeeded(PageUsage* page_usage);
500508
};
501509

502510
struct JsonWrapper {
503511
union {
504512
JsonConsT cons;
505513
FlatJsonT flat;
506514
};
515+
516+
bool DefragIfNeeded(PageUsage* page_usage);
507517
};
508518

509519
// My main data structure. Union of representations.

src/core/json/json_object.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,4 +43,12 @@ optional<JsonType> JsonFromString(string_view input, PMR_NS::memory_resource* mr
4343
return nullopt;
4444
}
4545

46+
JsonType DeepCopyJSON(const JsonType* j, PMR_NS::memory_resource* mr) {
47+
std::string serialized;
48+
j->dump(serialized);
49+
auto deserialized = JsonFromString(serialized, mr);
50+
DCHECK(deserialized.has_value());
51+
return std::move(deserialized.value());
52+
}
53+
4654
} // namespace dfly

src/core/json/json_object.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ using JsonType = jsoncons::pmr::json;
2626
// Build a json object from string. If the string is not legal json, will return nullopt
2727
std::optional<JsonType> JsonFromString(std::string_view input, PMR_NS::memory_resource* mr);
2828

29+
// Deep copy a JSON object, by first serializing it to a string and then deserializing the string.
30+
// The operation is intended to help during defragmentation, by copying into a page reserved for
31+
// malloc.
32+
JsonType DeepCopyJSON(const JsonType* j, PMR_NS::memory_resource* mr);
33+
2934
inline auto MakeJsonPathExpr(std::string_view path, std::error_code& ec)
3035
-> jsoncons::jsonpath::jsonpath_expression<JsonType> {
3136
return jsoncons::jsonpath::make_expression<JsonType, std::allocator<char>>(

src/core/page_usage_stats_test.cc

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,26 @@
44

55
#include "core/page_usage_stats.h"
66

7+
#include <absl/flags/reflection.h>
78
#include <gmock/gmock-matchers.h>
89

910
#include "base/gtest.h"
1011
#include "base/logging.h"
1112
#include "core/compact_object.h"
12-
#include "core/mi_memory_resource.h"
1313
#include "core/qlist.h"
1414
#include "core/score_map.h"
1515
#include "core/small_string.h"
1616
#include "core/sorted_map.h"
1717
#include "core/string_map.h"
1818
#include "core/string_set.h"
19+
#include "redis/redis_aux.h"
1920

2021
extern "C" {
2122
#include "redis/zmalloc.h"
2223
}
2324

25+
ABSL_DECLARE_FLAG(bool, experimental_flat_json);
26+
2427
using namespace dfly;
2528

2629
class PageUsageStatsTest : public ::testing::Test {
@@ -45,6 +48,8 @@ class PageUsageStatsTest : public ::testing::Test {
4548
}
4649

4750
void SetUp() override {
51+
CompactObj::InitThreadLocal(&m_);
52+
4853
score_map_ = std::make_unique<ScoreMap>(&m_);
4954
sorted_map_ = std::make_unique<detail::SortedMap>(&m_);
5055
string_set_ = std::make_unique<StringSet>(&m_);
@@ -176,3 +181,34 @@ TEST_F(PageUsageStatsTest, StatCollection) {
176181
const CollectedPageStats::ShardUsageSummary expected{{50, 65}, {90, 85}, {99, 89}};
177182
EXPECT_EQ(usage.at(1), expected);
178183
}
184+
185+
TEST_F(PageUsageStatsTest, JSONCons) {
186+
// Because of the static encoding it is not possible to easily test the flat encoding. Once the
187+
// encoding flag is set, it is not re-read. If friend class is used to access the compact object
188+
// inner fields and call `DefragIfNeeded` directly on the flat variant of the union, the test will
189+
// still fail. This is because freeing the compact object code path takes the wrong branch based
190+
// on encoding. The flat encoding was tested manually adjusting this same test with changed
191+
// encoding.
192+
std::string_view data{R"#({"data": "some", "count": 1, "checked": false})#"};
193+
194+
auto parsed = JsonFromString(data, &m_);
195+
EXPECT_TRUE(parsed.has_value());
196+
197+
c_obj_.SetJson(std::move(parsed.value()));
198+
199+
PageUsage p{CollectPageStats::YES, 0.1};
200+
p.SetForceReallocate(true);
201+
202+
c_obj_.DefragIfNeeded(&p);
203+
204+
const auto stats = p.CollectedStats();
205+
EXPECT_GT(stats.pages_scanned, 0);
206+
EXPECT_EQ(stats.objects_skipped_not_required, 0);
207+
208+
EXPECT_EQ(c_obj_.ObjType(), OBJ_JSON);
209+
210+
auto json_obj = c_obj_.GetJson();
211+
EXPECT_EQ(json_obj->at("data").as_string_view(), "some");
212+
EXPECT_EQ(json_obj->at("count").as_integer<uint8_t>(), 1);
213+
EXPECT_EQ(json_obj->at("checked").as_bool(), false);
214+
}

0 commit comments

Comments
 (0)