Skip to content

Commit 0b8f56d

Browse files
authored
Add PageView get_keys function; Introduce StableStringStore (#174)
* Initial commit * Fixed submodule issue * Addressed first set of feedback
1 parent 92009a6 commit 0b8f56d

File tree

5 files changed

+331
-0
lines changed

5 files changed

+331
-0
lines changed

src/llfs/page_view.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,12 @@ Status PageView::validate(PageId expected_id)
3737
return OkStatus();
3838
}
3939

40+
StatusOr<usize> PageView::get_keys([[maybe_unused]] LowerBoundParam lower_bound,
41+
[[maybe_unused]] KeyView* key_buffer_out,
42+
[[maybe_unused]] usize key_buffer_size,
43+
[[maybe_unused]] StableStringStore& storage) const
44+
{
45+
return StatusOr<usize>{batt::StatusCode::kUnimplemented};
46+
}
47+
4048
} // namespace llfs

src/llfs/page_view.hpp

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include <llfs/page_layout.hpp>
1919
//#include <llfs/page_loader.hpp>
2020
#include <llfs/seq.hpp>
21+
#include <llfs/stable_string_store.hpp>
2122
#include <llfs/user_data.hpp>
2223

2324
#include <batteries/async/mutex.hpp>
@@ -32,6 +33,7 @@ class PageView
3233
{
3334
public:
3435
//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
36+
using LowerBoundParam = std::variant<KeyView, usize>;
3537

3638
explicit PageView(std::shared_ptr<const PageBuffer>&& data) noexcept
3739
: data_{std::move(data)}
@@ -92,6 +94,30 @@ class PageView
9294
*/
9395
virtual Optional<KeyView> max_key() const = 0;
9496

97+
/** \brief Retrieves at most `key_buffer_size` number of keys contained in this page.
98+
*
99+
* \param lower_bound This parameter allows for "skipping" to an arbitrary place in the page's key
100+
* set. The caller can provide either a `KeyView` value or an index into the key set, which
101+
* represents the starting key from which this function will collect keys from to return.
102+
*
103+
* \param key_buffer_out The output buffer that will be filled by this function with the requested
104+
* keys.
105+
*
106+
* \param key_buffer_size The size of the output buffer holding the returned keys.
107+
*
108+
* \param storage A `StableStringStore` instance that the caller can provide so that the returned
109+
* keys can still be a list of `KeyView` even if the keys in the page are stored in a way that
110+
* isn't contiguous or are compressed. Specific implementations of `PageView` will choose to use
111+
* this based on their key storage.
112+
*
113+
* \return The number of keys filled into `key_buffer_out`. This value will either be
114+
* `key_buffer_size` or the number of keys between `lower_bound` and the end of the key set,
115+
* whichever is smaller. In the event that the `lower_bound` parameter provided does not exist in
116+
* the key set (or is out of the range of the key set), this function will return 0.
117+
*/
118+
virtual StatusOr<usize> get_keys(LowerBoundParam lower_bound, KeyView* key_buffer_out,
119+
usize key_buffer_size, StableStringStore& storage) const;
120+
95121
// Builds a key-based approximate member query (AMQ) filter for the page, to answer the question
96122
// whether a given key *might* be contained by the page.
97123
//

src/llfs/stable_string_store.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//#=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++
2+
//
3+
// Part of the LLFS Project, under Apache License v2.0.
4+
// See https://www.apache.org/licenses/LICENSE-2.0 for license information.
5+
// SPDX short identifier: Apache-2.0
6+
//
7+
//+++++++++++-+-+--+----- --- -- - - - -
8+
9+
#include <llfs/stable_string_store.hpp>
10+
//
11+
12+
#include <llfs/data_packer.hpp>
13+
14+
#include <batteries/algo/parallel_copy.hpp>
15+
#include <batteries/assert.hpp>
16+
#include <batteries/math.hpp>
17+
18+
namespace llfs {
19+
//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
20+
//
21+
StableStringStore::StableStringStore() : free_chunk_{this->chunk0_.data(), this->chunk0_.size()}
22+
{
23+
}
24+
25+
//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
26+
//
27+
MutableBuffer StableStringStore::allocate(usize n)
28+
{
29+
// Check if the current free_chunk_ is large enough to hold n bytes. If it isn't, we need to
30+
// dynamically allocate a new chunk.
31+
//
32+
if (this->free_chunk_.size() < n) {
33+
// Allocate new chunk, add it to the list of dynamically allocated chunks, and point free_chunk_
34+
// to this new chunk.
35+
//
36+
const usize new_chunk_size = batt::round_up_bits(batt::log2_ceil(kDynamicAllocSize), n);
37+
std::unique_ptr<char[]> new_chunk{new char[new_chunk_size]};
38+
char* const new_chunk_data = new_chunk.get();
39+
this->chunks_.emplace_back(std::move(new_chunk));
40+
this->free_chunk_ = MutableBuffer{new_chunk_data, new_chunk_size};
41+
}
42+
43+
BATT_CHECK_GE(this->free_chunk_.size(), n);
44+
45+
// Return the newly allocated chunk and advance the start of the free_chunk_ buffer by n bytes to
46+
// indicate that this region of memory is now occupied.
47+
//
48+
MutableBuffer stable_buffer{this->free_chunk_.data(), n};
49+
this->free_chunk_ += n;
50+
return stable_buffer;
51+
}
52+
53+
//==#==========+==+=+=++=+++++++++++-+-+--+----- --- -- - - - -
54+
//
55+
std::string_view StableStringStore::store(const std::string_view& s, batt::WorkerPool& worker_pool)
56+
{
57+
// Allocate a buffer the size of the input string data.
58+
//
59+
MutableBuffer stable_buffer = this->allocate(s.size());
60+
61+
BATT_CHECK_EQ(stable_buffer.size(), s.size());
62+
63+
// Check if we would benefit from parallelizing the copying process. If we do have workers in the
64+
// worker_pool and the size of the string data isn't too small, parallelize.
65+
//
66+
if (worker_pool.size() == 0 || s.size() < llfs::DataPacker::min_parallel_copy_size()) {
67+
std::memcpy(stable_buffer.data(), s.data(), s.size());
68+
} else {
69+
batt::ScopedWorkContext work_context{worker_pool};
70+
71+
const batt::TaskCount max_tasks{worker_pool.size() + 1};
72+
const batt::TaskSize min_task_size{llfs::DataPacker::min_parallel_copy_size()};
73+
74+
const char* const src_begin = s.data();
75+
const char* const src_end = src_begin + s.size();
76+
char* const dst_begin = static_cast<char*>(stable_buffer.data());
77+
78+
batt::parallel_copy(work_context, src_begin, src_end, dst_begin, min_task_size, max_tasks);
79+
}
80+
81+
// Return the copy.
82+
//
83+
return std::string_view{static_cast<const char*>(stable_buffer.data()), stable_buffer.size()};
84+
}
85+
86+
} // namespace llfs

src/llfs/stable_string_store.hpp

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//#=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++
2+
//
3+
// Part of the LLFS Project, under Apache License v2.0.
4+
// See https://www.apache.org/licenses/LICENSE-2.0 for license information.
5+
// SPDX short identifier: Apache-2.0
6+
//
7+
//+++++++++++-+-+--+----- --- -- - - - -
8+
9+
#pragma once
10+
#ifndef LLFS_STABLE_STRING_STORE_HPP
11+
#define LLFS_STABLE_STRING_STORE_HPP
12+
13+
#include <llfs/buffer.hpp>
14+
#include <llfs/int_types.hpp>
15+
16+
#include <batteries/async/worker_pool.hpp>
17+
#include <batteries/buffer.hpp>
18+
19+
#include <array>
20+
#include <memory>
21+
#include <string_view>
22+
#include <vector>
23+
24+
namespace llfs {
25+
26+
//=#=#==#==#===============+=+=+=+=++=++++++++++++++-++-+--+-+----+---------------
27+
/** \brief A class that allows the user to efficiently allocate and copy string data in memory that
28+
* is scoped to the lifetime of the object itself.
29+
*/
30+
class StableStringStore
31+
{
32+
public:
33+
static constexpr usize kStaticAllocSize = 32;
34+
static constexpr usize kDynamicAllocSize = 4096;
35+
36+
StableStringStore();
37+
38+
StableStringStore(const StableStringStore&) = delete;
39+
StableStringStore& operator=(const StableStringStore&) = delete;
40+
41+
/** \brief Allocates a buffer of size `n` bytes.
42+
*/
43+
MutableBuffer allocate(usize n);
44+
45+
/** \brief Copies the given `string_view` into a memory location managed by this
46+
* `StableStringStore` instance, and returns a `string_view` pointing to the stored data. The
47+
* `worker_pool`, if provided, is used the parallelize the copying process if necessary.
48+
*/
49+
std::string_view store(const std::string_view& s,
50+
batt::WorkerPool& worker_pool = batt::WorkerPool::null_pool());
51+
52+
/** \brief Copies the given `ConstBuffer` into a memory location managed by this
53+
* `StableStringStore` instance as string data, and returns a `ConstBuffer` pointing to the stored
54+
* data.
55+
*/
56+
ConstBuffer store(const ConstBuffer& buffer,
57+
batt::WorkerPool& worker_pool = batt::WorkerPool::null_pool())
58+
{
59+
const std::string_view s = this->store(
60+
std::string_view{static_cast<const char*>(buffer.data()), buffer.size()}, worker_pool);
61+
62+
return ConstBuffer{s.data(), s.size()};
63+
}
64+
65+
/** \brief Concatenates multiple chunks of data and copies the concatenation into a contiguous
66+
* buffer of memory.
67+
*/
68+
template <typename... Parts>
69+
ConstBuffer concat(Parts&&... parts)
70+
{
71+
usize total_size = 0;
72+
73+
// Compute the total amount of memory needed to be allocated for the result of the
74+
// concatenation.
75+
//
76+
const auto add_to_total = [&total_size](auto&& part) {
77+
total_size += batt::as_const_buffer(part).size();
78+
return 0;
79+
};
80+
81+
(add_to_total(parts), ...);
82+
83+
MutableBuffer mbuf = this->allocate(total_size);
84+
MutableBuffer cbuf = mbuf;
85+
86+
// Copy each part to memory.
87+
//
88+
const auto copy_part = [&mbuf](auto&& part) {
89+
auto src = batt::as_const_buffer(part);
90+
std::memcpy(mbuf.data(), src.data(), src.size());
91+
mbuf += src.size();
92+
return 0;
93+
};
94+
95+
(copy_part(parts), ...);
96+
97+
return cbuf;
98+
}
99+
100+
private:
101+
/** \brief The statically allocated block of memory that is initialized when this
102+
* `StableStringStore` instance is created, used as a starting point for memory allocations done
103+
* by this instance.
104+
*/
105+
std::array<char, kStaticAllocSize> chunk0_;
106+
107+
/** \brief A collection of dynamically allocated memory blocks, managing the chunks allocated
108+
* beyond `chunk0_`.
109+
*/
110+
std::vector<std::unique_ptr<char[]>> chunks_;
111+
112+
/** \brief A buffer representing the current chunk of memory that has free space available for
113+
* allocation.
114+
*/
115+
MutableBuffer free_chunk_;
116+
};
117+
118+
} // namespace llfs
119+
120+
#endif // LLFS_STABLE_STRING_STORE_HPP
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//#=##=##=#==#=#==#===#+==#+==========+==+=+=+=+=+=++=+++=+++++=-++++=-+++++++++++
2+
//
3+
// Part of the LLFS Project, under Apache License v2.0.
4+
// See https://www.apache.org/licenses/LICENSE-2.0 for license information.
5+
// SPDX short identifier: Apache-2.0
6+
//
7+
//+++++++++++-+-+--+----- --- -- - - - -
8+
9+
#include <llfs/stable_string_store.hpp>
10+
//
11+
12+
#include <gmock/gmock.h>
13+
#include <gtest/gtest.h>
14+
15+
#include <string>
16+
17+
namespace {
18+
19+
using namespace batt::int_types;
20+
21+
TEST(StableStringStore, StaticAllocationTest)
22+
{
23+
llfs::StableStringStore strings;
24+
25+
auto out =
26+
strings.concat(std::string_view{"Hello"}, std::string_view{", "}, std::string_view{"World!"});
27+
28+
std::string_view out_str{(const char*)out.data(), out.size()};
29+
30+
EXPECT_THAT(out_str, ::testing::StrEq("Hello, World!"));
31+
32+
// Since "Hello, World!" is less than kStaticAllocSize, test to see that the string's data was
33+
// allocated statically, i.e., it "lives" inside the bounds of the StableStringStore object
34+
// itself.
35+
//
36+
EXPECT_TRUE(out.data() >= static_cast<const void*>(&strings) &&
37+
out.data() < static_cast<const void*>(&strings + 1));
38+
}
39+
40+
TEST(StableStringStore, DynamicAllocationTest)
41+
{
42+
llfs::StableStringStore strings;
43+
const usize data_size = 1;
44+
const usize num_iterations_of_static_alloc = strings.kStaticAllocSize / data_size;
45+
46+
// Statically allocate a bunch of string data up to the static allocation limit.
47+
//
48+
for (usize i = 0; i < num_iterations_of_static_alloc; ++i) {
49+
std::string_view string_to_store{"a"};
50+
std::string_view copied_string = strings.store(string_to_store);
51+
EXPECT_TRUE(
52+
static_cast<const void*>(copied_string.data()) >= static_cast<const void*>(&strings) &&
53+
static_cast<const void*>(copied_string.data()) < static_cast<const void*>(&strings + 1));
54+
}
55+
56+
// Now perform another store. Since we have already allocated an amount of data greater than the
57+
// size of kStaticAllocSize, we end up dynmically allocating the data for this string.
58+
//
59+
std::string_view dynamically_allocated_string{"b"};
60+
std::string_view copy_stored = strings.store(dynamically_allocated_string);
61+
EXPECT_TRUE(static_cast<const void*>(copy_stored.data()) < static_cast<const void*>(&strings) ||
62+
static_cast<const void*>(copy_stored.data()) >=
63+
static_cast<const void*>(&strings + 1));
64+
}
65+
66+
TEST(StableStringStore, LargeDynamicAllocationTest)
67+
{
68+
llfs::StableStringStore strings;
69+
const usize data_size = strings.kDynamicAllocSize + 1;
70+
const usize num_allocations = 10;
71+
72+
// Allocate large strings, all with a size greater that kDynamicAllocSize. This will trigger
73+
// multiple dynamic memory allocations.
74+
//
75+
std::string_view previous_string;
76+
for (usize i = 0; i < num_allocations; ++i) {
77+
if (i > 0) {
78+
// Check to make sure that the memory for previously allocated strings doesn't go out of
79+
// scope; memory of the string data is scoped to the lifetime of the StableStringObject.
80+
//
81+
std::string expected_previous_string(data_size, 'a' + (i - 1));
82+
EXPECT_EQ(previous_string, expected_previous_string);
83+
}
84+
85+
std::string large_string_data(data_size, 'a' + i);
86+
std::string_view string_to_store{large_string_data};
87+
previous_string = strings.store(string_to_store);
88+
}
89+
}
90+
91+
} // namespace

0 commit comments

Comments
 (0)