Compare commits
19 Commits
master
...
property_s
Author | SHA1 | Date | |
---|---|---|---|
|
2f176d60a9 | ||
|
fdd5b132e2 | ||
|
796fe44c99 | ||
|
66376fea0e | ||
|
50e3e1d685 | ||
|
9b4c9c99ee | ||
|
6f4c62a895 | ||
|
cc883a9387 | ||
|
bd69a711c5 | ||
|
afc90297de | ||
|
a32e27c013 | ||
|
853dfe3c65 | ||
|
51301a8125 | ||
|
78fcb92738 | ||
|
bdf5d7633e | ||
|
e44bf85576 | ||
|
0ab4e0fa53 | ||
|
37f11a75d4 | ||
|
be66f03cc8 |
@ -300,6 +300,19 @@ endif()
|
||||
|
||||
option(ENABLE_JEMALLOC "Use jemalloc" ON)
|
||||
|
||||
option(MG_MEMORY_PROFILE "If build should be setup for memory profiling" OFF)
|
||||
if (MG_MEMORY_PROFILE AND ENABLE_JEMALLOC)
|
||||
message(STATUS "Jemalloc has been disabled because MG_MEMORY_PROFILE is enabled")
|
||||
set(ENABLE_JEMALLOC OFF)
|
||||
endif ()
|
||||
if (MG_MEMORY_PROFILE AND ASAN)
|
||||
message(STATUS "ASAN has been disabled because MG_MEMORY_PROFILE is enabled")
|
||||
set(ASAN OFF)
|
||||
endif ()
|
||||
if (MG_MEMORY_PROFILE)
|
||||
add_compile_definitions(MG_MEMORY_PROFILE)
|
||||
endif ()
|
||||
|
||||
if (ASAN)
|
||||
message(WARNING "Disabling jemalloc as it doesn't work well with ASAN")
|
||||
set(ENABLE_JEMALLOC OFF)
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -119,6 +119,8 @@ class Reader {
|
||||
auto GetHeader() const -> Header const &;
|
||||
auto GetNextRow(utils::MemoryResource *mem) -> std::optional<Row>;
|
||||
|
||||
void Reset();
|
||||
|
||||
private:
|
||||
// Some implementation issues that need clearing up, but this is mainly because
|
||||
// I don't want `boost/iostreams/filtering_stream.hpp` included in this header file
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -34,6 +34,10 @@ struct Reader::impl {
|
||||
|
||||
[[nodiscard]] bool HasHeader() const { return read_config_.with_header; }
|
||||
[[nodiscard]] auto Header() const -> Header const & { return header_; }
|
||||
void Reset() {
|
||||
line_buffer_.clear();
|
||||
line_buffer_.shrink_to_fit();
|
||||
}
|
||||
|
||||
auto GetNextRow(utils::MemoryResource *mem) -> std::optional<Reader::Row>;
|
||||
|
||||
@ -42,7 +46,7 @@ struct Reader::impl {
|
||||
|
||||
void TryInitializeHeader();
|
||||
|
||||
std::optional<utils::pmr::string> GetNextLine(utils::MemoryResource *mem);
|
||||
bool GetNextLine();
|
||||
|
||||
ParsingResult ParseHeader();
|
||||
|
||||
@ -55,6 +59,8 @@ struct Reader::impl {
|
||||
Config read_config_;
|
||||
uint64_t line_count_{1};
|
||||
uint16_t number_of_columns_{0};
|
||||
uint64_t estimated_number_of_columns_{0};
|
||||
utils::pmr::string line_buffer_{memory_};
|
||||
Reader::Header header_{memory_};
|
||||
};
|
||||
|
||||
@ -129,17 +135,16 @@ void Reader::impl::InitializeStream() {
|
||||
MG_ASSERT(csv_stream_.is_complete(), "Should be 'complete' for correct operation");
|
||||
}
|
||||
|
||||
std::optional<utils::pmr::string> Reader::impl::GetNextLine(utils::MemoryResource *mem) {
|
||||
utils::pmr::string line(mem);
|
||||
if (!std::getline(csv_stream_, line)) {
|
||||
bool Reader::impl::GetNextLine() {
|
||||
if (!std::getline(csv_stream_, line_buffer_)) {
|
||||
// reached end of file or an I/0 error occurred
|
||||
if (!csv_stream_.good()) {
|
||||
csv_stream_.reset(); // this will close the file_stream_ and clear the chain
|
||||
}
|
||||
return std::nullopt;
|
||||
return false;
|
||||
}
|
||||
++line_count_;
|
||||
return std::move(line);
|
||||
return true;
|
||||
}
|
||||
|
||||
Reader::ParsingResult Reader::impl::ParseHeader() {
|
||||
@ -170,6 +175,8 @@ void Reader::impl::TryInitializeHeader() {
|
||||
|
||||
const Reader::Header &Reader::GetHeader() const { return pimpl->Header(); }
|
||||
|
||||
void Reader::Reset() { pimpl->Reset(); }
|
||||
|
||||
namespace {
|
||||
enum class CsvParserState : uint8_t { INITIAL_FIELD, NEXT_FIELD, QUOTING, EXPECT_DELIMITER, DONE };
|
||||
|
||||
@ -179,6 +186,8 @@ Reader::ParsingResult Reader::impl::ParseRow(utils::MemoryResource *mem) {
|
||||
utils::pmr::vector<utils::pmr::string> row(mem);
|
||||
if (number_of_columns_ != 0) {
|
||||
row.reserve(number_of_columns_);
|
||||
} else if (estimated_number_of_columns_ != 0) {
|
||||
row.reserve(estimated_number_of_columns_);
|
||||
}
|
||||
|
||||
utils::pmr::string column(memory_);
|
||||
@ -186,13 +195,12 @@ Reader::ParsingResult Reader::impl::ParseRow(utils::MemoryResource *mem) {
|
||||
auto state = CsvParserState::INITIAL_FIELD;
|
||||
|
||||
do {
|
||||
const auto maybe_line = GetNextLine(mem);
|
||||
if (!maybe_line) {
|
||||
if (!GetNextLine()) {
|
||||
// The whole file was processed.
|
||||
break;
|
||||
}
|
||||
|
||||
std::string_view line_string_view = *maybe_line;
|
||||
std::string_view line_string_view = line_buffer_;
|
||||
|
||||
// remove '\r' from the end in case we have dos file format
|
||||
if (line_string_view.back() == '\r') {
|
||||
@ -312,6 +320,11 @@ Reader::ParsingResult Reader::impl::ParseRow(utils::MemoryResource *mem) {
|
||||
fmt::format("Expected {:d} columns in row {:d}, but got {:d}", number_of_columns_,
|
||||
line_count_ - 1, row.size()));
|
||||
}
|
||||
// To avoid unessisary dynamic growth of the row, remember the number of
|
||||
// columns for future calls
|
||||
if (number_of_columns_ == 0 && estimated_number_of_columns_ == 0) {
|
||||
estimated_number_of_columns_ = row.size();
|
||||
}
|
||||
|
||||
return std::move(row);
|
||||
}
|
||||
@ -319,7 +332,7 @@ Reader::ParsingResult Reader::impl::ParseRow(utils::MemoryResource *mem) {
|
||||
std::optional<Reader::Row> Reader::impl::GetNextRow(utils::MemoryResource *mem) {
|
||||
auto row = ParseRow(mem);
|
||||
|
||||
if (row.HasError()) {
|
||||
if (row.HasError()) [[unlikely]] {
|
||||
if (!read_config_.ignore_bad) {
|
||||
throw CsvReadException("CSV Reader: Bad row at line {:d}: {}", line_count_ - 1, row.GetError().message);
|
||||
}
|
||||
@ -333,7 +346,7 @@ std::optional<Reader::Row> Reader::impl::GetNextRow(utils::MemoryResource *mem)
|
||||
} while (row.HasError());
|
||||
}
|
||||
|
||||
if (row->empty()) {
|
||||
if (row->empty()) [[unlikely]] {
|
||||
// reached end of file
|
||||
return std::nullopt;
|
||||
}
|
||||
|
@ -59,12 +59,14 @@ class TypedValueResultStreamBase {
|
||||
public:
|
||||
explicit TypedValueResultStreamBase(memgraph::storage::Storage *storage);
|
||||
|
||||
std::vector<memgraph::communication::bolt::Value> DecodeValues(
|
||||
const std::vector<memgraph::query::TypedValue> &values) const;
|
||||
void DecodeValues(const std::vector<memgraph::query::TypedValue> &values);
|
||||
|
||||
auto AccessValues() const -> std::vector<memgraph::communication::bolt::Value> const & { return decoded_values_; }
|
||||
|
||||
protected:
|
||||
// NOTE: Needed only for ToBoltValue conversions
|
||||
memgraph::storage::Storage *storage_;
|
||||
std::vector<memgraph::communication::bolt::Value> decoded_values_;
|
||||
};
|
||||
|
||||
/// Wrapper around TEncoder which converts TypedValue to Value
|
||||
@ -75,16 +77,18 @@ class TypedValueResultStream : public TypedValueResultStreamBase {
|
||||
TypedValueResultStream(TEncoder *encoder, memgraph::storage::Storage *storage)
|
||||
: TypedValueResultStreamBase{storage}, encoder_(encoder) {}
|
||||
|
||||
void Result(const std::vector<memgraph::query::TypedValue> &values) { encoder_->MessageRecord(DecodeValues(values)); }
|
||||
void Result(const std::vector<memgraph::query::TypedValue> &values) {
|
||||
DecodeValues(values);
|
||||
encoder_->MessageRecord(AccessValues());
|
||||
}
|
||||
|
||||
private:
|
||||
TEncoder *encoder_;
|
||||
};
|
||||
|
||||
std::vector<memgraph::communication::bolt::Value> TypedValueResultStreamBase::DecodeValues(
|
||||
const std::vector<memgraph::query::TypedValue> &values) const {
|
||||
std::vector<memgraph::communication::bolt::Value> decoded_values;
|
||||
decoded_values.reserve(values.size());
|
||||
void TypedValueResultStreamBase::DecodeValues(const std::vector<memgraph::query::TypedValue> &values) {
|
||||
decoded_values_.reserve(values.size());
|
||||
decoded_values_.clear();
|
||||
for (const auto &v : values) {
|
||||
auto maybe_value = memgraph::glue::ToBoltValue(v, storage_, memgraph::storage::View::NEW);
|
||||
if (maybe_value.HasError()) {
|
||||
@ -99,9 +103,8 @@ std::vector<memgraph::communication::bolt::Value> TypedValueResultStreamBase::De
|
||||
throw memgraph::communication::bolt::ClientError("Unexpected storage error when streaming results.");
|
||||
}
|
||||
}
|
||||
decoded_values.emplace_back(std::move(*maybe_value));
|
||||
decoded_values_.emplace_back(std::move(*maybe_value));
|
||||
}
|
||||
return decoded_values;
|
||||
}
|
||||
|
||||
TypedValueResultStreamBase::TypedValueResultStreamBase(memgraph::storage::Storage *storage) : storage_(storage) {}
|
||||
|
@ -36,6 +36,19 @@ KVStore::KVStore(std::filesystem::path storage) : pimpl_(std::make_unique<impl>(
|
||||
pimpl_->db.reset(db);
|
||||
}
|
||||
|
||||
KVStore::KVStore(std::filesystem::path storage, rocksdb::Options db_options) : pimpl_(std::make_unique<impl>()) {
|
||||
pimpl_->storage = storage;
|
||||
pimpl_->options = std::move(db_options);
|
||||
if (!utils::EnsureDir(pimpl_->storage))
|
||||
throw KVStoreError("Folder for the key-value store " + pimpl_->storage.string() + " couldn't be initialized!");
|
||||
rocksdb::DB *db = nullptr;
|
||||
auto s = rocksdb::DB::Open(pimpl_->options, storage.c_str(), &db);
|
||||
if (!s.ok())
|
||||
throw KVStoreError("RocksDB couldn't be initialized inside " + storage.string() + " -- " +
|
||||
std::string(s.ToString()));
|
||||
pimpl_->db.reset(db);
|
||||
}
|
||||
|
||||
KVStore::~KVStore() {
|
||||
if (pimpl_ == nullptr) return;
|
||||
spdlog::debug("Destroying KVStore at {}", pimpl_->storage.string());
|
||||
|
@ -11,6 +11,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rocksdb/options.h>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
@ -43,6 +44,7 @@ class KVStore final {
|
||||
* storage directory because that will lead to undefined behaviour.
|
||||
*/
|
||||
explicit KVStore(std::filesystem::path storage);
|
||||
explicit KVStore(std::filesystem::path storage, rocksdb::Options db_options);
|
||||
|
||||
KVStore(const KVStore &other) = delete;
|
||||
KVStore(KVStore &&other);
|
||||
|
@ -42,6 +42,7 @@
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
#include "system/system.hpp"
|
||||
#include "telemetry/telemetry.hpp"
|
||||
#include "utils/on_scope_exit.hpp"
|
||||
#include "utils/signals.hpp"
|
||||
#include "utils/sysinfo/memory.hpp"
|
||||
#include "utils/system_info.hpp"
|
||||
@ -145,6 +146,8 @@ int main(int argc, char **argv) {
|
||||
// Unhandled exception handler init.
|
||||
std::set_terminate(&memgraph::utils::TerminateHandler);
|
||||
|
||||
memgraph::utils::OnScopeExit deinit_pds([]() { memgraph::storage::PDS::Deinit(); });
|
||||
|
||||
// Initialize Python
|
||||
auto *program_name = Py_DecodeLocale(argv[0], nullptr);
|
||||
MG_ASSERT(program_name);
|
||||
|
@ -68,7 +68,7 @@ ParsedQuery ParseQuery(const std::string &query_string, const std::map<std::stri
|
||||
parser = std::make_unique<frontend::opencypher::Parser>(stripped_query.query());
|
||||
} catch (const SyntaxException &e) {
|
||||
// There is a syntax exception in the stripped query. Re-run the parser
|
||||
// on the original query to get an appropriate error messsage.
|
||||
// on the original query to get an appropriate error message.
|
||||
parser = std::make_unique<frontend::opencypher::Parser>(query_string);
|
||||
|
||||
// If an exception was not thrown here, the stripper messed something
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -13,12 +13,12 @@
|
||||
|
||||
namespace memgraph::query {
|
||||
|
||||
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what) {
|
||||
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, std::string_view what) {
|
||||
TypedValue value = expr->Accept(*evaluator);
|
||||
try {
|
||||
return value.ValueInt();
|
||||
} catch (TypedValueException &e) {
|
||||
throw QueryRuntimeException(what + " must be an int");
|
||||
throw QueryRuntimeException(std::string(what) + " must be an int");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1209,7 +1209,7 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
/// @param what - Name of what's getting evaluated. Used for user feedback (via
|
||||
/// exception) when the evaluated value is not an int.
|
||||
/// @throw QueryRuntimeException if expression doesn't evaluate to an int.
|
||||
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what);
|
||||
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, std::string_view what);
|
||||
|
||||
std::optional<size_t> EvaluateMemoryLimit(ExpressionVisitor<TypedValue> &eval, Expression *memory_limit,
|
||||
size_t memory_scale);
|
||||
|
@ -246,27 +246,6 @@ std::optional<std::string> GetOptionalStringValue(query::Expression *expression,
|
||||
return {};
|
||||
};
|
||||
|
||||
bool IsAllShortestPathsQuery(const std::vector<memgraph::query::Clause *> &clauses) {
|
||||
for (const auto &clause : clauses) {
|
||||
if (clause->GetTypeInfo() != Match::kType) {
|
||||
continue;
|
||||
}
|
||||
auto *match_clause = utils::Downcast<Match>(clause);
|
||||
for (const auto &pattern : match_clause->patterns_) {
|
||||
for (const auto &atom : pattern->atoms_) {
|
||||
if (atom->GetTypeInfo() != EdgeAtom::kType) {
|
||||
continue;
|
||||
}
|
||||
auto *edge_atom = utils::Downcast<EdgeAtom>(atom);
|
||||
if (edge_atom->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
inline auto convertFromCoordinatorToReplicationMode(const CoordinatorQuery::SyncMode &sync_mode)
|
||||
-> replication_coordination_glue::ReplicationMode {
|
||||
switch (sync_mode) {
|
||||
@ -1670,8 +1649,7 @@ struct PullPlan {
|
||||
std::shared_ptr<QueryUserOrRole> user_or_role, std::atomic<TransactionStatus> *transaction_status,
|
||||
std::shared_ptr<utils::AsyncTimer> tx_timer,
|
||||
TriggerContextCollector *trigger_context_collector = nullptr,
|
||||
std::optional<size_t> memory_limit = {}, bool use_monotonic_memory = true,
|
||||
FrameChangeCollector *frame_change_collector_ = nullptr);
|
||||
std::optional<size_t> memory_limit = {}, FrameChangeCollector *frame_change_collector_ = nullptr);
|
||||
|
||||
std::optional<plan::ProfilingStatsWithTotalTime> Pull(AnyStream *stream, std::optional<int> n,
|
||||
const std::vector<Symbol> &output_symbols,
|
||||
@ -1696,26 +1674,17 @@ struct PullPlan {
|
||||
// we have to keep track of any unsent results from previous `PullPlan::Pull`
|
||||
// manually by using this flag.
|
||||
bool has_unsent_results_ = false;
|
||||
|
||||
// In the case of LOAD CSV, we want to use only PoolResource without MonotonicMemoryResource
|
||||
// to reuse allocated memory. As LOAD CSV is processing row by row
|
||||
// it is possible to reduce memory usage significantly if MemoryResource deals with memory allocation
|
||||
// can reuse memory that was allocated on processing the first row on all subsequent rows.
|
||||
// This flag signals to `PullPlan::Pull` which MemoryResource to use
|
||||
bool use_monotonic_memory_;
|
||||
};
|
||||
|
||||
PullPlan::PullPlan(const std::shared_ptr<PlanWrapper> plan, const Parameters ¶meters, const bool is_profile_query,
|
||||
DbAccessor *dba, InterpreterContext *interpreter_context, utils::MemoryResource *execution_memory,
|
||||
std::shared_ptr<QueryUserOrRole> user_or_role, std::atomic<TransactionStatus> *transaction_status,
|
||||
std::shared_ptr<utils::AsyncTimer> tx_timer, TriggerContextCollector *trigger_context_collector,
|
||||
const std::optional<size_t> memory_limit, bool use_monotonic_memory,
|
||||
FrameChangeCollector *frame_change_collector)
|
||||
const std::optional<size_t> memory_limit, FrameChangeCollector *frame_change_collector)
|
||||
: plan_(plan),
|
||||
cursor_(plan->plan().MakeCursor(execution_memory)),
|
||||
frame_(plan->symbol_table().max_position(), execution_memory),
|
||||
memory_limit_(memory_limit),
|
||||
use_monotonic_memory_(use_monotonic_memory) {
|
||||
memory_limit_(memory_limit) {
|
||||
ctx_.db_accessor = dba;
|
||||
ctx_.symbol_table = plan->symbol_table();
|
||||
ctx_.evaluation_context.timestamp = QueryTimestamp();
|
||||
@ -1741,6 +1710,7 @@ PullPlan::PullPlan(const std::shared_ptr<PlanWrapper> plan, const Parameters &pa
|
||||
ctx_.is_profile_query = is_profile_query;
|
||||
ctx_.trigger_context_collector = trigger_context_collector;
|
||||
ctx_.frame_change_collector = frame_change_collector;
|
||||
ctx_.evaluation_context.memory = execution_memory;
|
||||
}
|
||||
|
||||
std::optional<plan::ProfilingStatsWithTotalTime> PullPlan::Pull(AnyStream *stream, std::optional<int> n,
|
||||
@ -1764,43 +1734,14 @@ std::optional<plan::ProfilingStatsWithTotalTime> PullPlan::Pull(AnyStream *strea
|
||||
}
|
||||
}};
|
||||
|
||||
// Set up temporary memory for a single Pull. Initial memory comes from the
|
||||
// stack. 256 KiB should fit on the stack and should be more than enough for a
|
||||
// single `Pull`.
|
||||
static constexpr size_t stack_size = 256UL * 1024UL;
|
||||
char stack_data[stack_size];
|
||||
|
||||
utils::ResourceWithOutOfMemoryException resource_with_exception;
|
||||
utils::MonotonicBufferResource monotonic_memory{&stack_data[0], stack_size, &resource_with_exception};
|
||||
std::optional<utils::PoolResource> pool_memory;
|
||||
static constexpr auto kMaxBlockPerChunks = 128;
|
||||
|
||||
if (!use_monotonic_memory_) {
|
||||
pool_memory.emplace(kMaxBlockPerChunks, kExecutionPoolMaxBlockSize, &resource_with_exception,
|
||||
&resource_with_exception);
|
||||
} else {
|
||||
// We can throw on every query because a simple queries for deleting will use only
|
||||
// the stack allocated buffer.
|
||||
// Also, we want to throw only when the query engine requests more memory and not the storage
|
||||
// so we add the exception to the allocator.
|
||||
// TODO (mferencevic): Tune the parameters accordingly.
|
||||
pool_memory.emplace(kMaxBlockPerChunks, 1024, &monotonic_memory, &resource_with_exception);
|
||||
}
|
||||
|
||||
ctx_.evaluation_context.memory = &*pool_memory;
|
||||
|
||||
// Returns true if a result was pulled.
|
||||
const auto pull_result = [&]() -> bool { return cursor_->Pull(frame_, ctx_); };
|
||||
|
||||
const auto stream_values = [&]() {
|
||||
// TODO: The streamed values should also probably use the above memory.
|
||||
std::vector<TypedValue> values;
|
||||
values.reserve(output_symbols.size());
|
||||
|
||||
for (const auto &symbol : output_symbols) {
|
||||
values.emplace_back(frame_[symbol]);
|
||||
auto values = std::vector<TypedValue>(output_symbols.size());
|
||||
const auto stream_values = [&] {
|
||||
for (auto const i : ranges::views::iota(0UL, output_symbols.size())) {
|
||||
values[i] = frame_[output_symbols[i]];
|
||||
}
|
||||
|
||||
stream->Result(values);
|
||||
};
|
||||
|
||||
@ -1910,7 +1851,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper,
|
||||
std::function<void()> handler;
|
||||
|
||||
if (query_upper == "BEGIN") {
|
||||
ResetInterpreter();
|
||||
// ResetInterpreter();
|
||||
// TODO: Evaluate doing move(extras). Currently the extras is very small, but this will be important if it ever
|
||||
// becomes large.
|
||||
handler = [this, extras = extras] {
|
||||
@ -1988,30 +1929,6 @@ inline static void TryCaching(const AstStorage &ast_storage, FrameChangeCollecto
|
||||
}
|
||||
}
|
||||
|
||||
bool IsLoadCsvQuery(const std::vector<memgraph::query::Clause *> &clauses) {
|
||||
return std::any_of(clauses.begin(), clauses.end(),
|
||||
[](memgraph::query::Clause const *clause) { return clause->GetTypeInfo() == LoadCsv::kType; });
|
||||
}
|
||||
|
||||
bool IsCallBatchedProcedureQuery(const std::vector<memgraph::query::Clause *> &clauses) {
|
||||
EvaluationContext evaluation_context;
|
||||
|
||||
return std::ranges::any_of(clauses, [&evaluation_context](memgraph::query::Clause *clause) -> bool {
|
||||
if (!(clause->GetTypeInfo() == CallProcedure::kType)) return false;
|
||||
auto *call_procedure_clause = utils::Downcast<CallProcedure>(clause);
|
||||
|
||||
const auto &maybe_found = memgraph::query::procedure::FindProcedure(
|
||||
procedure::gModuleRegistry, call_procedure_clause->procedure_name_, evaluation_context.memory);
|
||||
if (!maybe_found) {
|
||||
throw QueryRuntimeException("There is no procedure named '{}'.", call_procedure_clause->procedure_name_);
|
||||
}
|
||||
const auto &[module, proc] = *maybe_found;
|
||||
if (!proc->info.is_batched) return false;
|
||||
spdlog::trace("Using PoolResource for batched query procedure");
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string, TypedValue> *summary,
|
||||
InterpreterContext *interpreter_context, CurrentDB ¤t_db,
|
||||
utils::MemoryResource *execution_memory, std::vector<Notification> *notifications,
|
||||
@ -2031,7 +1948,6 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
spdlog::info("Running query with memory limit of {}", utils::GetReadableSize(*memory_limit));
|
||||
}
|
||||
auto clauses = cypher_query->single_query_->clauses_;
|
||||
bool contains_csv = false;
|
||||
if (std::any_of(clauses.begin(), clauses.end(),
|
||||
[](const auto *clause) { return clause->GetTypeInfo() == LoadCsv::kType; })) {
|
||||
notifications->emplace_back(
|
||||
@ -2039,13 +1955,8 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
"It's important to note that the parser parses the values as strings. It's up to the user to "
|
||||
"convert the parsed row values to the appropriate type. This can be done using the built-in "
|
||||
"conversion functions such as ToInteger, ToFloat, ToBoolean etc.");
|
||||
contains_csv = true;
|
||||
}
|
||||
|
||||
// If this is LOAD CSV query, use PoolResource without MonotonicMemoryResource as we want to reuse allocated memory
|
||||
auto use_monotonic_memory =
|
||||
!contains_csv && !IsCallBatchedProcedureQuery(clauses) && !IsAllShortestPathsQuery(clauses);
|
||||
|
||||
MG_ASSERT(current_db.execution_db_accessor_, "Cypher query expects a current DB transaction");
|
||||
auto *dba =
|
||||
&*current_db
|
||||
@ -2084,7 +1995,7 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
current_db.trigger_context_collector_ ? &*current_db.trigger_context_collector_ : nullptr;
|
||||
auto pull_plan = std::make_shared<PullPlan>(
|
||||
plan, parsed_query.parameters, false, dba, interpreter_context, execution_memory, std::move(user_or_role),
|
||||
transaction_status, std::move(tx_timer), trigger_context_collector, memory_limit, use_monotonic_memory,
|
||||
transaction_status, std::move(tx_timer), trigger_context_collector, memory_limit,
|
||||
frame_change_collector->IsTrackingValues() ? frame_change_collector : nullptr);
|
||||
return PreparedQuery{std::move(header), std::move(parsed_query.required_privileges),
|
||||
[pull_plan = std::move(pull_plan), output_symbols = std::move(output_symbols), summary](
|
||||
@ -2198,18 +2109,6 @@ PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_tra
|
||||
|
||||
auto *cypher_query = utils::Downcast<CypherQuery>(parsed_inner_query.query);
|
||||
|
||||
bool contains_csv = false;
|
||||
auto clauses = cypher_query->single_query_->clauses_;
|
||||
if (std::any_of(clauses.begin(), clauses.end(),
|
||||
[](const auto *clause) { return clause->GetTypeInfo() == LoadCsv::kType; })) {
|
||||
contains_csv = true;
|
||||
}
|
||||
|
||||
// If this is LOAD CSV, BatchedProcedure or AllShortest query, use PoolResource without MonotonicMemoryResource as we
|
||||
// want to reuse allocated memory
|
||||
auto use_monotonic_memory =
|
||||
!contains_csv && !IsCallBatchedProcedureQuery(clauses) && !IsAllShortestPathsQuery(clauses);
|
||||
|
||||
MG_ASSERT(cypher_query, "Cypher grammar should not allow other queries in PROFILE");
|
||||
EvaluationContext evaluation_context;
|
||||
evaluation_context.timestamp = QueryTimestamp();
|
||||
@ -2243,14 +2142,14 @@ PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_tra
|
||||
// We want to execute the query we are profiling lazily, so we delay
|
||||
// the construction of the corresponding context.
|
||||
stats_and_total_time = std::optional<plan::ProfilingStatsWithTotalTime>{},
|
||||
pull_plan = std::shared_ptr<PullPlanVector>(nullptr), transaction_status, use_monotonic_memory,
|
||||
frame_change_collector, tx_timer = std::move(tx_timer)](
|
||||
AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
|
||||
pull_plan = std::shared_ptr<PullPlanVector>(nullptr), transaction_status, frame_change_collector,
|
||||
tx_timer = std::move(tx_timer)](AnyStream *stream,
|
||||
std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
|
||||
// No output symbols are given so that nothing is streamed.
|
||||
if (!stats_and_total_time) {
|
||||
stats_and_total_time =
|
||||
PullPlan(plan, parameters, true, dba, interpreter_context, execution_memory, std::move(user_or_role),
|
||||
transaction_status, std::move(tx_timer), nullptr, memory_limit, use_monotonic_memory,
|
||||
transaction_status, std::move(tx_timer), nullptr, memory_limit,
|
||||
frame_change_collector->IsTrackingValues() ? frame_change_collector : nullptr)
|
||||
.Pull(stream, {}, {}, summary);
|
||||
pull_plan = std::make_shared<PullPlanVector>(ProfilingStatsToTable(*stats_and_total_time));
|
||||
@ -4213,6 +4112,7 @@ PreparedQuery PrepareShowDatabasesQuery(ParsedQuery parsed_query, InterpreterCon
|
||||
std::optional<uint64_t> Interpreter::GetTransactionId() const { return current_transaction_; }
|
||||
|
||||
void Interpreter::BeginTransaction(QueryExtras const &extras) {
|
||||
ResetInterpreter();
|
||||
const auto prepared_query = PrepareTransactionQuery("BEGIN", extras);
|
||||
prepared_query.query_handler(nullptr, {});
|
||||
}
|
||||
@ -4247,12 +4147,12 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
const auto upper_case_query = utils::ToUpperCase(query_string);
|
||||
const auto trimmed_query = utils::Trim(upper_case_query);
|
||||
if (trimmed_query == "BEGIN" || trimmed_query == "COMMIT" || trimmed_query == "ROLLBACK") {
|
||||
auto resource = utils::MonotonicBufferResource(kExecutionMemoryBlockSize);
|
||||
auto prepared_query = PrepareTransactionQuery(trimmed_query, extras);
|
||||
auto &query_execution =
|
||||
query_executions_.emplace_back(QueryExecution::Create(std::move(resource), std::move(prepared_query)));
|
||||
std::optional<int> qid =
|
||||
in_explicit_transaction_ ? static_cast<int>(query_executions_.size() - 1) : std::optional<int>{};
|
||||
if (trimmed_query == "BEGIN") {
|
||||
ResetInterpreter();
|
||||
}
|
||||
auto &query_execution = query_executions_.emplace_back(QueryExecution::Create());
|
||||
query_execution->prepared_query = PrepareTransactionQuery(trimmed_query, extras);
|
||||
auto qid = in_explicit_transaction_ ? static_cast<int>(query_executions_.size() - 1) : std::optional<int>{};
|
||||
return {query_execution->prepared_query->header, query_execution->prepared_query->privileges, qid, {}};
|
||||
}
|
||||
|
||||
@ -4282,35 +4182,8 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
ParseQuery(query_string, params, &interpreter_context_->ast_cache, interpreter_context_->config.query);
|
||||
auto parsing_time = parsing_timer.Elapsed().count();
|
||||
|
||||
CypherQuery const *const cypher_query = [&]() -> CypherQuery * {
|
||||
if (auto *cypher_query = utils::Downcast<CypherQuery>(parsed_query.query)) {
|
||||
return cypher_query;
|
||||
}
|
||||
if (auto *profile_query = utils::Downcast<ProfileQuery>(parsed_query.query)) {
|
||||
return profile_query->cypher_query_;
|
||||
}
|
||||
return nullptr;
|
||||
}(); // IILE
|
||||
|
||||
auto const [usePool, hasAllShortestPaths] = [&]() -> std::pair<bool, bool> {
|
||||
if (!cypher_query) {
|
||||
return {false, false};
|
||||
}
|
||||
auto const &clauses = cypher_query->single_query_->clauses_;
|
||||
bool hasAllShortestPaths = IsAllShortestPathsQuery(clauses);
|
||||
// Using PoolResource without MonotonicMemoryResouce for LOAD CSV reduces memory usage.
|
||||
bool usePool = hasAllShortestPaths || IsCallBatchedProcedureQuery(clauses) || IsLoadCsvQuery(clauses);
|
||||
return {usePool, hasAllShortestPaths};
|
||||
}(); // IILE
|
||||
|
||||
// Setup QueryExecution
|
||||
// its MemoryResource is mostly used for allocations done on Frame and storing `row`s
|
||||
if (usePool) {
|
||||
query_executions_.emplace_back(QueryExecution::Create(utils::PoolResource(128, kExecutionPoolMaxBlockSize)));
|
||||
} else {
|
||||
query_executions_.emplace_back(QueryExecution::Create(utils::MonotonicBufferResource(kExecutionMemoryBlockSize)));
|
||||
}
|
||||
|
||||
query_executions_.emplace_back(QueryExecution::Create());
|
||||
auto &query_execution = query_executions_.back();
|
||||
query_execution_ptr = &query_execution;
|
||||
|
||||
@ -4379,9 +4252,7 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
|
||||
utils::Timer planning_timer;
|
||||
PreparedQuery prepared_query;
|
||||
utils::MemoryResource *memory_resource =
|
||||
std::visit([](auto &execution_memory) -> utils::MemoryResource * { return &execution_memory; },
|
||||
query_execution->execution_memory);
|
||||
utils::MemoryResource *memory_resource = query_execution->execution_memory.resource();
|
||||
frame_change_collector_.reset();
|
||||
frame_change_collector_.emplace();
|
||||
if (utils::Downcast<CypherQuery>(parsed_query.query)) {
|
||||
@ -4392,10 +4263,10 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
prepared_query = PrepareExplainQuery(std::move(parsed_query), &query_execution->summary,
|
||||
&query_execution->notifications, interpreter_context_, current_db_);
|
||||
} else if (utils::Downcast<ProfileQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareProfileQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
&query_execution->notifications, interpreter_context_, current_db_,
|
||||
&query_execution->execution_memory_with_exception, user_or_role_,
|
||||
&transaction_status_, current_timeout_timer_, &*frame_change_collector_);
|
||||
prepared_query =
|
||||
PrepareProfileQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
&query_execution->notifications, interpreter_context_, current_db_, memory_resource,
|
||||
user_or_role_, &transaction_status_, current_timeout_timer_, &*frame_change_collector_);
|
||||
} else if (utils::Downcast<DumpQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareDumpQuery(std::move(parsed_query), current_db_);
|
||||
} else if (utils::Downcast<IndexQuery>(parsed_query.query)) {
|
||||
@ -4597,7 +4468,7 @@ void RunTriggersAfterCommit(dbms::DatabaseAccess db_acc, InterpreterContext *int
|
||||
std::atomic<TransactionStatus> *transaction_status) {
|
||||
// Run the triggers
|
||||
for (const auto &trigger : db_acc->trigger_store()->AfterCommitTriggers().access()) {
|
||||
utils::MonotonicBufferResource execution_memory{kExecutionMemoryBlockSize};
|
||||
QueryAllocator execution_memory{};
|
||||
|
||||
// create a new transaction for each trigger
|
||||
auto tx_acc = db_acc->Access();
|
||||
@ -4608,7 +4479,7 @@ void RunTriggersAfterCommit(dbms::DatabaseAccess db_acc, InterpreterContext *int
|
||||
auto trigger_context = original_trigger_context;
|
||||
trigger_context.AdaptForAccessor(&db_accessor);
|
||||
try {
|
||||
trigger.Execute(&db_accessor, &execution_memory, flags::run_time::GetExecutionTimeout(),
|
||||
trigger.Execute(&db_accessor, execution_memory.resource(), flags::run_time::GetExecutionTimeout(),
|
||||
&interpreter_context->is_shutting_down, transaction_status, trigger_context);
|
||||
} catch (const utils::BasicException &exception) {
|
||||
spdlog::warn("Trigger '{}' failed with exception:\n{}", trigger.Name(), exception.what());
|
||||
@ -4762,11 +4633,12 @@ void Interpreter::Commit() {
|
||||
if (trigger_context) {
|
||||
// Run the triggers
|
||||
for (const auto &trigger : db->trigger_store()->BeforeCommitTriggers().access()) {
|
||||
utils::MonotonicBufferResource execution_memory{kExecutionMemoryBlockSize};
|
||||
QueryAllocator execution_memory{};
|
||||
AdvanceCommand();
|
||||
try {
|
||||
trigger.Execute(&*current_db_.execution_db_accessor_, &execution_memory, flags::run_time::GetExecutionTimeout(),
|
||||
&interpreter_context_->is_shutting_down, &transaction_status_, *trigger_context);
|
||||
trigger.Execute(&*current_db_.execution_db_accessor_, execution_memory.resource(),
|
||||
flags::run_time::GetExecutionTimeout(), &interpreter_context_->is_shutting_down,
|
||||
&transaction_status_, *trigger_context);
|
||||
} catch (const utils::BasicException &e) {
|
||||
throw utils::BasicException(
|
||||
fmt::format("Trigger '{}' caused the transaction to fail.\nException: {}", trigger.Name(), e.what()));
|
||||
|
@ -65,6 +65,54 @@ extern const Event SuccessfulQuery;
|
||||
|
||||
namespace memgraph::query {
|
||||
|
||||
struct QueryAllocator {
|
||||
QueryAllocator() = default;
|
||||
QueryAllocator(QueryAllocator const &) = delete;
|
||||
QueryAllocator &operator=(QueryAllocator const &) = delete;
|
||||
|
||||
// No move addresses to pool & monotonic fields must be stable
|
||||
QueryAllocator(QueryAllocator &&) = delete;
|
||||
QueryAllocator &operator=(QueryAllocator &&) = delete;
|
||||
|
||||
auto resource() -> utils::MemoryResource * {
|
||||
#ifndef MG_MEMORY_PROFILE
|
||||
return &pool;
|
||||
#else
|
||||
return upstream_resource();
|
||||
#endif
|
||||
}
|
||||
auto resource_without_pool() -> utils::MemoryResource * {
|
||||
#ifndef MG_MEMORY_PROFILE
|
||||
return &monotonic;
|
||||
#else
|
||||
return upstream_resource();
|
||||
#endif
|
||||
}
|
||||
auto resource_without_pool_or_mono() -> utils::MemoryResource * { return upstream_resource(); }
|
||||
|
||||
private:
|
||||
// At least one page to ensure not sharing page with other subsystems
|
||||
static constexpr auto kMonotonicInitialSize = 4UL * 1024UL;
|
||||
// TODO: need to profile to check for good defaults, also maybe PoolResource
|
||||
// needs to be smarter. We expect more reuse of smaller objects than larger
|
||||
// objects. 64*1024B is maybe wasteful, whereas 256*32B maybe sensible.
|
||||
// Depends on number of small objects expected.
|
||||
static constexpr auto kPoolBlockPerChunk = 64UL;
|
||||
static constexpr auto kPoolMaxBlockSize = 1024UL;
|
||||
|
||||
static auto upstream_resource() -> utils::MemoryResource * {
|
||||
// singleton ResourceWithOutOfMemoryException
|
||||
// explicitly backed by NewDeleteResource
|
||||
static auto upstream = utils::ResourceWithOutOfMemoryException{utils::NewDeleteResource()};
|
||||
return &upstream;
|
||||
}
|
||||
|
||||
#ifndef MG_MEMORY_PROFILE
|
||||
memgraph::utils::MonotonicBufferResource monotonic{kMonotonicInitialSize, upstream_resource()};
|
||||
memgraph::utils::PoolResource pool{kPoolBlockPerChunk, &monotonic, upstream_resource()};
|
||||
#endif
|
||||
};
|
||||
|
||||
struct InterpreterContext;
|
||||
|
||||
inline constexpr size_t kExecutionMemoryBlockSize = 1UL * 1024UL * 1024UL;
|
||||
@ -298,52 +346,37 @@ class Interpreter final {
|
||||
query_executions_.clear();
|
||||
system_transaction_.reset();
|
||||
transaction_queries_->clear();
|
||||
current_timeout_timer_.reset();
|
||||
if (current_db_.db_acc_ && current_db_.db_acc_->is_deleting()) {
|
||||
current_db_.db_acc_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
struct QueryExecution {
|
||||
std::variant<utils::MonotonicBufferResource, utils::PoolResource> execution_memory;
|
||||
utils::ResourceWithOutOfMemoryException execution_memory_with_exception;
|
||||
std::optional<PreparedQuery> prepared_query;
|
||||
QueryAllocator execution_memory; // NOTE: before all other fields which uses this memory
|
||||
|
||||
std::optional<PreparedQuery> prepared_query;
|
||||
std::map<std::string, TypedValue> summary;
|
||||
std::vector<Notification> notifications;
|
||||
|
||||
static auto Create(std::variant<utils::MonotonicBufferResource, utils::PoolResource> memory_resource,
|
||||
std::optional<PreparedQuery> prepared_query = std::nullopt) -> std::unique_ptr<QueryExecution> {
|
||||
return std::make_unique<QueryExecution>(std::move(memory_resource), std::move(prepared_query));
|
||||
}
|
||||
static auto Create() -> std::unique_ptr<QueryExecution> { return std::make_unique<QueryExecution>(); }
|
||||
|
||||
explicit QueryExecution(std::variant<utils::MonotonicBufferResource, utils::PoolResource> memory_resource,
|
||||
std::optional<PreparedQuery> prepared_query)
|
||||
: execution_memory(std::move(memory_resource)), prepared_query{std::move(prepared_query)} {
|
||||
std::visit(
|
||||
[&](auto &memory_resource) {
|
||||
execution_memory_with_exception = utils::ResourceWithOutOfMemoryException(&memory_resource);
|
||||
},
|
||||
execution_memory);
|
||||
};
|
||||
explicit QueryExecution() = default;
|
||||
|
||||
QueryExecution(const QueryExecution &) = delete;
|
||||
QueryExecution(QueryExecution &&) = default;
|
||||
QueryExecution(QueryExecution &&) = delete;
|
||||
QueryExecution &operator=(const QueryExecution &) = delete;
|
||||
QueryExecution &operator=(QueryExecution &&) = default;
|
||||
QueryExecution &operator=(QueryExecution &&) = delete;
|
||||
|
||||
~QueryExecution() {
|
||||
// We should always release the execution memory AFTER we
|
||||
// destroy the prepared query which is using that instance
|
||||
// of execution memory.
|
||||
prepared_query.reset();
|
||||
std::visit([](auto &memory_resource) { memory_resource.Release(); }, execution_memory);
|
||||
}
|
||||
~QueryExecution() = default;
|
||||
|
||||
void CleanRuntimeData() {
|
||||
if (prepared_query.has_value()) {
|
||||
prepared_query.reset();
|
||||
}
|
||||
// Called from Commit/Abort once query has been fully used
|
||||
|
||||
prepared_query.reset();
|
||||
notifications.clear();
|
||||
// TODO: double check is summary still needed here
|
||||
// can we dispose of it and also execution_memory at this point?
|
||||
}
|
||||
};
|
||||
|
||||
@ -413,9 +446,7 @@ std::map<std::string, TypedValue> Interpreter::Pull(TStream *result_stream, std:
|
||||
try {
|
||||
// Wrap the (statically polymorphic) stream type into a common type which
|
||||
// the handler knows.
|
||||
AnyStream stream{result_stream,
|
||||
std::visit([](auto &execution_memory) -> utils::MemoryResource * { return &execution_memory; },
|
||||
query_execution->execution_memory)};
|
||||
AnyStream stream{result_stream, query_execution->execution_memory.resource()};
|
||||
const auto maybe_res = query_execution->prepared_query->query_handler(&stream, n);
|
||||
// Stream is using execution memory of the query_execution which
|
||||
// can be deleted after its execution so the stream should be cleared
|
||||
|
@ -69,6 +69,7 @@
|
||||
#include "utils/pmr/vector.hpp"
|
||||
#include "utils/readable_size.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/tag.hpp"
|
||||
#include "utils/temporal.hpp"
|
||||
#include "utils/typeinfo.hpp"
|
||||
|
||||
@ -864,17 +865,15 @@ bool Expand::ExpandCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
SCOPED_PROFILE_OP_BY_REF(self_);
|
||||
|
||||
// A helper function for expanding a node from an edge.
|
||||
auto pull_node = [this, &frame](const EdgeAccessor &new_edge, EdgeAtom::Direction direction) {
|
||||
auto pull_node = [this, &frame]<EdgeAtom::Direction direction>(const EdgeAccessor &new_edge,
|
||||
utils::tag_value<direction>) {
|
||||
if (self_.common_.existing_node) return;
|
||||
switch (direction) {
|
||||
case EdgeAtom::Direction::IN:
|
||||
frame[self_.common_.node_symbol] = new_edge.From();
|
||||
break;
|
||||
case EdgeAtom::Direction::OUT:
|
||||
frame[self_.common_.node_symbol] = new_edge.To();
|
||||
break;
|
||||
case EdgeAtom::Direction::BOTH:
|
||||
LOG_FATAL("Must indicate exact expansion direction here");
|
||||
if constexpr (direction == EdgeAtom::Direction::IN) {
|
||||
frame[self_.common_.node_symbol] = new_edge.From();
|
||||
} else if constexpr (direction == EdgeAtom::Direction::OUT) {
|
||||
frame[self_.common_.node_symbol] = new_edge.To();
|
||||
} else {
|
||||
LOG_FATAL("Must indicate exact expansion direction here");
|
||||
}
|
||||
};
|
||||
|
||||
@ -893,7 +892,7 @@ bool Expand::ExpandCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
#endif
|
||||
|
||||
frame[self_.common_.edge_symbol] = edge;
|
||||
pull_node(edge, EdgeAtom::Direction::IN);
|
||||
pull_node(edge, utils::tag_v<EdgeAtom::Direction::IN>);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -913,7 +912,7 @@ bool Expand::ExpandCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
}
|
||||
#endif
|
||||
frame[self_.common_.edge_symbol] = edge;
|
||||
pull_node(edge, EdgeAtom::Direction::OUT);
|
||||
pull_node(edge, utils::tag_v<EdgeAtom::Direction::OUT>);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -1007,12 +1006,12 @@ bool Expand::ExpandCursor::InitEdges(Frame &frame, ExecutionContext &context) {
|
||||
auto existing_node = *expansion_info_.existing_node;
|
||||
|
||||
auto edges_result = UnwrapEdgesResult(vertex.InEdges(self_.view_, self_.common_.edge_types, existing_node));
|
||||
in_edges_.emplace(edges_result.edges);
|
||||
in_edges_.emplace(std::move(edges_result.edges));
|
||||
num_expanded_first = edges_result.expanded_count;
|
||||
}
|
||||
} else {
|
||||
auto edges_result = UnwrapEdgesResult(vertex.InEdges(self_.view_, self_.common_.edge_types));
|
||||
in_edges_.emplace(edges_result.edges);
|
||||
in_edges_.emplace(std::move(edges_result.edges));
|
||||
num_expanded_first = edges_result.expanded_count;
|
||||
}
|
||||
if (in_edges_) {
|
||||
@ -1026,12 +1025,12 @@ bool Expand::ExpandCursor::InitEdges(Frame &frame, ExecutionContext &context) {
|
||||
if (expansion_info_.existing_node) {
|
||||
auto existing_node = *expansion_info_.existing_node;
|
||||
auto edges_result = UnwrapEdgesResult(vertex.OutEdges(self_.view_, self_.common_.edge_types, existing_node));
|
||||
out_edges_.emplace(edges_result.edges);
|
||||
out_edges_.emplace(std::move(edges_result.edges));
|
||||
num_expanded_second = edges_result.expanded_count;
|
||||
}
|
||||
} else {
|
||||
auto edges_result = UnwrapEdgesResult(vertex.OutEdges(self_.view_, self_.common_.edge_types));
|
||||
out_edges_.emplace(edges_result.edges);
|
||||
out_edges_.emplace(std::move(edges_result.edges));
|
||||
num_expanded_second = edges_result.expanded_count;
|
||||
}
|
||||
if (out_edges_) {
|
||||
@ -1117,14 +1116,14 @@ auto ExpandFromVertex(const VertexAccessor &vertex, EdgeAtom::Direction directio
|
||||
|
||||
if (direction != EdgeAtom::Direction::OUT) {
|
||||
auto edges = UnwrapEdgesResult(vertex.InEdges(view, edge_types)).edges;
|
||||
if (edges.begin() != edges.end()) {
|
||||
if (!edges.empty()) {
|
||||
chain_elements.emplace_back(wrapper(EdgeAtom::Direction::IN, std::move(edges)));
|
||||
}
|
||||
}
|
||||
|
||||
if (direction != EdgeAtom::Direction::IN) {
|
||||
auto edges = UnwrapEdgesResult(vertex.OutEdges(view, edge_types)).edges;
|
||||
if (edges.begin() != edges.end()) {
|
||||
if (!edges.empty()) {
|
||||
chain_elements.emplace_back(wrapper(EdgeAtom::Direction::OUT, std::move(edges)));
|
||||
}
|
||||
}
|
||||
@ -1244,8 +1243,13 @@ class ExpandVariableCursor : public Cursor {
|
||||
}
|
||||
|
||||
// reset the frame value to an empty edge list
|
||||
auto *pull_memory = context.evaluation_context.memory;
|
||||
frame[self_.common_.edge_symbol] = TypedValue::TVector(pull_memory);
|
||||
if (frame[self_.common_.edge_symbol].IsList()) {
|
||||
// Preserve the list capacity if possible
|
||||
frame[self_.common_.edge_symbol].ValueList().clear();
|
||||
} else {
|
||||
auto *pull_memory = context.evaluation_context.memory;
|
||||
frame[self_.common_.edge_symbol] = TypedValue::TVector(pull_memory);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -4474,9 +4478,8 @@ class UnwindCursor : public Cursor {
|
||||
TypedValue input_value = self_.input_expression_->Accept(evaluator);
|
||||
if (input_value.type() != TypedValue::Type::List)
|
||||
throw QueryRuntimeException("Argument of UNWIND must be a list, but '{}' was provided.", input_value.type());
|
||||
// Copy the evaluted input_value_list to our vector.
|
||||
// eval memory != query memory
|
||||
input_value_ = input_value.ValueList();
|
||||
// Move the evaluted input_value_list to our vector.
|
||||
input_value_ = std::move(input_value.ValueList());
|
||||
input_value_it_ = input_value_.begin();
|
||||
}
|
||||
|
||||
@ -5336,6 +5339,7 @@ class LoadCsvCursor : public Cursor {
|
||||
"1");
|
||||
}
|
||||
did_pull_ = true;
|
||||
reader_->Reset();
|
||||
}
|
||||
|
||||
auto row = reader_->GetNextRow(context.evaluation_context.memory);
|
||||
|
@ -76,18 +76,13 @@ using UniqueCursorPtr = std::unique_ptr<Cursor, std::function<void(Cursor *)>>;
|
||||
template <class TCursor, class... TArgs>
|
||||
std::unique_ptr<Cursor, std::function<void(Cursor *)>> MakeUniqueCursorPtr(utils::Allocator<TCursor> allocator,
|
||||
TArgs &&...args) {
|
||||
auto *ptr = allocator.allocate(1);
|
||||
try {
|
||||
auto *cursor = new (ptr) TCursor(std::forward<TArgs>(args)...);
|
||||
return std::unique_ptr<Cursor, std::function<void(Cursor *)>>(cursor, [allocator](Cursor *base_ptr) mutable {
|
||||
auto *p = static_cast<TCursor *>(base_ptr);
|
||||
p->~TCursor();
|
||||
allocator.deallocate(p, 1);
|
||||
});
|
||||
} catch (...) {
|
||||
allocator.deallocate(ptr, 1);
|
||||
throw;
|
||||
}
|
||||
auto *cursor = allocator.template new_object<TCursor>(std::forward<TArgs>(args)...);
|
||||
auto dtr = [allocator](Cursor *base_ptr) mutable {
|
||||
auto *p = static_cast<TCursor *>(base_ptr);
|
||||
allocator.delete_object(p);
|
||||
};
|
||||
// TODO: not std::function
|
||||
return std::unique_ptr<Cursor, std::function<void(Cursor *)>>(cursor, std::move(dtr));
|
||||
}
|
||||
|
||||
class Once;
|
||||
|
@ -191,9 +191,9 @@ std::shared_ptr<Trigger::TriggerPlan> Trigger::GetPlan(DbAccessor *db_accessor)
|
||||
return trigger_plan_;
|
||||
}
|
||||
|
||||
void Trigger::Execute(DbAccessor *dba, utils::MonotonicBufferResource *execution_memory,
|
||||
const double max_execution_time_sec, std::atomic<bool> *is_shutting_down,
|
||||
std::atomic<TransactionStatus> *transaction_status, const TriggerContext &context) const {
|
||||
void Trigger::Execute(DbAccessor *dba, utils::MemoryResource *execution_memory, const double max_execution_time_sec,
|
||||
std::atomic<bool> *is_shutting_down, std::atomic<TransactionStatus> *transaction_status,
|
||||
const TriggerContext &context) const {
|
||||
if (!context.ShouldEventTrigger(event_type_)) {
|
||||
return;
|
||||
}
|
||||
@ -214,22 +214,7 @@ void Trigger::Execute(DbAccessor *dba, utils::MonotonicBufferResource *execution
|
||||
ctx.is_shutting_down = is_shutting_down;
|
||||
ctx.transaction_status = transaction_status;
|
||||
ctx.is_profile_query = false;
|
||||
|
||||
// Set up temporary memory for a single Pull. Initial memory comes from the
|
||||
// stack. 256 KiB should fit on the stack and should be more than enough for a
|
||||
// single `Pull`.
|
||||
static constexpr size_t stack_size = 256UL * 1024UL;
|
||||
char stack_data[stack_size];
|
||||
|
||||
// We can throw on every query because a simple queries for deleting will use only
|
||||
// the stack allocated buffer.
|
||||
// Also, we want to throw only when the query engine requests more memory and not the storage
|
||||
// so we add the exception to the allocator.
|
||||
utils::ResourceWithOutOfMemoryException resource_with_exception;
|
||||
utils::MonotonicBufferResource monotonic_memory(&stack_data[0], stack_size, &resource_with_exception);
|
||||
// TODO (mferencevic): Tune the parameters accordingly.
|
||||
utils::PoolResource pool_memory(128, 1024, &monotonic_memory);
|
||||
ctx.evaluation_context.memory = &pool_memory;
|
||||
ctx.evaluation_context.memory = execution_memory;
|
||||
|
||||
auto cursor = plan.plan().MakeCursor(execution_memory);
|
||||
Frame frame{plan.symbol_table().max_position(), execution_memory};
|
||||
|
@ -39,7 +39,7 @@ struct Trigger {
|
||||
utils::SkipList<QueryCacheEntry> *query_cache, DbAccessor *db_accessor,
|
||||
const InterpreterConfig::Query &query_config, std::shared_ptr<QueryUserOrRole> owner);
|
||||
|
||||
void Execute(DbAccessor *dba, utils::MonotonicBufferResource *execution_memory, double max_execution_time_sec,
|
||||
void Execute(DbAccessor *dba, utils::MemoryResource *execution_memory, double max_execution_time_sec,
|
||||
std::atomic<bool> *is_shutting_down, std::atomic<TransactionStatus> *transaction_status,
|
||||
const TriggerContext &context) const;
|
||||
|
||||
|
@ -43,6 +43,7 @@ add_library(mg-storage-v2 STATIC
|
||||
replication/rpc.cpp
|
||||
replication/replication_storage_state.cpp
|
||||
inmemory/replication/recovery.cpp
|
||||
property_disk_store.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(mg-storage-v2 mg::replication Threads::Threads mg-utils gflags absl::flat_hash_map mg-rpc mg-slk mg-events mg-memory)
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -58,7 +58,7 @@ void ExistenceConstraints::LoadExistenceConstraints(const std::vector<std::strin
|
||||
|
||||
[[nodiscard]] std::optional<ConstraintViolation> ExistenceConstraints::ValidateVertexOnConstraint(
|
||||
const Vertex &vertex, const LabelId &label, const PropertyId &property) {
|
||||
if (!vertex.deleted && utils::Contains(vertex.labels, label) && !vertex.properties.HasProperty(property)) {
|
||||
if (!vertex.deleted && utils::Contains(vertex.labels, label) && !vertex.HasProperty(property)) {
|
||||
return ConstraintViolation{ConstraintViolation::Type::EXISTENCE, label, std::set<PropertyId>{property}};
|
||||
}
|
||||
return std::nullopt;
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -90,12 +90,13 @@ bool DiskLabelIndex::SyncVertexToLabelIndexStorage(const Vertex &vertex, uint64_
|
||||
if (!utils::Contains(vertex.labels, index_label)) {
|
||||
continue;
|
||||
}
|
||||
if (!disk_transaction
|
||||
->Put(utils::SerializeVertexAsKeyForLabelIndex(index_label, vertex.gid),
|
||||
utils::SerializeVertexAsValueForLabelIndex(index_label, vertex.labels, vertex.properties))
|
||||
.ok()) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Re-enable
|
||||
// if (!disk_transaction
|
||||
// ->Put(utils::SerializeVertexAsKeyForLabelIndex(index_label, vertex.gid),
|
||||
// utils::SerializeVertexAsValueForLabelIndex(index_label, vertex.labels, vertex.properties))
|
||||
// .ok()) {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
|
||||
return CommitWithTimestamp(disk_transaction.get(), commit_timestamp);
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -20,7 +20,9 @@ namespace memgraph::storage {
|
||||
namespace {
|
||||
|
||||
bool IsVertexIndexedByLabelProperty(const Vertex &vertex, LabelId label, PropertyId property) {
|
||||
return utils::Contains(vertex.labels, label) && vertex.properties.HasProperty(property);
|
||||
// TODO: Re-enable
|
||||
// return utils::Contains(vertex.labels, label) && vertex.properties.HasProperty(property);
|
||||
return false;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool ClearTransactionEntriesWithRemovedIndexingLabel(
|
||||
@ -94,12 +96,14 @@ bool DiskLabelPropertyIndex::SyncVertexToLabelPropertyIndexStorage(const Vertex
|
||||
}
|
||||
for (const auto &[index_label, index_property] : index_) {
|
||||
if (IsVertexIndexedByLabelProperty(vertex, index_label, index_property)) {
|
||||
if (!disk_transaction
|
||||
->Put(utils::SerializeVertexAsKeyForLabelPropertyIndex(index_label, index_property, vertex.gid),
|
||||
utils::SerializeVertexAsValueForLabelPropertyIndex(index_label, vertex.labels, vertex.properties))
|
||||
.ok()) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Re-enable
|
||||
// if (!disk_transaction
|
||||
// ->Put(utils::SerializeVertexAsKeyForLabelPropertyIndex(index_label, index_property, vertex.gid),
|
||||
// utils::SerializeVertexAsValueForLabelPropertyIndex(index_label, vertex.labels,
|
||||
// vertex.properties))
|
||||
// .ok()) {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
return CommitWithTimestamp(disk_transaction.get(), commit_timestamp);
|
||||
|
@ -172,7 +172,7 @@ bool VertexHasLabel(const Vertex &vertex, LabelId label, Transaction *transactio
|
||||
|
||||
PropertyValue GetVertexProperty(const Vertex &vertex, PropertyId property, Transaction *transaction, View view) {
|
||||
bool deleted = vertex.deleted;
|
||||
PropertyValue value = vertex.properties.GetProperty(property);
|
||||
PropertyValue value = vertex.GetProperty(property);
|
||||
Delta *delta = vertex.delta;
|
||||
ApplyDeltasForRead(transaction, delta, view, [&deleted, &value, property](const Delta &delta) {
|
||||
switch (delta.action) {
|
||||
@ -628,10 +628,11 @@ std::unordered_set<Gid> DiskStorage::MergeVerticesFromMainCacheWithLabelIndexCac
|
||||
spdlog::trace("Loaded vertex with gid: {} from main index storage to label index", vertex.gid.ToString());
|
||||
uint64_t ts = utils::GetEarliestTimestamp(vertex.delta);
|
||||
/// TODO: here are doing serialization and then later deserialization again -> expensive
|
||||
LoadVertexToLabelIndexCache(transaction, utils::SerializeVertexAsKeyForLabelIndex(label, vertex.gid),
|
||||
utils::SerializeVertexAsValueForLabelIndex(label, vertex.labels, vertex.properties),
|
||||
CreateDeleteDeserializedIndexObjectDelta(index_deltas, std::nullopt, ts),
|
||||
indexed_vertices->access());
|
||||
// TODO: Re-enable
|
||||
// LoadVertexToLabelIndexCache(transaction, utils::SerializeVertexAsKeyForLabelIndex(label, vertex.gid),
|
||||
// utils::SerializeVertexAsValueForLabelIndex(label, vertex.labels,
|
||||
// vertex.properties), CreateDeleteDeserializedIndexObjectDelta(index_deltas,
|
||||
// std::nullopt, ts), indexed_vertices->access());
|
||||
}
|
||||
}
|
||||
return gids;
|
||||
@ -678,10 +679,11 @@ std::unordered_set<Gid> DiskStorage::MergeVerticesFromMainCacheWithLabelProperty
|
||||
gids.insert(vertex.gid);
|
||||
if (label_property_filter(vertex, label, property, view)) {
|
||||
uint64_t ts = utils::GetEarliestTimestamp(vertex.delta);
|
||||
LoadVertexToLabelPropertyIndexCache(
|
||||
transaction, utils::SerializeVertexAsKeyForLabelPropertyIndex(label, property, vertex.gid),
|
||||
utils::SerializeVertexAsValueForLabelPropertyIndex(label, vertex.labels, vertex.properties),
|
||||
CreateDeleteDeserializedIndexObjectDelta(index_deltas, std::nullopt, ts), indexed_vertices->access());
|
||||
// TODO: Re-enable
|
||||
// LoadVertexToLabelPropertyIndexCache(
|
||||
// transaction, utils::SerializeVertexAsKeyForLabelPropertyIndex(label, property, vertex.gid),
|
||||
// utils::SerializeVertexAsValueForLabelPropertyIndex(label, vertex.labels, vertex.properties),
|
||||
// CreateDeleteDeserializedIndexObjectDelta(index_deltas, std::nullopt, ts), indexed_vertices->access());
|
||||
}
|
||||
}
|
||||
|
||||
@ -763,10 +765,11 @@ std::unordered_set<Gid> DiskStorage::MergeVerticesFromMainCacheWithLabelProperty
|
||||
if (VertexHasLabel(vertex, label, transaction, view) &&
|
||||
IsPropertyValueWithinInterval(prop_value, lower_bound, upper_bound)) {
|
||||
uint64_t ts = utils::GetEarliestTimestamp(vertex.delta);
|
||||
LoadVertexToLabelPropertyIndexCache(
|
||||
transaction, utils::SerializeVertexAsKeyForLabelPropertyIndex(label, property, vertex.gid),
|
||||
utils::SerializeVertexAsValueForLabelPropertyIndex(label, vertex.labels, vertex.properties),
|
||||
CreateDeleteDeserializedIndexObjectDelta(index_deltas, std::nullopt, ts), indexed_vertices->access());
|
||||
// TODO: Re-enable
|
||||
// LoadVertexToLabelPropertyIndexCache(
|
||||
// transaction, utils::SerializeVertexAsKeyForLabelPropertyIndex(label, property, vertex.gid),
|
||||
// utils::SerializeVertexAsValueForLabelPropertyIndex(label, vertex.labels, vertex.properties),
|
||||
// CreateDeleteDeserializedIndexObjectDelta(index_deltas, std::nullopt, ts), indexed_vertices->access());
|
||||
}
|
||||
}
|
||||
return gids;
|
||||
@ -1028,14 +1031,15 @@ bool DiskStorage::WriteVertexToVertexColumnFamily(Transaction *transaction, cons
|
||||
MG_ASSERT(transaction->commit_timestamp, "Writing vertex to disk but commit timestamp not set.");
|
||||
auto commit_ts = transaction->commit_timestamp->load(std::memory_order_relaxed);
|
||||
const auto ser_vertex = utils::SerializeVertex(vertex);
|
||||
auto status = transaction->disk_transaction_->Put(kvstore_->vertex_chandle, ser_vertex,
|
||||
utils::SerializeProperties(vertex.properties));
|
||||
if (status.ok()) {
|
||||
spdlog::trace("rocksdb: Saved vertex with key {} and ts {} to vertex column family", ser_vertex, commit_ts);
|
||||
return true;
|
||||
}
|
||||
spdlog::error("rocksdb: Failed to save vertex with key {} and ts {} to vertex column family", ser_vertex, commit_ts);
|
||||
return false;
|
||||
// TODO: Re-enable
|
||||
// auto status = transaction->disk_transaction_->Put(kvstore_->vertex_chandle, ser_vertex,
|
||||
// utils::SerializeProperties(vertex.properties));
|
||||
// if (status.ok()) {
|
||||
// spdlog::trace("rocksdb: Saved vertex with key {} and ts {} to vertex column family", ser_vertex, commit_ts);
|
||||
return true;
|
||||
// }
|
||||
// spdlog::error("rocksdb: Failed to save vertex with key {} and ts {} to vertex column family", ser_vertex,
|
||||
// commit_ts); return false;
|
||||
}
|
||||
|
||||
bool DiskStorage::WriteEdgeToEdgeColumnFamily(Transaction *transaction, const std::string &serialized_edge_key,
|
||||
@ -1358,7 +1362,8 @@ VertexAccessor DiskStorage::CreateVertexFromDisk(Transaction *transaction, utils
|
||||
MG_ASSERT(inserted, "The vertex must be inserted here!");
|
||||
MG_ASSERT(it != accessor.end(), "Invalid Vertex accessor!");
|
||||
it->labels = std::move(label_ids);
|
||||
it->properties = std::move(properties);
|
||||
// TODO: Re-enable
|
||||
// it->properties = std::move(properties);
|
||||
delta->prev.Set(&*it);
|
||||
return {&*it, this, transaction};
|
||||
}
|
||||
@ -1419,7 +1424,8 @@ std::optional<EdgeAccessor> DiskStorage::CreateEdgeFromDisk(const VertexAccessor
|
||||
MG_ASSERT(it != acc.end(), "Invalid Edge accessor!");
|
||||
edge = EdgeRef(&*it);
|
||||
delta->prev.Set(&*it);
|
||||
edge.ptr->properties.SetBuffer(properties);
|
||||
// TODO Re-enable
|
||||
// edge.ptr->properties.SetBuffer(properties);
|
||||
}
|
||||
|
||||
ModifiedEdgeInfo modified_edge(Delta::Action::DELETE_DESERIALIZED_OBJECT, from_vertex->gid, to_vertex->gid, edge_type,
|
||||
|
@ -28,7 +28,7 @@ namespace {
|
||||
|
||||
bool IsVertexUnderConstraint(const Vertex &vertex, const LabelId &constraint_label,
|
||||
const std::set<PropertyId> &constraint_properties) {
|
||||
return utils::Contains(vertex.labels, constraint_label) && vertex.properties.HasAllProperties(constraint_properties);
|
||||
return utils::Contains(vertex.labels, constraint_label) && vertex.HasAllProperties(constraint_properties);
|
||||
}
|
||||
|
||||
bool IsDifferentVertexWithSameConstraintLabel(const std::string &key, const Gid gid, const LabelId constraint_label) {
|
||||
@ -105,7 +105,7 @@ std::optional<ConstraintViolation> DiskUniqueConstraints::Validate(
|
||||
std::optional<ConstraintViolation> DiskUniqueConstraints::TestIfVertexSatisifiesUniqueConstraint(
|
||||
const Vertex &vertex, std::vector<std::vector<PropertyValue>> &unique_storage, const LabelId &constraint_label,
|
||||
const std::set<PropertyId> &constraint_properties) const {
|
||||
auto property_values = vertex.properties.ExtractPropertyValues(constraint_properties);
|
||||
auto property_values = vertex.ExtractPropertyValues(constraint_properties);
|
||||
|
||||
/// TODO: better naming. Is vertex unique
|
||||
if (property_values.has_value() &&
|
||||
@ -227,10 +227,11 @@ bool DiskUniqueConstraints::SyncVertexToUniqueConstraintsStorage(const Vertex &v
|
||||
if (IsVertexUnderConstraint(vertex, constraint_label, constraint_properties)) {
|
||||
auto key = utils::SerializeVertexAsKeyForUniqueConstraint(constraint_label, constraint_properties,
|
||||
vertex.gid.ToString());
|
||||
auto value = utils::SerializeVertexAsValueForUniqueConstraint(constraint_label, vertex.labels, vertex.properties);
|
||||
if (!disk_transaction->Put(key, value).ok()) {
|
||||
return false;
|
||||
}
|
||||
// TODO: Re-enable
|
||||
// auto value = utils::SerializeVertexAsValueForUniqueConstraint(constraint_label, vertex.labels,
|
||||
// vertex.properties); if (!disk_transaction->Put(key, value).ok()) {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
/// TODO: extract and better message
|
||||
|
@ -277,7 +277,6 @@ void LoadPartialEdges(const std::filesystem::path &path, utils::SkipList<Edge> &
|
||||
{
|
||||
auto props_size = snapshot.ReadUint();
|
||||
if (!props_size) throw RecoveryFailure("Couldn't read the size of edge properties!");
|
||||
auto &props = it->properties;
|
||||
read_properties.clear();
|
||||
read_properties.reserve(*props_size);
|
||||
for (uint64_t j = 0; j < *props_size; ++j) {
|
||||
@ -287,7 +286,7 @@ void LoadPartialEdges(const std::filesystem::path &path, utils::SkipList<Edge> &
|
||||
if (!value) throw RecoveryFailure("Couldn't read edge property value!");
|
||||
read_properties.emplace_back(get_property_from_id(*key), std::move(*value));
|
||||
}
|
||||
props.InitProperties(std::move(read_properties));
|
||||
it->InitProperties(std::move(read_properties));
|
||||
}
|
||||
} else {
|
||||
spdlog::debug("Ensuring edge {} doesn't have any properties.", *gid);
|
||||
@ -370,7 +369,6 @@ uint64_t LoadPartialVertices(const std::filesystem::path &path, utils::SkipList<
|
||||
{
|
||||
auto props_size = snapshot.ReadUint();
|
||||
if (!props_size) throw RecoveryFailure("Couldn't read size of vertex properties!");
|
||||
auto &props = it->properties;
|
||||
read_properties.clear();
|
||||
read_properties.reserve(*props_size);
|
||||
for (uint64_t j = 0; j < *props_size; ++j) {
|
||||
@ -380,7 +378,7 @@ uint64_t LoadPartialVertices(const std::filesystem::path &path, utils::SkipList<
|
||||
if (!value) throw RecoveryFailure("Couldn't read vertex property value!");
|
||||
read_properties.emplace_back(get_property_from_id(*key), std::move(*value));
|
||||
}
|
||||
props.InitProperties(std::move(read_properties));
|
||||
it->InitProperties(std::move(read_properties));
|
||||
}
|
||||
|
||||
// Skip in edges.
|
||||
@ -720,7 +718,6 @@ RecoveredSnapshot LoadSnapshotVersion14(const std::filesystem::path &path, utils
|
||||
{
|
||||
auto props_size = snapshot.ReadUint();
|
||||
if (!props_size) throw RecoveryFailure("Couldn't read the size of properties!");
|
||||
auto &props = it->properties;
|
||||
for (uint64_t j = 0; j < *props_size; ++j) {
|
||||
auto key = snapshot.ReadUint();
|
||||
if (!key) throw RecoveryFailure("Couldn't read edge property id!");
|
||||
@ -728,7 +725,7 @@ RecoveredSnapshot LoadSnapshotVersion14(const std::filesystem::path &path, utils
|
||||
if (!value) throw RecoveryFailure("Couldn't read edge property value!");
|
||||
SPDLOG_TRACE("Recovered property \"{}\" with value \"{}\" for edge {}.",
|
||||
name_id_mapper->IdToName(snapshot_id_map.at(*key)), *value, *gid);
|
||||
props.SetProperty(get_property_from_id(*key), *value);
|
||||
it->SetProperty(get_property_from_id(*key), *value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -796,7 +793,6 @@ RecoveredSnapshot LoadSnapshotVersion14(const std::filesystem::path &path, utils
|
||||
{
|
||||
auto props_size = snapshot.ReadUint();
|
||||
if (!props_size) throw RecoveryFailure("Couldn't read the size of properties!");
|
||||
auto &props = it->properties;
|
||||
for (uint64_t j = 0; j < *props_size; ++j) {
|
||||
auto key = snapshot.ReadUint();
|
||||
if (!key) throw RecoveryFailure("Couldn't read the vertex property id!");
|
||||
@ -804,7 +800,7 @@ RecoveredSnapshot LoadSnapshotVersion14(const std::filesystem::path &path, utils
|
||||
if (!value) throw RecoveryFailure("Couldn't read the vertex property value!");
|
||||
SPDLOG_TRACE("Recovered property \"{}\" with value \"{}\" for vertex {}.",
|
||||
name_id_mapper->IdToName(snapshot_id_map.at(*key)), *value, *gid);
|
||||
props.SetProperty(get_property_from_id(*key), *value);
|
||||
it->SetProperty(get_property_from_id(*key), *value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -597,7 +597,7 @@ void EncodeDelta(BaseEncoder *encoder, NameIdMapper *name_id_mapper, SalientConf
|
||||
// TODO (mferencevic): Mitigate the memory allocation introduced here
|
||||
// (with the `GetProperty` call). It is the only memory allocation in the
|
||||
// entire WAL file writing logic.
|
||||
encoder->WritePropertyValue(vertex.properties.GetProperty(delta.property.key));
|
||||
encoder->WritePropertyValue(vertex.GetProperty(delta.property.key));
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_LABEL:
|
||||
@ -646,7 +646,7 @@ void EncodeDelta(BaseEncoder *encoder, NameIdMapper *name_id_mapper, const Delta
|
||||
// TODO (mferencevic): Mitigate the memory allocation introduced here
|
||||
// (with the `GetProperty` call). It is the only memory allocation in the
|
||||
// entire WAL file writing logic.
|
||||
encoder->WritePropertyValue(edge.properties.GetProperty(delta.property.key));
|
||||
encoder->WritePropertyValue(edge.GetProperty(delta.property.key));
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
@ -842,7 +842,7 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
|
||||
auto property_id = PropertyId::FromUint(name_id_mapper->NameToId(delta.vertex_edge_set_property.property));
|
||||
auto &property_value = delta.vertex_edge_set_property.value;
|
||||
|
||||
vertex->properties.SetProperty(property_id, property_value);
|
||||
vertex->SetProperty(property_id, property_value);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -926,7 +926,7 @@ RecoveryInfo LoadWal(const std::filesystem::path &path, RecoveredIndicesAndConst
|
||||
if (edge == edge_acc.end()) throw RecoveryFailure("The edge doesn't exist!");
|
||||
auto property_id = PropertyId::FromUint(name_id_mapper->NameToId(delta.vertex_edge_set_property.property));
|
||||
auto &property_value = delta.vertex_edge_set_property.value;
|
||||
edge->properties.SetProperty(property_id, property_value);
|
||||
edge->SetProperty(property_id, property_value);
|
||||
break;
|
||||
}
|
||||
case WalDeltaData::Type::TRANSACTION_END:
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -19,6 +19,10 @@
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/rw_spin_lock.hpp"
|
||||
|
||||
#include "storage/v2/property_disk_store.hpp"
|
||||
|
||||
// #include "storage/v2/property_disk_store.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
struct Vertex;
|
||||
@ -30,16 +34,120 @@ struct Edge {
|
||||
"Edge must be created with an initial DELETE_OBJECT delta!");
|
||||
}
|
||||
|
||||
~Edge() {
|
||||
// TODO: Don't want to do this here
|
||||
if (!moved) ClearProperties();
|
||||
}
|
||||
|
||||
Edge(Edge &) = delete;
|
||||
Edge &operator=(Edge &) = delete;
|
||||
Edge(Edge &&) = default;
|
||||
Edge &operator=(Edge &&) = delete;
|
||||
|
||||
Gid gid;
|
||||
|
||||
PropertyStore properties;
|
||||
// PropertyStore properties;
|
||||
|
||||
mutable utils::RWSpinLock lock;
|
||||
bool deleted;
|
||||
// uint8_t PAD;
|
||||
// uint16_t PAD;
|
||||
|
||||
bool has_prop;
|
||||
|
||||
class HotFixMove {
|
||||
public:
|
||||
HotFixMove() {}
|
||||
HotFixMove(HotFixMove &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// We want only the latest object to be marked as not-moved; while all previous should be marked as moved
|
||||
moved = false;
|
||||
other.moved = true;
|
||||
}
|
||||
}
|
||||
HotFixMove(HotFixMove &) = delete;
|
||||
HotFixMove &operator=(HotFixMove &) = delete;
|
||||
HotFixMove &operator=(HotFixMove &&) = delete;
|
||||
|
||||
operator bool() const { return moved; }
|
||||
|
||||
private:
|
||||
bool moved{false};
|
||||
} moved;
|
||||
|
||||
Delta *delta;
|
||||
|
||||
Gid HotFixForGID() const { return Gid::FromUint(gid.AsUint() + (1 << 31)); }
|
||||
|
||||
PropertyValue GetProperty(PropertyId property) const {
|
||||
if (!has_prop) return {};
|
||||
const auto prop = PDS::get()->Get(HotFixForGID(), property);
|
||||
if (prop) return *prop;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SetProperty(PropertyId property, const PropertyValue &value) {
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->Set(HotFixForGID(), property, value);
|
||||
}
|
||||
|
||||
template <typename TContainer>
|
||||
bool InitProperties(const TContainer &properties) {
|
||||
auto *pds = PDS::get();
|
||||
for (const auto &[property, value] : properties) {
|
||||
if (value.IsNull()) {
|
||||
continue;
|
||||
}
|
||||
if (!pds->Set(HotFixForGID(), property, value)) {
|
||||
return false;
|
||||
}
|
||||
has_prop = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearProperties() {
|
||||
if (!has_prop) return;
|
||||
has_prop = false;
|
||||
auto *pds = PDS::get();
|
||||
pds->Clear(HotFixForGID());
|
||||
}
|
||||
|
||||
std::map<PropertyId, PropertyValue> Properties() {
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->Get(HotFixForGID());
|
||||
}
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> UpdateProperties(
|
||||
std::map<PropertyId, PropertyValue> &properties) {
|
||||
if (!has_prop && properties.empty()) return {};
|
||||
auto old_properties = Properties();
|
||||
ClearProperties();
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> id_old_new_change;
|
||||
id_old_new_change.reserve(properties.size() + old_properties.size());
|
||||
for (const auto &[prop_id, new_value] : properties) {
|
||||
if (!old_properties.contains(prop_id)) {
|
||||
id_old_new_change.emplace_back(prop_id, PropertyValue(), new_value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[old_key, old_value] : old_properties) {
|
||||
auto [it, inserted] = properties.emplace(old_key, old_value);
|
||||
if (!inserted) {
|
||||
auto &new_value = it->second;
|
||||
id_old_new_change.emplace_back(it->first, old_value, new_value);
|
||||
}
|
||||
}
|
||||
|
||||
MG_ASSERT(InitProperties(properties));
|
||||
return id_old_new_change;
|
||||
}
|
||||
|
||||
uint64_t PropertySize(PropertyId property) const {
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->GetSize(HotFixForGID(), property);
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(alignof(Edge) >= 8, "The Edge should be aligned to at least 8!");
|
||||
|
@ -128,11 +128,11 @@ Result<storage::PropertyValue> EdgeAccessor::SetProperty(PropertyId property, co
|
||||
if (!PrepareForWrite(transaction_, edge_.ptr)) return Error::SERIALIZATION_ERROR;
|
||||
|
||||
if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
|
||||
using ReturnType = decltype(edge_.ptr->properties.GetProperty(property));
|
||||
using ReturnType = decltype(edge_.ptr->GetProperty(property));
|
||||
std::optional<ReturnType> current_value;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[¤t_value, &property, &value, transaction = transaction_, edge = edge_]() {
|
||||
current_value.emplace(edge.ptr->properties.GetProperty(property));
|
||||
current_value.emplace(edge.ptr->GetProperty(property));
|
||||
// We could skip setting the value if the previous one is the same to the new
|
||||
// one. This would save some memory as a delta would not be created as well as
|
||||
// avoid copying the value. The reason we are not doing that is because the
|
||||
@ -140,7 +140,7 @@ Result<storage::PropertyValue> EdgeAccessor::SetProperty(PropertyId property, co
|
||||
// "modify in-place". Additionally, the created delta will make other
|
||||
// transactions get a SERIALIZATION_ERROR.
|
||||
CreateAndLinkDelta(transaction, edge.ptr, Delta::SetPropertyTag(), property, *current_value);
|
||||
edge.ptr->properties.SetProperty(property, value);
|
||||
edge.ptr->SetProperty(property, value);
|
||||
}};
|
||||
std::invoke(atomic_memory_block);
|
||||
|
||||
@ -162,7 +162,7 @@ Result<bool> EdgeAccessor::InitProperties(const std::map<storage::PropertyId, st
|
||||
|
||||
if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
if (!edge_.ptr->properties.InitProperties(properties)) return false;
|
||||
if (!edge_.ptr->InitProperties(properties)) return false;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{[&properties, transaction_ = transaction_, edge_ = edge_]() {
|
||||
for (const auto &[property, _] : properties) {
|
||||
CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property, PropertyValue());
|
||||
@ -184,11 +184,11 @@ Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> EdgeAc
|
||||
|
||||
if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
using ReturnType = decltype(edge_.ptr->properties.UpdateProperties(properties));
|
||||
using ReturnType = decltype(edge_.ptr->UpdateProperties(properties));
|
||||
std::optional<ReturnType> id_old_new_change;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[transaction_ = transaction_, edge_ = edge_, &properties, &id_old_new_change]() {
|
||||
id_old_new_change.emplace(edge_.ptr->properties.UpdateProperties(properties));
|
||||
id_old_new_change.emplace(edge_.ptr->UpdateProperties(properties));
|
||||
for (auto &[property, old_value, new_value] : *id_old_new_change) {
|
||||
CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property, std::move(old_value));
|
||||
}
|
||||
@ -207,15 +207,15 @@ Result<std::map<PropertyId, PropertyValue>> EdgeAccessor::ClearProperties() {
|
||||
|
||||
if (edge_.ptr->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
using ReturnType = decltype(edge_.ptr->properties.Properties());
|
||||
using ReturnType = decltype(edge_.ptr->Properties());
|
||||
std::optional<ReturnType> properties;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{[&properties, transaction_ = transaction_, edge_ = edge_]() {
|
||||
properties.emplace(edge_.ptr->properties.Properties());
|
||||
properties.emplace(edge_.ptr->Properties());
|
||||
for (const auto &property : *properties) {
|
||||
CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property.first, property.second);
|
||||
}
|
||||
|
||||
edge_.ptr->properties.ClearProperties();
|
||||
edge_.ptr->ClearProperties();
|
||||
}};
|
||||
std::invoke(atomic_memory_block);
|
||||
|
||||
@ -231,7 +231,7 @@ Result<PropertyValue> EdgeAccessor::GetProperty(PropertyId property, View view)
|
||||
{
|
||||
auto guard = std::shared_lock{edge_.ptr->lock};
|
||||
deleted = edge_.ptr->deleted;
|
||||
value.emplace(edge_.ptr->properties.GetProperty(property));
|
||||
value.emplace(edge_.ptr->GetProperty(property));
|
||||
delta = edge_.ptr->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &value, property](const Delta &delta) {
|
||||
@ -271,7 +271,7 @@ Result<uint64_t> EdgeAccessor::GetPropertySize(PropertyId property, View view) c
|
||||
auto guard = std::shared_lock{edge_.ptr->lock};
|
||||
Delta *delta = edge_.ptr->delta;
|
||||
if (!delta) {
|
||||
return edge_.ptr->properties.PropertySize(property);
|
||||
return edge_.ptr->PropertySize(property);
|
||||
}
|
||||
|
||||
auto property_result = this->GetProperty(property, view);
|
||||
@ -295,7 +295,7 @@ Result<std::map<PropertyId, PropertyValue>> EdgeAccessor::Properties(View view)
|
||||
{
|
||||
auto guard = std::shared_lock{edge_.ptr->lock};
|
||||
deleted = edge_.ptr->deleted;
|
||||
properties = edge_.ptr->properties.Properties();
|
||||
properties = edge_.ptr->Properties();
|
||||
delta = edge_.ptr->delta;
|
||||
}
|
||||
ApplyDeltasForRead(transaction_, delta, view, [&exists, &deleted, &properties](const Delta &delta) {
|
||||
|
@ -121,7 +121,7 @@ inline bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, Prop
|
||||
has_label = utils::Contains(vertex.labels, label);
|
||||
// Avoid IsPropertyEqual if already not possible
|
||||
if (delta == nullptr && (deleted || !has_label)) return false;
|
||||
current_value_equal_to_value = vertex.properties.IsPropertyEqual(key, value);
|
||||
current_value_equal_to_value = vertex.IsPropertyEqual(key, value);
|
||||
}
|
||||
|
||||
if (!deleted && has_label && current_value_equal_to_value) {
|
||||
@ -186,7 +186,7 @@ inline bool CurrentVersionHasLabelProperty(const Vertex &vertex, LabelId label,
|
||||
auto guard = std::shared_lock{vertex.lock};
|
||||
deleted = vertex.deleted;
|
||||
has_label = utils::Contains(vertex.labels, label);
|
||||
current_value_equal_to_value = vertex.properties.IsPropertyEqual(key, value);
|
||||
current_value_equal_to_value = vertex.IsPropertyEqual(key, value);
|
||||
delta = vertex.delta;
|
||||
}
|
||||
|
||||
@ -246,7 +246,7 @@ inline void TryInsertLabelPropertyIndex(Vertex &vertex, std::pair<LabelId, Prope
|
||||
if (vertex.deleted || !utils::Contains(vertex.labels, label_property_pair.first)) {
|
||||
return;
|
||||
}
|
||||
auto value = vertex.properties.GetProperty(label_property_pair.second);
|
||||
auto value = vertex.GetProperty(label_property_pair.second);
|
||||
if (value.IsNull()) {
|
||||
return;
|
||||
}
|
||||
|
@ -90,7 +90,7 @@ void InMemoryLabelPropertyIndex::UpdateOnAddLabel(LabelId added_label, Vertex *v
|
||||
if (label_prop.first != added_label) {
|
||||
continue;
|
||||
}
|
||||
auto prop_value = vertex_after_update->properties.GetProperty(label_prop.second);
|
||||
auto prop_value = vertex_after_update->GetProperty(label_prop.second);
|
||||
if (!prop_value.IsNull()) {
|
||||
auto acc = storage.access();
|
||||
acc.insert(Entry{std::move(prop_value), vertex_after_update, tx.start_timestamp});
|
||||
|
@ -1044,7 +1044,7 @@ void InMemoryStorage::InMemoryAccessor::Abort() {
|
||||
const auto &properties = index_stats.property_label.l2p.find(current->label.value);
|
||||
if (properties != index_stats.property_label.l2p.end()) {
|
||||
for (const auto &property : properties->second) {
|
||||
auto current_value = vertex->properties.GetProperty(property);
|
||||
auto current_value = vertex->GetProperty(property);
|
||||
if (!current_value.IsNull()) {
|
||||
label_property_cleanup[current->label.value].emplace_back(std::move(current_value), vertex);
|
||||
}
|
||||
@ -1065,13 +1065,13 @@ void InMemoryStorage::InMemoryAccessor::Abort() {
|
||||
// value
|
||||
const auto &labels = index_stats.property_label.p2l.find(current->property.key);
|
||||
if (labels != index_stats.property_label.p2l.end()) {
|
||||
auto current_value = vertex->properties.GetProperty(current->property.key);
|
||||
auto current_value = vertex->GetProperty(current->property.key);
|
||||
if (!current_value.IsNull()) {
|
||||
property_cleanup[current->property.key].emplace_back(std::move(current_value), vertex);
|
||||
}
|
||||
}
|
||||
// Setting the correct value
|
||||
vertex->properties.SetProperty(current->property.key, *current->property.value);
|
||||
vertex->SetProperty(current->property.key, *current->property.value);
|
||||
break;
|
||||
}
|
||||
case Delta::Action::ADD_IN_EDGE: {
|
||||
@ -1146,7 +1146,7 @@ void InMemoryStorage::InMemoryAccessor::Abort() {
|
||||
current->timestamp->load(std::memory_order_acquire) == transaction_.transaction_id) {
|
||||
switch (current->action) {
|
||||
case Delta::Action::SET_PROPERTY: {
|
||||
edge->properties.SetProperty(current->property.key, *current->property.value);
|
||||
edge->SetProperty(current->property.key, *current->property.value);
|
||||
break;
|
||||
}
|
||||
case Delta::Action::DELETE_DESERIALIZED_OBJECT:
|
||||
|
@ -64,7 +64,7 @@ bool LastCommittedVersionHasLabelProperty(const Vertex &vertex, LabelId label, c
|
||||
|
||||
size_t i = 0;
|
||||
for (const auto &property : properties) {
|
||||
current_value_equal_to_value[i] = vertex.properties.IsPropertyEqual(property, value_array[i]);
|
||||
current_value_equal_to_value[i] = vertex.IsPropertyEqual(property, value_array[i]);
|
||||
property_array.values[i] = property;
|
||||
i++;
|
||||
}
|
||||
@ -155,7 +155,7 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, const std::
|
||||
// If delta we need to fetch for later processing
|
||||
size_t i = 0;
|
||||
for (const auto &property : properties) {
|
||||
current_value_equal_to_value[i] = vertex.properties.IsPropertyEqual(property, values[i]);
|
||||
current_value_equal_to_value[i] = vertex.IsPropertyEqual(property, values[i]);
|
||||
property_array.values[i] = property;
|
||||
i++;
|
||||
}
|
||||
@ -163,7 +163,7 @@ bool AnyVersionHasLabelProperty(const Vertex &vertex, LabelId label, const std::
|
||||
// otherwise do a short-circuiting check (we already know !deleted && has_label)
|
||||
size_t i = 0;
|
||||
for (const auto &property : properties) {
|
||||
if (!vertex.properties.IsPropertyEqual(property, values[i])) return false;
|
||||
if (!vertex.IsPropertyEqual(property, values[i])) return false;
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
@ -269,7 +269,7 @@ void InMemoryUniqueConstraints::UpdateBeforeCommit(const Vertex *vertex, const T
|
||||
}
|
||||
|
||||
for (auto &[props, storage] : constraint->second) {
|
||||
auto values = vertex->properties.ExtractPropertyValues(props);
|
||||
auto values = vertex->ExtractPropertyValues(props);
|
||||
|
||||
if (!values) {
|
||||
continue;
|
||||
@ -334,7 +334,7 @@ std::optional<ConstraintViolation> InMemoryUniqueConstraints::DoValidate(
|
||||
if (vertex.deleted || !utils::Contains(vertex.labels, label)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto values = vertex.properties.ExtractPropertyValues(properties);
|
||||
auto values = vertex.ExtractPropertyValues(properties);
|
||||
if (!values) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@ -359,7 +359,7 @@ void InMemoryUniqueConstraints::AbortEntries(std::span<Vertex const *const> vert
|
||||
}
|
||||
|
||||
for (auto &[props, storage] : constraint->second) {
|
||||
auto values = vertex->properties.ExtractPropertyValues(props);
|
||||
auto values = vertex->ExtractPropertyValues(props);
|
||||
|
||||
if (!values) {
|
||||
continue;
|
||||
@ -451,7 +451,7 @@ std::optional<ConstraintViolation> InMemoryUniqueConstraints::Validate(const Ver
|
||||
}
|
||||
|
||||
for (const auto &[properties, storage] : constraint->second) {
|
||||
auto value_array = vertex.properties.ExtractPropertyValues(properties);
|
||||
auto value_array = vertex.ExtractPropertyValues(properties);
|
||||
|
||||
if (!value_array) {
|
||||
continue;
|
||||
|
49
src/storage/v2/property_disk_store.cpp
Normal file
49
src/storage/v2/property_disk_store.cpp
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "storage/v2/property_disk_store.hpp"
|
||||
|
||||
#include <rocksdb/compression_type.h>
|
||||
#include <rocksdb/filter_policy.h>
|
||||
#include <rocksdb/memtablerep.h>
|
||||
#include <rocksdb/options.h>
|
||||
#include <rocksdb/slice_transform.h>
|
||||
#include <rocksdb/statistics.h>
|
||||
#include <rocksdb/table.h>
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
PDS *PDS::ptr_ = nullptr;
|
||||
|
||||
PDS::PDS(std::filesystem::path root)
|
||||
: kvstore_{root / "pds", std::invoke([]() {
|
||||
rocksdb::Options options;
|
||||
rocksdb::BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = rocksdb::NewLRUCache(128 * 1024 * 1024);
|
||||
table_options.filter_policy.reset(rocksdb::NewBloomFilterPolicy(sizeof(storage::Gid)));
|
||||
table_options.optimize_filters_for_memory = false;
|
||||
table_options.enable_index_compression = false;
|
||||
options.table_factory.reset(rocksdb::NewBlockBasedTableFactory(table_options));
|
||||
options.prefix_extractor.reset(rocksdb::NewFixedPrefixTransform(sizeof(storage::Gid)));
|
||||
options.max_background_jobs = 4;
|
||||
options.enable_pipelined_write = true;
|
||||
options.avoid_unnecessary_blocking_io = true;
|
||||
|
||||
options.create_if_missing = true;
|
||||
|
||||
options.use_direct_io_for_flush_and_compaction = true;
|
||||
options.use_direct_reads = true;
|
||||
|
||||
// options.compression = rocksdb::kLZ4HCCompression;
|
||||
return options;
|
||||
})} {}
|
||||
|
||||
} // namespace memgraph::storage
|
137
src/storage/v2/property_disk_store.hpp
Normal file
137
src/storage/v2/property_disk_store.hpp
Normal file
@ -0,0 +1,137 @@
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <rocksdb/options.h>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <json/json.hpp>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include "kvstore/kvstore.hpp"
|
||||
#include "slk/streams.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
|
||||
#include "slk/serialization.hpp"
|
||||
#include "storage/v2/replication/slk.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
class PDS {
|
||||
public:
|
||||
static void Init(std::filesystem::path root) {
|
||||
if (ptr_ == nullptr) ptr_ = new PDS(root);
|
||||
}
|
||||
|
||||
static void Deinit() { delete ptr_; }
|
||||
|
||||
static PDS *get() {
|
||||
if (ptr_ == nullptr) {
|
||||
ptr_ = new PDS("/tmp");
|
||||
}
|
||||
return ptr_;
|
||||
}
|
||||
|
||||
static std::string ToKey(Gid gid, PropertyId pid) {
|
||||
std::string key(sizeof(gid) + sizeof(pid), '\0');
|
||||
memcpy(key.data(), &gid, sizeof(gid));
|
||||
memcpy(&key[sizeof(gid)], &pid, sizeof(pid));
|
||||
return key;
|
||||
}
|
||||
|
||||
static std::string ToPrefix(Gid gid) {
|
||||
std::string key(sizeof(gid), '\0');
|
||||
memcpy(key.data(), &gid, sizeof(gid));
|
||||
return key;
|
||||
}
|
||||
|
||||
static Gid ToGid(std::string_view sv) {
|
||||
uint64_t gid;
|
||||
gid = *((uint64_t *)sv.data());
|
||||
return Gid::FromUint(gid);
|
||||
}
|
||||
|
||||
static PropertyId ToPid(std::string_view sv) {
|
||||
uint32_t pid;
|
||||
pid = *((uint32_t *)&sv[sizeof(Gid)]);
|
||||
return PropertyId::FromUint(pid);
|
||||
}
|
||||
|
||||
static PropertyValue ToPV(std::string_view sv) {
|
||||
PropertyValue pv;
|
||||
slk::Reader reader((const uint8_t *)sv.data(), sv.size());
|
||||
slk::Load(&pv, &reader);
|
||||
return pv;
|
||||
}
|
||||
|
||||
static std::string ToStr(const PropertyValue &pv) {
|
||||
std::string val{};
|
||||
slk::Builder builder([&val](const uint8_t *data, size_t size, bool /*have_more*/) {
|
||||
const auto old_size = val.size();
|
||||
val.resize(old_size + size);
|
||||
memcpy(&val[old_size], data, size);
|
||||
});
|
||||
slk::Save(pv, &builder);
|
||||
builder.Finalize();
|
||||
return val;
|
||||
}
|
||||
|
||||
std::optional<PropertyValue> Get(Gid gid, PropertyId pid) {
|
||||
const auto element = kvstore_.Get(ToKey(gid, pid));
|
||||
if (element) {
|
||||
return ToPV(*element);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
size_t GetSize(Gid gid, PropertyId pid) {
|
||||
const auto element = kvstore_.Get(ToKey(gid, pid));
|
||||
if (element) {
|
||||
return element->size();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::map<PropertyId, PropertyValue> Get(Gid gid) {
|
||||
std::map<PropertyId, PropertyValue> res;
|
||||
auto itr = kvstore_.begin(ToPrefix(gid));
|
||||
auto end = kvstore_.end(ToPrefix(gid));
|
||||
for (; itr != end; ++itr) {
|
||||
if (!itr.IsValid()) continue;
|
||||
res[ToPid(itr->first)] = ToPV(itr->second);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
auto Set(Gid gid, PropertyId pid, const PropertyValue &pv) {
|
||||
if (pv.IsNull()) {
|
||||
return kvstore_.Delete(ToKey(gid, pid));
|
||||
}
|
||||
return kvstore_.Put(ToKey(gid, pid), ToStr(pv));
|
||||
}
|
||||
|
||||
void Clear(Gid gid) { kvstore_.DeletePrefix(ToPrefix(gid)); }
|
||||
|
||||
bool Has(Gid gid, PropertyId pid) { return kvstore_.Size(ToKey(gid, pid)) != 0; }
|
||||
|
||||
// kvstore::KVStore::iterator Itr() {}
|
||||
|
||||
private:
|
||||
PDS(std::filesystem::path root);
|
||||
kvstore::KVStore kvstore_;
|
||||
static PDS *ptr_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::storage
|
@ -92,7 +92,28 @@ class PropertyValue {
|
||||
// TODO: Implement copy assignment operators for primitive types.
|
||||
// TODO: Implement copy and move assignment operators for non-primitive types.
|
||||
|
||||
~PropertyValue() { DestroyValue(); }
|
||||
~PropertyValue() {
|
||||
switch (type_) {
|
||||
// destructor for primitive types does nothing
|
||||
case Type::Null:
|
||||
case Type::Bool:
|
||||
case Type::Int:
|
||||
case Type::Double:
|
||||
case Type::TemporalData:
|
||||
return;
|
||||
|
||||
// destructor for non primitive types since we used placement new
|
||||
case Type::String:
|
||||
std::destroy_at(&string_v.val_);
|
||||
return;
|
||||
case Type::List:
|
||||
std::destroy_at(&list_v.val_);
|
||||
return;
|
||||
case Type::Map:
|
||||
std::destroy_at(&map_v.val_);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Type type() const { return type_; }
|
||||
|
||||
@ -189,8 +210,6 @@ class PropertyValue {
|
||||
}
|
||||
|
||||
private:
|
||||
void DestroyValue() noexcept;
|
||||
|
||||
// NOTE: this may look strange but it is for better data layout
|
||||
// https://eel.is/c++draft/class.union#general-note-1
|
||||
union {
|
||||
@ -357,13 +376,13 @@ inline PropertyValue::PropertyValue(const PropertyValue &other) : type_(other.ty
|
||||
this->double_v.val_ = other.double_v.val_;
|
||||
return;
|
||||
case Type::String:
|
||||
new (&string_v.val_) std::string(other.string_v.val_);
|
||||
std::construct_at(&string_v.val_, other.string_v.val_);
|
||||
return;
|
||||
case Type::List:
|
||||
new (&list_v.val_) std::vector<PropertyValue>(other.list_v.val_);
|
||||
std::construct_at(&list_v.val_, other.list_v.val_);
|
||||
return;
|
||||
case Type::Map:
|
||||
new (&map_v.val_) std::map<std::string, PropertyValue>(other.map_v.val_);
|
||||
std::construct_at(&map_v.val_, other.map_v.val_);
|
||||
return;
|
||||
case Type::TemporalData:
|
||||
this->temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
@ -371,7 +390,7 @@ inline PropertyValue::PropertyValue(const PropertyValue &other) : type_(other.ty
|
||||
}
|
||||
}
|
||||
|
||||
inline PropertyValue::PropertyValue(PropertyValue &&other) noexcept : type_(std::exchange(other.type_, Type::Null)) {
|
||||
inline PropertyValue::PropertyValue(PropertyValue &&other) noexcept : type_(other.type_) {
|
||||
switch (type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
@ -386,15 +405,12 @@ inline PropertyValue::PropertyValue(PropertyValue &&other) noexcept : type_(std:
|
||||
break;
|
||||
case Type::String:
|
||||
std::construct_at(&string_v.val_, std::move(other.string_v.val_));
|
||||
std::destroy_at(&other.string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
std::construct_at(&list_v.val_, std::move(other.list_v.val_));
|
||||
std::destroy_at(&other.list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
std::construct_at(&map_v.val_, std::move(other.map_v.val_));
|
||||
std::destroy_at(&other.map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
@ -403,38 +419,88 @@ inline PropertyValue::PropertyValue(PropertyValue &&other) noexcept : type_(std:
|
||||
}
|
||||
|
||||
inline PropertyValue &PropertyValue::operator=(const PropertyValue &other) {
|
||||
if (this == &other) return *this;
|
||||
if (type_ == other.type_) {
|
||||
if (this == &other) return *this;
|
||||
switch (other.type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
bool_v.val_ = other.bool_v.val_;
|
||||
break;
|
||||
case Type::Int:
|
||||
int_v.val_ = other.int_v.val_;
|
||||
break;
|
||||
case Type::Double:
|
||||
double_v.val_ = other.double_v.val_;
|
||||
break;
|
||||
case Type::String:
|
||||
string_v.val_ = other.string_v.val_;
|
||||
break;
|
||||
case Type::List:
|
||||
list_v.val_ = other.list_v.val_;
|
||||
break;
|
||||
case Type::Map:
|
||||
map_v.val_ = other.map_v.val_;
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
break;
|
||||
}
|
||||
return *this;
|
||||
} else {
|
||||
// destroy
|
||||
switch (type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
break;
|
||||
case Type::Int:
|
||||
break;
|
||||
case Type::Double:
|
||||
break;
|
||||
case Type::String:
|
||||
std::destroy_at(&string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
std::destroy_at(&list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
std::destroy_at(&map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
break;
|
||||
}
|
||||
// construct
|
||||
auto *new_this = std::launder(this);
|
||||
switch (other.type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
new_this->bool_v.val_ = other.bool_v.val_;
|
||||
break;
|
||||
case Type::Int:
|
||||
new_this->int_v.val_ = other.int_v.val_;
|
||||
break;
|
||||
case Type::Double:
|
||||
new_this->double_v.val_ = other.double_v.val_;
|
||||
break;
|
||||
case Type::String:
|
||||
std::construct_at(&new_this->string_v.val_, other.string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
std::construct_at(&new_this->list_v.val_, other.list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
std::construct_at(&new_this->map_v.val_, other.map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
new_this->temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
break;
|
||||
}
|
||||
|
||||
DestroyValue();
|
||||
type_ = other.type_;
|
||||
|
||||
switch (other.type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
this->bool_v.val_ = other.bool_v.val_;
|
||||
break;
|
||||
case Type::Int:
|
||||
this->int_v.val_ = other.int_v.val_;
|
||||
break;
|
||||
case Type::Double:
|
||||
this->double_v.val_ = other.double_v.val_;
|
||||
break;
|
||||
case Type::String:
|
||||
new (&string_v.val_) std::string(other.string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
new (&list_v.val_) std::vector<PropertyValue>(other.list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
new (&map_v.val_) std::map<std::string, PropertyValue>(other.map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
this->temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
break;
|
||||
new_this->type_ = other.type_;
|
||||
return *new_this;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
inline PropertyValue &PropertyValue::operator=(PropertyValue &&other) noexcept {
|
||||
@ -456,48 +522,71 @@ inline PropertyValue &PropertyValue::operator=(PropertyValue &&other) noexcept {
|
||||
break;
|
||||
case Type::String:
|
||||
string_v.val_ = std::move(other.string_v.val_);
|
||||
std::destroy_at(&other.string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
list_v.val_ = std::move(other.list_v.val_);
|
||||
std::destroy_at(&other.list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
map_v.val_ = std::move(other.map_v.val_);
|
||||
std::destroy_at(&other.map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
break;
|
||||
}
|
||||
other.type_ = Type::Null;
|
||||
return *this;
|
||||
} else {
|
||||
std::destroy_at(this);
|
||||
return *std::construct_at(std::launder(this), std::move(other));
|
||||
}
|
||||
}
|
||||
// destroy
|
||||
switch (type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
break;
|
||||
case Type::Int:
|
||||
break;
|
||||
case Type::Double:
|
||||
break;
|
||||
case Type::String:
|
||||
std::destroy_at(&string_v.val_);
|
||||
break;
|
||||
case Type::List:
|
||||
std::destroy_at(&list_v.val_);
|
||||
break;
|
||||
case Type::Map:
|
||||
std::destroy_at(&map_v.val_);
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
break;
|
||||
}
|
||||
// construct (no need to destroy moved from type)
|
||||
auto *new_this = std::launder(this);
|
||||
switch (other.type_) {
|
||||
case Type::Null:
|
||||
break;
|
||||
case Type::Bool:
|
||||
new_this->bool_v.val_ = other.bool_v.val_;
|
||||
break;
|
||||
case Type::Int:
|
||||
new_this->int_v.val_ = other.int_v.val_;
|
||||
break;
|
||||
case Type::Double:
|
||||
new_this->double_v.val_ = other.double_v.val_;
|
||||
break;
|
||||
case Type::String:
|
||||
std::construct_at(&new_this->string_v.val_, std::move(other.string_v.val_));
|
||||
break;
|
||||
case Type::List:
|
||||
std::construct_at(&new_this->list_v.val_, std::move(other.list_v.val_));
|
||||
break;
|
||||
case Type::Map:
|
||||
std::construct_at(&new_this->map_v.val_, std::move(other.map_v.val_));
|
||||
break;
|
||||
case Type::TemporalData:
|
||||
new_this->temporal_data_v.val_ = other.temporal_data_v.val_;
|
||||
break;
|
||||
}
|
||||
|
||||
inline void PropertyValue::DestroyValue() noexcept {
|
||||
switch (std::exchange(type_, Type::Null)) {
|
||||
// destructor for primitive types does nothing
|
||||
case Type::Null:
|
||||
case Type::Bool:
|
||||
case Type::Int:
|
||||
case Type::Double:
|
||||
case Type::TemporalData:
|
||||
return;
|
||||
|
||||
// destructor for non primitive types since we used placement new
|
||||
case Type::String:
|
||||
std::destroy_at(&string_v.val_);
|
||||
return;
|
||||
case Type::List:
|
||||
std::destroy_at(&list_v.val_);
|
||||
return;
|
||||
case Type::Map:
|
||||
std::destroy_at(&map_v.val_);
|
||||
return;
|
||||
new_this->type_ = other.type_;
|
||||
return *new_this;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,9 @@ Storage::Storage(Config config, StorageMode storage_mode)
|
||||
indices_(config, storage_mode),
|
||||
constraints_(config, storage_mode) {
|
||||
spdlog::info("Created database with {} storage mode.", StorageModeToString(storage_mode));
|
||||
|
||||
// TODO: Make this work with MT
|
||||
PDS::Init(config_.durability.storage_directory);
|
||||
}
|
||||
|
||||
Storage::Accessor::Accessor(SharedAccess /* tag */, Storage *storage, IsolationLevel isolation_level,
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -12,6 +12,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
@ -21,19 +22,31 @@
|
||||
#include "storage/v2/property_store.hpp"
|
||||
#include "utils/rw_spin_lock.hpp"
|
||||
|
||||
#include "storage/v2/property_disk_store.hpp"
|
||||
|
||||
namespace memgraph::storage {
|
||||
|
||||
struct Vertex {
|
||||
Vertex(Gid gid, Delta *delta) : gid(gid), deleted(false), delta(delta) {
|
||||
Vertex(Gid gid, Delta *delta) : gid(gid), deleted(false), has_prop(false), delta(delta) {
|
||||
MG_ASSERT(delta == nullptr || delta->action == Delta::Action::DELETE_OBJECT ||
|
||||
delta->action == Delta::Action::DELETE_DESERIALIZED_OBJECT,
|
||||
"Vertex must be created with an initial DELETE_OBJECT delta!");
|
||||
}
|
||||
|
||||
~Vertex() {
|
||||
// TODO: Move to another place <- this will get called twice if moved...
|
||||
if (!moved) ClearProperties();
|
||||
}
|
||||
|
||||
Vertex(Vertex &) = delete;
|
||||
Vertex &operator=(Vertex &) = delete;
|
||||
Vertex(Vertex &&) noexcept = default;
|
||||
Vertex &operator=(Vertex &&) = delete;
|
||||
|
||||
const Gid gid;
|
||||
|
||||
std::vector<LabelId> labels;
|
||||
PropertyStore properties;
|
||||
// PropertyStore properties;
|
||||
|
||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> in_edges;
|
||||
std::vector<std::tuple<EdgeTypeId, Vertex *, EdgeRef>> out_edges;
|
||||
@ -42,8 +55,138 @@ struct Vertex {
|
||||
bool deleted;
|
||||
// uint8_t PAD;
|
||||
// uint16_t PAD;
|
||||
bool has_prop;
|
||||
|
||||
class HotFixMove {
|
||||
public:
|
||||
HotFixMove() {}
|
||||
HotFixMove(HotFixMove &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// We want only the latest object to be marked as not-moved; while all previous should be marked as moved
|
||||
moved = false;
|
||||
other.moved = true;
|
||||
}
|
||||
}
|
||||
HotFixMove(HotFixMove &) = delete;
|
||||
HotFixMove &operator=(HotFixMove &) = delete;
|
||||
HotFixMove &operator=(HotFixMove &&) = delete;
|
||||
|
||||
operator bool() const { return moved; }
|
||||
|
||||
private:
|
||||
bool moved{false};
|
||||
} moved;
|
||||
|
||||
Delta *delta;
|
||||
|
||||
PropertyValue GetProperty(PropertyId property) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
const auto prop = PDS::get()->Get(gid, property);
|
||||
if (prop) return *prop;
|
||||
return {};
|
||||
}
|
||||
|
||||
bool SetProperty(PropertyId property, const PropertyValue &value) {
|
||||
// if (deleted) return {};
|
||||
has_prop = true;
|
||||
return PDS::get()->Set(gid, property, value);
|
||||
}
|
||||
|
||||
bool HasProperty(PropertyId property) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->Has(gid, property);
|
||||
}
|
||||
|
||||
bool HasAllProperties(const std::set<PropertyId> &properties) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
return std::all_of(properties.begin(), properties.end(), [this](const auto &prop) { return HasProperty(prop); });
|
||||
}
|
||||
|
||||
bool IsPropertyEqual(PropertyId property, const PropertyValue &value) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return value.IsNull();
|
||||
const auto val = GetProperty(property);
|
||||
return val == value;
|
||||
}
|
||||
|
||||
template <typename TContainer>
|
||||
bool InitProperties(const TContainer &properties) {
|
||||
// if (deleted) return {};
|
||||
auto *pds = PDS::get();
|
||||
for (const auto &[property, value] : properties) {
|
||||
if (value.IsNull()) {
|
||||
continue;
|
||||
}
|
||||
if (!pds->Set(gid, property, value)) {
|
||||
return false;
|
||||
}
|
||||
has_prop = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearProperties() {
|
||||
if (!has_prop) return;
|
||||
has_prop = false;
|
||||
auto *pds = PDS::get();
|
||||
pds->Clear(gid);
|
||||
}
|
||||
|
||||
std::map<PropertyId, PropertyValue> Properties() {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->Get(gid);
|
||||
}
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> UpdateProperties(
|
||||
std::map<PropertyId, PropertyValue> &properties) {
|
||||
// if (deleted) return {};
|
||||
auto old_properties = Properties();
|
||||
ClearProperties();
|
||||
|
||||
std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>> id_old_new_change;
|
||||
id_old_new_change.reserve(properties.size() + old_properties.size());
|
||||
for (const auto &[prop_id, new_value] : properties) {
|
||||
if (!old_properties.contains(prop_id)) {
|
||||
id_old_new_change.emplace_back(prop_id, PropertyValue(), new_value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &[old_key, old_value] : old_properties) {
|
||||
auto [it, inserted] = properties.emplace(old_key, old_value);
|
||||
if (!inserted) {
|
||||
auto &new_value = it->second;
|
||||
id_old_new_change.emplace_back(it->first, old_value, new_value);
|
||||
}
|
||||
}
|
||||
|
||||
MG_ASSERT(InitProperties(properties));
|
||||
return id_old_new_change;
|
||||
}
|
||||
|
||||
uint64_t PropertySize(PropertyId property) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
return PDS::get()->GetSize(gid, property);
|
||||
}
|
||||
|
||||
std::optional<std::vector<PropertyValue>> ExtractPropertyValues(const std::set<PropertyId> &properties) const {
|
||||
// if (deleted) return {};
|
||||
if (!has_prop) return {};
|
||||
std::vector<PropertyValue> value_array;
|
||||
value_array.reserve(properties.size());
|
||||
for (const auto &prop : properties) {
|
||||
auto value = GetProperty(prop);
|
||||
if (value.IsNull()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
value_array.emplace_back(std::move(value));
|
||||
}
|
||||
return value_array;
|
||||
}
|
||||
};
|
||||
|
||||
static_assert(alignof(Vertex) >= 8, "The Vertex should be aligned to at least 8!");
|
||||
|
@ -261,7 +261,7 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
|
||||
|
||||
if (vertex_->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
auto current_value = vertex_->properties.GetProperty(property);
|
||||
auto current_value = vertex_->GetProperty(property);
|
||||
// We could skip setting the value if the previous one is the same to the new
|
||||
// one. This would save some memory as a delta would not be created as well as
|
||||
// avoid copying the value. The reason we are not doing that is because the
|
||||
@ -272,7 +272,7 @@ Result<PropertyValue> VertexAccessor::SetProperty(PropertyId property, const Pro
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[transaction = transaction_, vertex = vertex_, &value, &property, ¤t_value]() {
|
||||
CreateAndLinkDelta(transaction, vertex, Delta::SetPropertyTag(), property, current_value);
|
||||
vertex->properties.SetProperty(property, value);
|
||||
vertex->SetProperty(property, value);
|
||||
}};
|
||||
std::invoke(atomic_memory_block);
|
||||
|
||||
@ -303,7 +303,7 @@ Result<bool> VertexAccessor::InitProperties(const std::map<storage::PropertyId,
|
||||
bool result{false};
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[&result, &properties, storage = storage_, transaction = transaction_, vertex = vertex_]() {
|
||||
if (!vertex->properties.InitProperties(properties)) {
|
||||
if (!vertex->InitProperties(properties)) {
|
||||
result = false;
|
||||
return;
|
||||
}
|
||||
@ -339,11 +339,11 @@ Result<std::vector<std::tuple<PropertyId, PropertyValue, PropertyValue>>> Vertex
|
||||
|
||||
if (vertex_->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
using ReturnType = decltype(vertex_->properties.UpdateProperties(properties));
|
||||
using ReturnType = decltype(vertex_->UpdateProperties(properties));
|
||||
std::optional<ReturnType> id_old_new_change;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[storage = storage_, transaction = transaction_, vertex = vertex_, &properties, &id_old_new_change]() {
|
||||
id_old_new_change.emplace(vertex->properties.UpdateProperties(properties));
|
||||
id_old_new_change.emplace(vertex->UpdateProperties(properties));
|
||||
if (!id_old_new_change.has_value()) {
|
||||
return;
|
||||
}
|
||||
@ -375,11 +375,11 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
|
||||
|
||||
if (vertex_->deleted) return Error::DELETED_OBJECT;
|
||||
|
||||
using ReturnType = decltype(vertex_->properties.Properties());
|
||||
using ReturnType = decltype(vertex_->Properties());
|
||||
std::optional<ReturnType> properties;
|
||||
utils::AtomicMemoryBlock atomic_memory_block{
|
||||
[storage = storage_, transaction = transaction_, vertex = vertex_, &properties]() {
|
||||
properties.emplace(vertex->properties.Properties());
|
||||
properties.emplace(vertex->Properties());
|
||||
if (!properties.has_value()) {
|
||||
return;
|
||||
}
|
||||
@ -391,7 +391,7 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::ClearProperties() {
|
||||
if (transaction->constraint_verification_info) {
|
||||
transaction->constraint_verification_info->RemovedProperty(vertex);
|
||||
}
|
||||
vertex->properties.ClearProperties();
|
||||
vertex->ClearProperties();
|
||||
}};
|
||||
std::invoke(atomic_memory_block);
|
||||
|
||||
@ -406,7 +406,7 @@ Result<PropertyValue> VertexAccessor::GetProperty(PropertyId property, View view
|
||||
{
|
||||
auto guard = std::shared_lock{vertex_->lock};
|
||||
deleted = vertex_->deleted;
|
||||
value = vertex_->properties.GetProperty(property);
|
||||
value = vertex_->GetProperty(property);
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
|
||||
@ -451,7 +451,7 @@ Result<uint64_t> VertexAccessor::GetPropertySize(PropertyId property, View view)
|
||||
auto guard = std::shared_lock{vertex_->lock};
|
||||
Delta *delta = vertex_->delta;
|
||||
if (!delta) {
|
||||
return vertex_->properties.PropertySize(property);
|
||||
return vertex_->PropertySize(property);
|
||||
}
|
||||
}
|
||||
|
||||
@ -474,7 +474,7 @@ Result<std::map<PropertyId, PropertyValue>> VertexAccessor::Properties(View view
|
||||
{
|
||||
auto guard = std::shared_lock{vertex_->lock};
|
||||
deleted = vertex_->deleted;
|
||||
properties = vertex_->properties.Properties();
|
||||
properties = vertex_->Properties();
|
||||
delta = vertex_->delta;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -150,128 +150,133 @@ void *MonotonicBufferResource::DoAllocate(size_t bytes, size_t alignment) {
|
||||
|
||||
namespace impl {
|
||||
|
||||
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *memory)
|
||||
: blocks_per_chunk_(blocks_per_chunk), block_size_(block_size), chunks_(memory) {}
|
||||
|
||||
Pool::~Pool() { MG_ASSERT(chunks_.empty(), "You need to call Release before destruction!"); }
|
||||
|
||||
void *Pool::Allocate() {
|
||||
auto allocate_block_from_chunk = [this](Chunk *chunk) {
|
||||
unsigned char *available_block = chunk->data + (chunk->first_available_block_ix * block_size_);
|
||||
// Update free-list pointer (index in our case) by reading "next" from the
|
||||
// available_block.
|
||||
chunk->first_available_block_ix = *available_block;
|
||||
--chunk->blocks_available;
|
||||
return available_block;
|
||||
};
|
||||
if (last_alloc_chunk_ && last_alloc_chunk_->blocks_available > 0U)
|
||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
||||
// Find a Chunk with available memory.
|
||||
for (auto &chunk : chunks_) {
|
||||
if (chunk.blocks_available > 0U) {
|
||||
last_alloc_chunk_ = &chunk;
|
||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
||||
}
|
||||
}
|
||||
// We haven't found a Chunk with available memory, so allocate a new one.
|
||||
if (block_size_ > std::numeric_limits<size_t>::max() / blocks_per_chunk_) throw BadAlloc("Allocation size overflow");
|
||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
||||
Pool::Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *chunk_memory)
|
||||
: blocks_per_chunk_(blocks_per_chunk), block_size_(block_size), chunks_(chunk_memory) {
|
||||
// Use the next pow2 of block_size_ as alignment, so that we cover alignment
|
||||
// requests between 1 and block_size_. Users of this class should make sure
|
||||
// that requested alignment of particular blocks is never greater than the
|
||||
// block itself.
|
||||
size_t alignment = Ceil2(block_size_);
|
||||
if (alignment < block_size_) throw BadAlloc("Allocation alignment overflow");
|
||||
auto *data = reinterpret_cast<unsigned char *>(GetUpstreamResource()->Allocate(data_size, alignment));
|
||||
// Form a free-list of blocks in data.
|
||||
for (unsigned char i = 0U; i < blocks_per_chunk_; ++i) {
|
||||
*(data + (i * block_size_)) = i + 1U;
|
||||
}
|
||||
Chunk chunk{data, 0, blocks_per_chunk_};
|
||||
// Insert the big block in the sorted position.
|
||||
auto it = std::lower_bound(chunks_.begin(), chunks_.end(), chunk,
|
||||
[](const auto &a, const auto &b) { return a.data < b.data; });
|
||||
try {
|
||||
it = chunks_.insert(it, chunk);
|
||||
} catch (...) {
|
||||
GetUpstreamResource()->Deallocate(data, data_size, alignment);
|
||||
throw;
|
||||
}
|
||||
if (block_size_ > std::numeric_limits<size_t>::max() / blocks_per_chunk_) throw BadAlloc("Allocation size overflow");
|
||||
}
|
||||
|
||||
last_alloc_chunk_ = &*it;
|
||||
last_dealloc_chunk_ = &*it;
|
||||
return allocate_block_from_chunk(last_alloc_chunk_);
|
||||
Pool::~Pool() {
|
||||
if (!chunks_.empty()) {
|
||||
auto *resource = GetUpstreamResource();
|
||||
auto const dataSize = blocks_per_chunk_ * block_size_;
|
||||
auto const alignment = Ceil2(block_size_);
|
||||
for (auto &chunk : chunks_) {
|
||||
resource->Deallocate(chunk.raw_data, dataSize, alignment);
|
||||
}
|
||||
chunks_.clear();
|
||||
}
|
||||
free_list_ = nullptr;
|
||||
}
|
||||
|
||||
void *Pool::Allocate() {
|
||||
if (!free_list_) [[unlikely]] {
|
||||
// need new chunk
|
||||
auto const data_size = blocks_per_chunk_ * block_size_;
|
||||
auto const alignment = Ceil2(block_size_);
|
||||
auto *resource = GetUpstreamResource();
|
||||
auto *data = reinterpret_cast<std::byte *>(resource->Allocate(data_size, alignment));
|
||||
try {
|
||||
auto &new_chunk = chunks_.emplace_front(data);
|
||||
free_list_ = new_chunk.build_freelist(block_size_, blocks_per_chunk_);
|
||||
} catch (...) {
|
||||
resource->Deallocate(data, data_size, alignment);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return std::exchange(free_list_, *reinterpret_cast<std::byte **>(free_list_));
|
||||
}
|
||||
|
||||
void Pool::Deallocate(void *p) {
|
||||
MG_ASSERT(last_dealloc_chunk_, "No chunk to deallocate");
|
||||
MG_ASSERT(!chunks_.empty(),
|
||||
"Expected a call to Deallocate after at least a "
|
||||
"single Allocate has been done.");
|
||||
auto is_in_chunk = [this, p](const Chunk &chunk) {
|
||||
auto ptr = reinterpret_cast<uintptr_t>(p);
|
||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
||||
return reinterpret_cast<uintptr_t>(chunk.data) <= ptr && ptr < reinterpret_cast<uintptr_t>(chunk.data + data_size);
|
||||
};
|
||||
auto deallocate_block_from_chunk = [this, p](Chunk *chunk) {
|
||||
// NOTE: This check is not enough to cover all double-free issues.
|
||||
MG_ASSERT(chunk->blocks_available < blocks_per_chunk_,
|
||||
"Deallocating more blocks than a chunk can contain, possibly a "
|
||||
"double-free situation or we have a bug in the allocator.");
|
||||
// Link the block into the free-list
|
||||
auto *block = reinterpret_cast<unsigned char *>(p);
|
||||
*block = chunk->first_available_block_ix;
|
||||
chunk->first_available_block_ix = (block - chunk->data) / block_size_;
|
||||
chunk->blocks_available++;
|
||||
};
|
||||
if (is_in_chunk(*last_dealloc_chunk_)) {
|
||||
deallocate_block_from_chunk(last_dealloc_chunk_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the chunk which served this allocation
|
||||
Chunk chunk{reinterpret_cast<unsigned char *>(p) - blocks_per_chunk_ * block_size_, 0, 0};
|
||||
auto it = std::lower_bound(chunks_.begin(), chunks_.end(), chunk,
|
||||
[](const auto &a, const auto &b) { return a.data <= b.data; });
|
||||
MG_ASSERT(it != chunks_.end(), "Failed deallocation in utils::Pool");
|
||||
MG_ASSERT(is_in_chunk(*it), "Failed deallocation in utils::Pool");
|
||||
|
||||
// Update last_alloc_chunk_ as well because it now has a free block.
|
||||
// Additionally this corresponds with C++ pattern of allocations and
|
||||
// deallocations being done in reverse order.
|
||||
last_alloc_chunk_ = &*it;
|
||||
last_dealloc_chunk_ = &*it;
|
||||
deallocate_block_from_chunk(last_dealloc_chunk_);
|
||||
// TODO: We could release the Chunk to upstream memory
|
||||
}
|
||||
|
||||
void Pool::Release() {
|
||||
for (auto &chunk : chunks_) {
|
||||
size_t data_size = blocks_per_chunk_ * block_size_;
|
||||
size_t alignment = Ceil2(block_size_);
|
||||
GetUpstreamResource()->Deallocate(chunk.data, data_size, alignment);
|
||||
}
|
||||
chunks_.clear();
|
||||
last_alloc_chunk_ = nullptr;
|
||||
last_dealloc_chunk_ = nullptr;
|
||||
*reinterpret_cast<std::byte **>(p) = std::exchange(free_list_, reinterpret_cast<std::byte *>(p));
|
||||
}
|
||||
|
||||
} // namespace impl
|
||||
|
||||
PoolResource::PoolResource(size_t max_blocks_per_chunk, size_t max_block_size, MemoryResource *memory_pools,
|
||||
MemoryResource *memory_unpooled)
|
||||
: pools_(memory_pools),
|
||||
unpooled_(memory_unpooled),
|
||||
max_blocks_per_chunk_(std::min(max_blocks_per_chunk, static_cast<size_t>(impl::Pool::MaxBlocksInChunk()))),
|
||||
max_block_size_(max_block_size) {
|
||||
MG_ASSERT(max_blocks_per_chunk_ > 0U, "Invalid number of blocks per chunk");
|
||||
MG_ASSERT(max_block_size_ > 0U, "Invalid size of block");
|
||||
struct NullMemoryResourceImpl final : public MemoryResource {
|
||||
NullMemoryResourceImpl() = default;
|
||||
NullMemoryResourceImpl(NullMemoryResourceImpl const &) = default;
|
||||
NullMemoryResourceImpl &operator=(NullMemoryResourceImpl const &) = default;
|
||||
NullMemoryResourceImpl(NullMemoryResourceImpl &&) = default;
|
||||
NullMemoryResourceImpl &operator=(NullMemoryResourceImpl &&) = default;
|
||||
~NullMemoryResourceImpl() override = default;
|
||||
|
||||
private:
|
||||
void *DoAllocate(size_t /*bytes*/, size_t /*alignment*/) override {
|
||||
throw BadAlloc{"NullMemoryResource doesn't allocate"};
|
||||
}
|
||||
void DoDeallocate(void * /*p*/, size_t /*bytes*/, size_t /*alignment*/) override {
|
||||
throw BadAlloc{"NullMemoryResource doesn't deallocate"};
|
||||
}
|
||||
bool DoIsEqual(MemoryResource const &other) const noexcept override {
|
||||
return dynamic_cast<NullMemoryResourceImpl const *>(&other) != nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
MemoryResource *NullMemoryResource() noexcept {
|
||||
static auto res = NullMemoryResourceImpl{};
|
||||
return &res;
|
||||
}
|
||||
|
||||
namespace impl {
|
||||
|
||||
/// 1 bit sensitivity test
|
||||
static_assert(bin_index<1>(9U) == 0);
|
||||
static_assert(bin_index<1>(10U) == 0);
|
||||
static_assert(bin_index<1>(11U) == 0);
|
||||
static_assert(bin_index<1>(12U) == 0);
|
||||
static_assert(bin_index<1>(13U) == 0);
|
||||
static_assert(bin_index<1>(14U) == 0);
|
||||
static_assert(bin_index<1>(15U) == 0);
|
||||
static_assert(bin_index<1>(16U) == 0);
|
||||
|
||||
static_assert(bin_index<1>(17U) == 1);
|
||||
static_assert(bin_index<1>(18U) == 1);
|
||||
static_assert(bin_index<1>(19U) == 1);
|
||||
static_assert(bin_index<1>(20U) == 1);
|
||||
static_assert(bin_index<1>(21U) == 1);
|
||||
static_assert(bin_index<1>(22U) == 1);
|
||||
static_assert(bin_index<1>(23U) == 1);
|
||||
static_assert(bin_index<1>(24U) == 1);
|
||||
static_assert(bin_index<1>(25U) == 1);
|
||||
static_assert(bin_index<1>(26U) == 1);
|
||||
static_assert(bin_index<1>(27U) == 1);
|
||||
static_assert(bin_index<1>(28U) == 1);
|
||||
static_assert(bin_index<1>(29U) == 1);
|
||||
static_assert(bin_index<1>(30U) == 1);
|
||||
static_assert(bin_index<1>(31U) == 1);
|
||||
static_assert(bin_index<1>(32U) == 1);
|
||||
|
||||
/// 2 bit sensitivity test
|
||||
|
||||
static_assert(bin_index<2>(9U) == 0);
|
||||
static_assert(bin_index<2>(10U) == 0);
|
||||
static_assert(bin_index<2>(11U) == 0);
|
||||
static_assert(bin_index<2>(12U) == 0);
|
||||
|
||||
static_assert(bin_index<2>(13U) == 1);
|
||||
static_assert(bin_index<2>(14U) == 1);
|
||||
static_assert(bin_index<2>(15U) == 1);
|
||||
static_assert(bin_index<2>(16U) == 1);
|
||||
|
||||
static_assert(bin_index<2>(17U) == 2);
|
||||
static_assert(bin_index<2>(18U) == 2);
|
||||
static_assert(bin_index<2>(19U) == 2);
|
||||
static_assert(bin_index<2>(20U) == 2);
|
||||
static_assert(bin_index<2>(21U) == 2);
|
||||
static_assert(bin_index<2>(22U) == 2);
|
||||
static_assert(bin_index<2>(23U) == 2);
|
||||
static_assert(bin_index<2>(24U) == 2);
|
||||
|
||||
} // namespace impl
|
||||
|
||||
void *PoolResource::DoAllocate(size_t bytes, size_t alignment) {
|
||||
// Take the max of `bytes` and `alignment` so that we simplify handling
|
||||
// alignment requests.
|
||||
size_t block_size = std::max(bytes, alignment);
|
||||
size_t block_size = std::max({bytes, alignment, 1UL});
|
||||
// Check that we have received a regular allocation request with non-padded
|
||||
// structs/classes in play. These will always have
|
||||
// `sizeof(T) % alignof(T) == 0`. Special requests which don't have that
|
||||
@ -279,80 +284,36 @@ void *PoolResource::DoAllocate(size_t bytes, size_t alignment) {
|
||||
// have to write a general-purpose allocator which has to behave as complex
|
||||
// as malloc/free.
|
||||
if (block_size % alignment != 0) throw BadAlloc("Requested bytes must be a multiple of alignment");
|
||||
if (block_size > max_block_size_) {
|
||||
// Allocate a big block.
|
||||
BigBlock big_block{bytes, alignment, GetUpstreamResourceBlocks()->Allocate(bytes, alignment)};
|
||||
// Insert the big block in the sorted position.
|
||||
auto it = std::lower_bound(unpooled_.begin(), unpooled_.end(), big_block,
|
||||
[](const auto &a, const auto &b) { return a.data < b.data; });
|
||||
try {
|
||||
unpooled_.insert(it, big_block);
|
||||
} catch (...) {
|
||||
GetUpstreamResourceBlocks()->Deallocate(big_block.data, bytes, alignment);
|
||||
throw;
|
||||
}
|
||||
return big_block.data;
|
||||
}
|
||||
// Allocate a regular block, first check if last_alloc_pool_ is suitable.
|
||||
if (last_alloc_pool_ && last_alloc_pool_->GetBlockSize() == block_size) {
|
||||
return last_alloc_pool_->Allocate();
|
||||
}
|
||||
// Find the pool with greater or equal block_size.
|
||||
impl::Pool pool(block_size, max_blocks_per_chunk_, GetUpstreamResource());
|
||||
auto it = std::lower_bound(pools_.begin(), pools_.end(), pool,
|
||||
[](const auto &a, const auto &b) { return a.GetBlockSize() < b.GetBlockSize(); });
|
||||
if (it != pools_.end() && it->GetBlockSize() == block_size) {
|
||||
last_alloc_pool_ = &*it;
|
||||
last_dealloc_pool_ = &*it;
|
||||
return it->Allocate();
|
||||
}
|
||||
// We don't have a pool for this block_size, so insert it in the sorted
|
||||
// position.
|
||||
it = pools_.emplace(it, std::move(pool));
|
||||
last_alloc_pool_ = &*it;
|
||||
last_dealloc_pool_ = &*it;
|
||||
return it->Allocate();
|
||||
}
|
||||
|
||||
if (block_size <= 64) {
|
||||
return mini_pools_[(block_size - 1UL) / 8UL].Allocate();
|
||||
}
|
||||
if (block_size <= 128) {
|
||||
return pools_3bit_.allocate(block_size);
|
||||
}
|
||||
if (block_size <= 512) {
|
||||
return pools_4bit_.allocate(block_size);
|
||||
}
|
||||
if (block_size <= 1024) {
|
||||
return pools_5bit_.allocate(block_size);
|
||||
}
|
||||
return unpooled_memory_->Allocate(bytes, alignment);
|
||||
}
|
||||
void PoolResource::DoDeallocate(void *p, size_t bytes, size_t alignment) {
|
||||
size_t block_size = std::max(bytes, alignment);
|
||||
MG_ASSERT(block_size % alignment == 0,
|
||||
"PoolResource shouldn't serve allocation requests where bytes aren't "
|
||||
"a multiple of alignment");
|
||||
if (block_size > max_block_size_) {
|
||||
// Deallocate a big block.
|
||||
BigBlock big_block{bytes, alignment, p};
|
||||
auto it = std::lower_bound(unpooled_.begin(), unpooled_.end(), big_block,
|
||||
[](const auto &a, const auto &b) { return a.data < b.data; });
|
||||
MG_ASSERT(it != unpooled_.end(), "Failed deallocation");
|
||||
MG_ASSERT(it->data == p && it->bytes == bytes && it->alignment == alignment, "Failed deallocation");
|
||||
unpooled_.erase(it);
|
||||
GetUpstreamResourceBlocks()->Deallocate(p, bytes, alignment);
|
||||
return;
|
||||
size_t block_size = std::max({bytes, alignment, 1UL});
|
||||
DMG_ASSERT(block_size % alignment == 0);
|
||||
|
||||
if (block_size <= 64) {
|
||||
mini_pools_[(block_size - 1UL) / 8UL].Deallocate(p);
|
||||
} else if (block_size <= 128) {
|
||||
pools_3bit_.deallocate(p, block_size);
|
||||
} else if (block_size <= 512) {
|
||||
pools_4bit_.deallocate(p, block_size);
|
||||
} else if (block_size <= 1024) {
|
||||
pools_5bit_.deallocate(p, block_size);
|
||||
} else {
|
||||
unpooled_memory_->Deallocate(p, bytes, alignment);
|
||||
}
|
||||
// Deallocate a regular block, first check if last_dealloc_pool_ is suitable.
|
||||
if (last_dealloc_pool_ && last_dealloc_pool_->GetBlockSize() == block_size) return last_dealloc_pool_->Deallocate(p);
|
||||
// Find the pool with equal block_size.
|
||||
impl::Pool pool(block_size, max_blocks_per_chunk_, GetUpstreamResource());
|
||||
auto it = std::lower_bound(pools_.begin(), pools_.end(), pool,
|
||||
[](const auto &a, const auto &b) { return a.GetBlockSize() < b.GetBlockSize(); });
|
||||
MG_ASSERT(it != pools_.end(), "Failed deallocation");
|
||||
MG_ASSERT(it->GetBlockSize() == block_size, "Failed deallocation");
|
||||
last_alloc_pool_ = &*it;
|
||||
last_dealloc_pool_ = &*it;
|
||||
return it->Deallocate(p);
|
||||
}
|
||||
|
||||
void PoolResource::Release() {
|
||||
for (auto &pool : pools_) pool.Release();
|
||||
pools_.clear();
|
||||
for (auto &big_block : unpooled_)
|
||||
GetUpstreamResourceBlocks()->Deallocate(big_block.data, big_block.bytes, big_block.alignment);
|
||||
unpooled_.clear();
|
||||
last_alloc_pool_ = nullptr;
|
||||
last_dealloc_pool_ = nullptr;
|
||||
}
|
||||
|
||||
// PoolResource END
|
||||
|
||||
bool PoolResource::DoIsEqual(MemoryResource const &other) const noexcept { return this == &other; }
|
||||
} // namespace memgraph::utils
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -15,7 +15,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <forward_list>
|
||||
#include <list>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
@ -248,6 +252,8 @@ bool operator!=(const Allocator<T> &a, const Allocator<U> &b) {
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
auto NullMemoryResource() noexcept -> MemoryResource *;
|
||||
|
||||
/// Wraps std::pmr::memory_resource for use with out MemoryResource
|
||||
class StdMemoryResource final : public MemoryResource {
|
||||
public:
|
||||
@ -380,37 +386,45 @@ class MonotonicBufferResource final : public MemoryResource {
|
||||
|
||||
namespace impl {
|
||||
|
||||
template <class T>
|
||||
using AList = std::forward_list<T, Allocator<T>>;
|
||||
|
||||
template <class T>
|
||||
using AVector = std::vector<T, Allocator<T>>;
|
||||
|
||||
/// Holds a number of Chunks each serving blocks of particular size. When a
|
||||
/// Chunk runs out of available blocks, a new Chunk is allocated. The naming is
|
||||
/// taken from `libstdc++` implementation, but the implementation details are
|
||||
/// more similar to `FixedAllocator` described in "Small Object Allocation" from
|
||||
/// "Modern C++ Design".
|
||||
/// Chunk runs out of available blocks, a new Chunk is allocated.
|
||||
class Pool final {
|
||||
/// Holds a pointer into a chunk of memory which consists of equal sized
|
||||
/// blocks. Each Chunk can handle `std::numeric_limits<unsigned char>::max()`
|
||||
/// number of blocks. Blocks form a "free-list", where each unused block has
|
||||
/// an embedded index to the next unused block.
|
||||
/// blocks. Blocks form a "free-list"
|
||||
struct Chunk {
|
||||
unsigned char *data;
|
||||
unsigned char first_available_block_ix;
|
||||
unsigned char blocks_available;
|
||||
// TODO: make blocks_per_chunk a per chunk thing (ie. allow chunk growth)
|
||||
std::byte *raw_data;
|
||||
explicit Chunk(std::byte *rawData) : raw_data(rawData) {}
|
||||
std::byte *build_freelist(std::size_t block_size, std::size_t blocks_in_chunk) {
|
||||
auto current = raw_data;
|
||||
std::byte *prev = nullptr;
|
||||
auto end = current + (blocks_in_chunk * block_size);
|
||||
while (current != end) {
|
||||
std::byte **list_entry = reinterpret_cast<std::byte **>(current);
|
||||
*list_entry = std::exchange(prev, current);
|
||||
current += block_size;
|
||||
}
|
||||
DMG_ASSERT(prev != nullptr);
|
||||
return prev;
|
||||
}
|
||||
};
|
||||
|
||||
unsigned char blocks_per_chunk_;
|
||||
size_t block_size_;
|
||||
AVector<Chunk> chunks_;
|
||||
Chunk *last_alloc_chunk_{nullptr};
|
||||
Chunk *last_dealloc_chunk_{nullptr};
|
||||
std::byte *free_list_{nullptr};
|
||||
uint8_t blocks_per_chunk_{};
|
||||
std::size_t block_size_{};
|
||||
|
||||
AList<Chunk> chunks_; // TODO: do ourself so we can do fast Release (detect monotonic, do nothing)
|
||||
|
||||
public:
|
||||
static constexpr auto MaxBlocksInChunk() {
|
||||
return std::numeric_limits<decltype(Chunk::first_available_block_ix)>::max();
|
||||
}
|
||||
static constexpr auto MaxBlocksInChunk = std::numeric_limits<decltype(blocks_per_chunk_)>::max();
|
||||
|
||||
Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *memory);
|
||||
Pool(size_t block_size, unsigned char blocks_per_chunk, MemoryResource *chunk_memory);
|
||||
|
||||
Pool(const Pool &) = delete;
|
||||
Pool &operator=(const Pool &) = delete;
|
||||
@ -430,8 +444,145 @@ class Pool final {
|
||||
void *Allocate();
|
||||
|
||||
void Deallocate(void *p);
|
||||
};
|
||||
|
||||
void Release();
|
||||
// C++ overloads for clz
|
||||
constexpr auto clz(unsigned int x) { return __builtin_clz(x); }
|
||||
constexpr auto clz(unsigned long x) { return __builtin_clzl(x); }
|
||||
constexpr auto clz(unsigned long long x) { return __builtin_clzll(x); }
|
||||
|
||||
template <typename T>
|
||||
constexpr auto bits_sizeof = sizeof(T) * CHAR_BIT;
|
||||
|
||||
/// 0-based bit index of the most significant bit assumed that `n` != 0
|
||||
template <typename T>
|
||||
constexpr auto msb_index(T n) {
|
||||
return bits_sizeof<T> - clz(n) - T(1);
|
||||
}
|
||||
|
||||
/* This function will in O(1) time provide a bin index based on:
|
||||
* B - the number of most significant bits to be sensitive to
|
||||
* LB - the value that should be considered below the consideration for bin index of 0 (LB is exclusive)
|
||||
*
|
||||
* lets say we were:
|
||||
* - sensitive to two bits (B == 2)
|
||||
* - lowest bin is for 8 (LB == 8)
|
||||
*
|
||||
* our bin indexes would look like:
|
||||
* 0 - 0000'1100 12
|
||||
* 1 - 0001'0000 16
|
||||
* 2 - 0001'1000 24
|
||||
* 3 - 0010'0000 32
|
||||
* 4 - 0011'0000 48
|
||||
* 5 - 0100'0000 64
|
||||
* 6 - 0110'0000 96
|
||||
* 7 - 1000'0000 128
|
||||
* 8 - 1100'0000 192
|
||||
* ...
|
||||
*
|
||||
* Example:
|
||||
* Given n == 70, we want to return the bin index to the first value which is
|
||||
* larger than n.
|
||||
* bin_index<2,8>(70) => 6, as 64 (index 5) < 70 and 70 <= 96 (index 6)
|
||||
*/
|
||||
template <std::size_t B = 2, std::size_t LB = 8>
|
||||
constexpr std::size_t bin_index(std::size_t n) {
|
||||
static_assert(B >= 1U, "Needs to be sensitive to at least one bit");
|
||||
static_assert(LB != 0U, "Lower bound need to be non-zero");
|
||||
DMG_ASSERT(n > LB);
|
||||
|
||||
// We will alway be sensitive to at least the MSB
|
||||
// exponent tells us how many bits we need to use to select within a level
|
||||
constexpr auto kExponent = B - 1U;
|
||||
// 2^exponent gives the size of each level
|
||||
constexpr auto kSize = 1U << kExponent;
|
||||
// offset help adjust results down to be inline with bin_index(LB) == 0
|
||||
constexpr auto kOffset = msb_index(LB);
|
||||
|
||||
auto const msb_idx = msb_index(n);
|
||||
DMG_ASSERT(msb_idx != 0);
|
||||
|
||||
auto const mask = (1u << msb_idx) - 1u;
|
||||
auto const under = n & mask;
|
||||
auto const selector = under >> (msb_idx - kExponent);
|
||||
|
||||
auto const rest = under & (mask >> kExponent);
|
||||
auto const no_overflow = rest == 0U;
|
||||
|
||||
auto const msb_level = kSize * (msb_idx - kOffset);
|
||||
return msb_level + selector - no_overflow;
|
||||
}
|
||||
|
||||
// This is the inverse opperation for bin_index
|
||||
// bin_size(bin_index(X)-1) < X <= bin_size(bin_index(X))
|
||||
template <std::size_t B = 2, std::size_t LB = 8>
|
||||
std::size_t bin_size(std::size_t idx) {
|
||||
constexpr auto kExponent = B - 1U;
|
||||
constexpr auto kSize = 1U << kExponent;
|
||||
constexpr auto kOffset = msb_index(LB);
|
||||
|
||||
// no need to optimise `/` or `%` compiler can see `kSize` is a power of 2
|
||||
auto const level = (idx + 1) / kSize;
|
||||
auto const sub_level = (idx + 1) % kSize;
|
||||
return (1U << (level + kOffset)) | (sub_level << (level + kOffset - kExponent));
|
||||
}
|
||||
|
||||
template <std::size_t Bits, std::size_t LB, std::size_t UB>
|
||||
struct MultiPool {
|
||||
static_assert(LB < UB, "lower bound must be less than upper bound");
|
||||
static_assert(IsPow2(LB) && IsPow2(UB), "Design untested for non powers of 2");
|
||||
static_assert((LB << Bits) % sizeof(void *) == 0, "Smallest pool must have space and alignment for freelist");
|
||||
|
||||
// upper bound is inclusive
|
||||
static bool is_size_handled(std::size_t size) { return LB < size && size <= UB; }
|
||||
static bool is_above_upper_bound(std::size_t size) { return UB < size; }
|
||||
|
||||
static constexpr auto n_bins = bin_index<Bits, LB>(UB) + 1U;
|
||||
|
||||
MultiPool(uint8_t blocks_per_chunk, MemoryResource *memory, MemoryResource *internal_memory)
|
||||
: blocks_per_chunk_{blocks_per_chunk}, memory_{memory}, internal_memory_{internal_memory} {}
|
||||
|
||||
~MultiPool() {
|
||||
if (pools_) {
|
||||
auto pool_alloc = Allocator<Pool>(internal_memory_);
|
||||
for (auto i = 0U; i != n_bins; ++i) {
|
||||
pool_alloc.destroy(&pools_[i]);
|
||||
}
|
||||
pool_alloc.deallocate(pools_, n_bins);
|
||||
}
|
||||
}
|
||||
|
||||
void *allocate(std::size_t bytes) {
|
||||
auto idx = bin_index<Bits, LB>(bytes);
|
||||
if (!pools_) initialise_pools();
|
||||
return pools_[idx].Allocate();
|
||||
}
|
||||
|
||||
void deallocate(void *ptr, std::size_t bytes) {
|
||||
auto idx = bin_index<Bits, LB>(bytes);
|
||||
pools_[idx].Deallocate(ptr);
|
||||
}
|
||||
|
||||
private:
|
||||
void initialise_pools() {
|
||||
auto pool_alloc = Allocator<Pool>(internal_memory_);
|
||||
auto pools = pool_alloc.allocate(n_bins);
|
||||
try {
|
||||
for (auto i = 0U; i != n_bins; ++i) {
|
||||
auto block_size = bin_size<Bits, LB>(i);
|
||||
pool_alloc.construct(&pools[i], block_size, blocks_per_chunk_, memory_);
|
||||
}
|
||||
pools_ = pools;
|
||||
} catch (...) {
|
||||
pool_alloc.deallocate(pools, n_bins);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
Pool *pools_{};
|
||||
uint8_t blocks_per_chunk_{};
|
||||
MemoryResource *memory_{};
|
||||
MemoryResource *internal_memory_{};
|
||||
};
|
||||
|
||||
} // namespace impl
|
||||
@ -442,8 +593,6 @@ class Pool final {
|
||||
///
|
||||
/// This class has the following properties with regards to memory management.
|
||||
///
|
||||
/// * All allocated memory will be freed upon destruction, even if Deallocate
|
||||
/// has not been called for some of the allocated blocks.
|
||||
/// * It consists of a collection of impl::Pool instances, each serving
|
||||
/// requests for different block sizes. Each impl::Pool manages a collection
|
||||
/// of impl::Pool::Chunk instances which are divided into blocks of uniform
|
||||
@ -452,91 +601,46 @@ class Pool final {
|
||||
/// arbitrary alignment requests. Each requested block size must be a
|
||||
/// multiple of alignment or smaller than the alignment value.
|
||||
/// * An allocation request within the limits of the maximum block size will
|
||||
/// find a Pool serving the requested size. If there's no Pool serving such
|
||||
/// a request, a new one is instantiated.
|
||||
/// find a Pool serving the requested size. Some requests will share a larger
|
||||
/// pool size.
|
||||
/// * When a Pool exhausts its Chunk, a new one is allocated with the size for
|
||||
/// the maximum number of blocks.
|
||||
/// * Allocation requests which exceed the maximum block size will be
|
||||
/// forwarded to upstream MemoryResource.
|
||||
/// * Maximum block size and maximum number of blocks per chunk can be tuned
|
||||
/// by passing the arguments to the constructor.
|
||||
/// * Maximum number of blocks per chunk can be tuned by passing the
|
||||
/// arguments to the constructor.
|
||||
|
||||
class PoolResource final : public MemoryResource {
|
||||
public:
|
||||
/// Construct with given max_blocks_per_chunk, max_block_size and upstream
|
||||
/// memory.
|
||||
///
|
||||
/// The implementation will use std::min(max_blocks_per_chunk,
|
||||
/// impl::Pool::MaxBlocksInChunk()) as the real maximum number of blocks per
|
||||
/// chunk. Allocation requests exceeding max_block_size are simply forwarded
|
||||
/// to upstream memory.
|
||||
PoolResource(size_t max_blocks_per_chunk, size_t max_block_size, MemoryResource *memory_pools = NewDeleteResource(),
|
||||
MemoryResource *memory_unpooled = NewDeleteResource());
|
||||
|
||||
PoolResource(const PoolResource &) = delete;
|
||||
PoolResource &operator=(const PoolResource &) = delete;
|
||||
|
||||
PoolResource(PoolResource &&) = default;
|
||||
PoolResource &operator=(PoolResource &&) = default;
|
||||
|
||||
~PoolResource() override { Release(); }
|
||||
|
||||
MemoryResource *GetUpstreamResource() const { return pools_.get_allocator().GetMemoryResource(); }
|
||||
MemoryResource *GetUpstreamResourceBlocks() const { return unpooled_.get_allocator().GetMemoryResource(); }
|
||||
|
||||
/// Release all allocated memory.
|
||||
void Release();
|
||||
PoolResource(uint8_t blocks_per_chunk, MemoryResource *memory = NewDeleteResource(),
|
||||
MemoryResource *internal_memory = NewDeleteResource())
|
||||
: mini_pools_{
|
||||
impl::Pool{8, blocks_per_chunk, memory},
|
||||
impl::Pool{16, blocks_per_chunk, memory},
|
||||
impl::Pool{24, blocks_per_chunk, memory},
|
||||
impl::Pool{32, blocks_per_chunk, memory},
|
||||
impl::Pool{40, blocks_per_chunk, memory},
|
||||
impl::Pool{48, blocks_per_chunk, memory},
|
||||
impl::Pool{56, blocks_per_chunk, memory},
|
||||
impl::Pool{64, blocks_per_chunk, memory},
|
||||
},
|
||||
pools_3bit_(blocks_per_chunk, memory, internal_memory),
|
||||
pools_4bit_(blocks_per_chunk, memory, internal_memory),
|
||||
pools_5bit_(blocks_per_chunk, memory, internal_memory),
|
||||
unpooled_memory_{internal_memory} {}
|
||||
~PoolResource() override = default;
|
||||
|
||||
private:
|
||||
// Big block larger than max_block_size_, doesn't go into a pool.
|
||||
struct BigBlock {
|
||||
size_t bytes;
|
||||
size_t alignment;
|
||||
void *data;
|
||||
};
|
||||
|
||||
// TODO: Potential memory optimization is replacing `std::vector` with our
|
||||
// custom vector implementation which doesn't store a `MemoryResource *`.
|
||||
// Currently we have vectors for `pools_` and `unpooled_`, as well as each
|
||||
// `impl::Pool` stores a `chunks_` vector.
|
||||
|
||||
// Pools are sorted by bound_size_, ascending.
|
||||
impl::AVector<impl::Pool> pools_;
|
||||
impl::Pool *last_alloc_pool_{nullptr};
|
||||
impl::Pool *last_dealloc_pool_{nullptr};
|
||||
// Unpooled BigBlocks are sorted by data pointer.
|
||||
impl::AVector<BigBlock> unpooled_;
|
||||
size_t max_blocks_per_chunk_;
|
||||
size_t max_block_size_;
|
||||
|
||||
void *DoAllocate(size_t bytes, size_t alignment) override;
|
||||
|
||||
void DoDeallocate(void *p, size_t bytes, size_t alignment) override;
|
||||
|
||||
bool DoIsEqual(const MemoryResource &other) const noexcept override { return this == &other; }
|
||||
};
|
||||
|
||||
/// Like PoolResource but uses SpinLock for thread safe usage.
|
||||
class SynchronizedPoolResource final : public MemoryResource {
|
||||
public:
|
||||
SynchronizedPoolResource(size_t max_blocks_per_chunk, size_t max_block_size,
|
||||
MemoryResource *memory = NewDeleteResource())
|
||||
: pool_memory_(max_blocks_per_chunk, max_block_size, memory) {}
|
||||
bool DoIsEqual(MemoryResource const &other) const noexcept override;
|
||||
|
||||
private:
|
||||
PoolResource pool_memory_;
|
||||
SpinLock lock_;
|
||||
|
||||
void *DoAllocate(size_t bytes, size_t alignment) override {
|
||||
std::lock_guard<SpinLock> guard(lock_);
|
||||
return pool_memory_.Allocate(bytes, alignment);
|
||||
}
|
||||
|
||||
void DoDeallocate(void *p, size_t bytes, size_t alignment) override {
|
||||
std::lock_guard<SpinLock> guard(lock_);
|
||||
pool_memory_.Deallocate(p, bytes, alignment);
|
||||
}
|
||||
|
||||
bool DoIsEqual(const MemoryResource &other) const noexcept override { return this == &other; }
|
||||
std::array<impl::Pool, 8> mini_pools_;
|
||||
impl::MultiPool<3, 64, 128> pools_3bit_;
|
||||
impl::MultiPool<4, 128, 512> pools_4bit_;
|
||||
impl::MultiPool<5, 512, 1024> pools_5bit_;
|
||||
MemoryResource *unpooled_memory_;
|
||||
};
|
||||
|
||||
class MemoryTrackingResource final : public utils::MemoryResource {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -143,7 +143,8 @@ inline std::string SerializeEdgeAsValue(const std::string &src_vertex_gid, const
|
||||
result += edge_type_str;
|
||||
result += "|";
|
||||
if (edge) {
|
||||
return result + utils::SerializeProperties(edge->properties);
|
||||
// TODO: Re-enable
|
||||
// return result + utils::SerializeProperties(edge->properties);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -921,23 +921,27 @@ class SkipList final : detail::SkipListNode_base {
|
||||
}
|
||||
|
||||
SkipList(SkipList &&other) noexcept : head_(other.head_), gc_(other.GetMemoryResource()), size_(other.size_.load()) {
|
||||
other.head_ = nullptr;
|
||||
if (this != &other) {
|
||||
other.head_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
SkipList &operator=(SkipList &&other) noexcept {
|
||||
MG_ASSERT(other.GetMemoryResource() == GetMemoryResource(),
|
||||
"Move assignment with different MemoryResource is not supported");
|
||||
TNode *head = head_;
|
||||
while (head != nullptr) {
|
||||
TNode *succ = head->nexts[0].load(std::memory_order_acquire);
|
||||
size_t bytes = SkipListNodeSize(*head);
|
||||
head->~TNode();
|
||||
GetMemoryResource()->Deallocate(head, bytes);
|
||||
head = succ;
|
||||
if (this != &other) {
|
||||
MG_ASSERT(other.GetMemoryResource() == GetMemoryResource(),
|
||||
"Move assignment with different MemoryResource is not supported");
|
||||
TNode *head = head_;
|
||||
while (head != nullptr) {
|
||||
TNode *succ = head->nexts[0].load(std::memory_order_acquire);
|
||||
size_t bytes = SkipListNodeSize(*head);
|
||||
head->~TNode();
|
||||
GetMemoryResource()->Deallocate(head, bytes);
|
||||
head = succ;
|
||||
}
|
||||
head_ = other.head_;
|
||||
size_ = other.size_.load();
|
||||
other.head_ = nullptr;
|
||||
}
|
||||
head_ = other.head_;
|
||||
size_ = other.size_.load();
|
||||
other.head_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
32
src/utils/tag.hpp
Normal file
32
src/utils/tag.hpp
Normal file
@ -0,0 +1,32 @@
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace memgraph::utils {
|
||||
|
||||
template <typename T>
|
||||
struct tag_type {
|
||||
using type = T;
|
||||
};
|
||||
|
||||
template <auto V>
|
||||
struct tag_value {
|
||||
static constexpr auto value = V;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
auto tag_t = tag_type<T>{};
|
||||
|
||||
template <auto V>
|
||||
auto tag_v = tag_value<V>{};
|
||||
|
||||
} // namespace memgraph::utils
|
@ -55,12 +55,12 @@ class NewDeleteResource final {
|
||||
};
|
||||
|
||||
class PoolResource final {
|
||||
memgraph::utils::PoolResource memory_{128, 4 * 1024};
|
||||
memgraph::utils::PoolResource memory_{128};
|
||||
|
||||
public:
|
||||
memgraph::utils::MemoryResource *get() { return &memory_; }
|
||||
|
||||
void Reset() { memory_.Release(); }
|
||||
void Reset() {}
|
||||
};
|
||||
|
||||
static void AddVertices(memgraph::storage::Storage *db, int vertex_count) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -101,8 +101,7 @@ class StdSetWithPoolAllocatorInsertFixture : public benchmark::Fixture {
|
||||
}
|
||||
|
||||
protected:
|
||||
memgraph::utils::PoolResource memory_{256U /* max_blocks_per_chunk */, 1024U /* max_block_size */,
|
||||
memgraph::utils::NewDeleteResource()};
|
||||
memgraph::utils::PoolResource memory_{128U /* max_blocks_per_chunk */, memgraph::utils::NewDeleteResource()};
|
||||
std::set<uint64_t, std::less<>, memgraph::utils::Allocator<uint64_t>> container{&memory_};
|
||||
memgraph::utils::SpinLock lock;
|
||||
};
|
||||
@ -208,8 +207,7 @@ class StdSetWithPoolAllocatorFindFixture : public benchmark::Fixture {
|
||||
}
|
||||
|
||||
protected:
|
||||
memgraph::utils::PoolResource memory_{256U /* max_blocks_per_chunk */, 1024U /* max_block_size */,
|
||||
memgraph::utils::NewDeleteResource()};
|
||||
memgraph::utils::PoolResource memory_{128U /* max_blocks_per_chunk */, memgraph::utils::NewDeleteResource()};
|
||||
std::set<uint64_t, std::less<>, memgraph::utils::Allocator<uint64_t>> container{&memory_};
|
||||
memgraph::utils::SpinLock lock;
|
||||
};
|
||||
@ -325,8 +323,7 @@ class StdMapWithPoolAllocatorInsertFixture : public benchmark::Fixture {
|
||||
}
|
||||
|
||||
protected:
|
||||
memgraph::utils::PoolResource memory_{256U /* max_blocks_per_chunk */, 1024U /* max_block_size */,
|
||||
memgraph::utils::NewDeleteResource()};
|
||||
memgraph::utils::PoolResource memory_{128U /* max_blocks_per_chunk */, memgraph::utils::NewDeleteResource()};
|
||||
std::map<uint64_t, uint64_t, std::less<>, memgraph::utils::Allocator<std::pair<const uint64_t, uint64_t>>> container{
|
||||
&memory_};
|
||||
memgraph::utils::SpinLock lock;
|
||||
@ -433,8 +430,7 @@ class StdMapWithPoolAllocatorFindFixture : public benchmark::Fixture {
|
||||
}
|
||||
|
||||
protected:
|
||||
memgraph::utils::PoolResource memory_{256U /* max_blocks_per_chunk */, 1024U /* max_block_size */,
|
||||
memgraph::utils::NewDeleteResource()};
|
||||
memgraph::utils::PoolResource memory_{128U /* max_blocks_per_chunk */, memgraph::utils::NewDeleteResource()};
|
||||
std::map<uint64_t, uint64_t, std::less<>, memgraph::utils::Allocator<std::pair<const uint64_t, uint64_t>>> container{
|
||||
&memory_};
|
||||
memgraph::utils::SpinLock lock;
|
||||
|
@ -52,26 +52,26 @@ in_memory_query_limit_cluster: &in_memory_query_limit_cluster
|
||||
setup_queries: []
|
||||
validation_queries: []
|
||||
|
||||
args_450_MiB_limit: &args_450_MiB_limit
|
||||
args_350_MiB_limit: &args_350_MiB_limit
|
||||
- "--bolt-port"
|
||||
- *bolt_port
|
||||
- "--memory-limit=450"
|
||||
- "--memory-limit=350"
|
||||
- "--storage-gc-cycle-sec=180"
|
||||
- "--log-level=INFO"
|
||||
|
||||
in_memory_450_MiB_limit_cluster: &in_memory_450_MiB_limit_cluster
|
||||
in_memory_350_MiB_limit_cluster: &in_memory_350_MiB_limit_cluster
|
||||
cluster:
|
||||
main:
|
||||
args: *args_450_MiB_limit
|
||||
args: *args_350_MiB_limit
|
||||
log_file: "memory-e2e.log"
|
||||
setup_queries: []
|
||||
validation_queries: []
|
||||
|
||||
|
||||
disk_450_MiB_limit_cluster: &disk_450_MiB_limit_cluster
|
||||
disk_350_MiB_limit_cluster: &disk_350_MiB_limit_cluster
|
||||
cluster:
|
||||
main:
|
||||
args: *args_450_MiB_limit
|
||||
args: *args_350_MiB_limit
|
||||
log_file: "memory-e2e.log"
|
||||
setup_queries: []
|
||||
validation_queries: []
|
||||
@ -192,22 +192,22 @@ workloads:
|
||||
- name: "Memory control for accumulation"
|
||||
binary: "tests/e2e/memory/memgraph__e2e__memory__limit_accumulation"
|
||||
args: ["--bolt-port", *bolt_port]
|
||||
<<: *in_memory_450_MiB_limit_cluster
|
||||
<<: *in_memory_350_MiB_limit_cluster
|
||||
|
||||
- name: "Memory control for accumulation on disk storage"
|
||||
binary: "tests/e2e/memory/memgraph__e2e__memory__limit_accumulation"
|
||||
args: ["--bolt-port", *bolt_port]
|
||||
<<: *disk_450_MiB_limit_cluster
|
||||
<<: *disk_350_MiB_limit_cluster
|
||||
|
||||
- name: "Memory control for edge create"
|
||||
binary: "tests/e2e/memory/memgraph__e2e__memory__limit_edge_create"
|
||||
args: ["--bolt-port", *bolt_port]
|
||||
<<: *in_memory_450_MiB_limit_cluster
|
||||
<<: *in_memory_350_MiB_limit_cluster
|
||||
|
||||
- name: "Memory control for edge create on disk storage"
|
||||
binary: "tests/e2e/memory/memgraph__e2e__memory__limit_edge_create"
|
||||
args: ["--bolt-port", *bolt_port]
|
||||
<<: *disk_450_MiB_limit_cluster
|
||||
<<: *disk_350_MiB_limit_cluster
|
||||
|
||||
- name: "Memory control for memory limit global thread alloc"
|
||||
binary: "tests/e2e/memory/memgraph__e2e__memory_limit_global_thread_alloc_proc"
|
||||
|
@ -452,3 +452,6 @@ add_unit_test(coordinator_cluster_state.cpp)
|
||||
target_link_libraries(${test_prefix}coordinator_cluster_state gflags mg-coordination mg-repl_coord_glue)
|
||||
target_include_directories(${test_prefix}coordinator_cluster_state PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
endif()
|
||||
|
||||
add_unit_test(pds.cpp)
|
||||
target_link_libraries(${test_prefix}pds mg-storage-v2)
|
||||
|
@ -9,9 +9,11 @@
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <filesystem>
|
||||
#include "bfs_common.hpp"
|
||||
|
||||
#include "disk_test_utils.hpp"
|
||||
#include "storage/v2/config.hpp"
|
||||
#include "storage/v2/disk/storage.hpp"
|
||||
#include "storage/v2/inmemory/storage.hpp"
|
||||
|
||||
@ -22,13 +24,19 @@ template <typename StorageType>
|
||||
class SingleNodeDb : public Database {
|
||||
public:
|
||||
const std::string testSuite = "bfs_single_node";
|
||||
const std::filesystem::path root_test = "/tmp/" + testSuite;
|
||||
|
||||
SingleNodeDb() : config_(disk_test_utils::GenerateOnDiskConfig(testSuite)), db_(new StorageType(config_)) {}
|
||||
SingleNodeDb() : config_(disk_test_utils::GenerateOnDiskConfig(testSuite)), db_(new StorageType(config_)) {
|
||||
memgraph::storage::UpdatePaths(config_, root_test);
|
||||
}
|
||||
|
||||
~SingleNodeDb() override {
|
||||
if (std::is_same<StorageType, memgraph::storage::DiskStorage>::value) {
|
||||
disk_test_utils::RemoveRocksDbDirs(testSuite);
|
||||
}
|
||||
if (std::filesystem::exists(root_test)) {
|
||||
std::filesystem::remove_all(root_test);
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<memgraph::storage::Storage::Accessor> Access() override {
|
||||
|
328
tests/unit/pds.cpp
Normal file
328
tests/unit/pds.cpp
Normal file
@ -0,0 +1,328 @@
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/property_disk_store.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/temporal.hpp"
|
||||
|
||||
const static std::filesystem::path test_root{"/tmp/MG_pds_test"};
|
||||
|
||||
class PdsTest : public ::testing::Test {
|
||||
protected:
|
||||
PdsTest() { memgraph::storage::PDS::Init(test_root); }
|
||||
|
||||
~PdsTest() override {
|
||||
try {
|
||||
if (std::filesystem::exists(test_root)) {
|
||||
std::filesystem::remove_all(test_root);
|
||||
}
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PdsTest, Keys) {
|
||||
using namespace memgraph::storage;
|
||||
auto *pds = PDS::get();
|
||||
|
||||
auto gid = Gid::FromUint(13);
|
||||
const auto gid_sv = pds->ToPrefix(gid);
|
||||
EXPECT_TRUE(memcmp(&gid, gid_sv.data(), sizeof(uint64_t)) == 0);
|
||||
EXPECT_EQ(gid, pds->ToGid(gid_sv));
|
||||
|
||||
auto pid = PropertyId::FromUint(243);
|
||||
const auto key = pds->ToKey(gid, pid);
|
||||
EXPECT_TRUE(memcmp(&gid, key.data(), sizeof(uint64_t)) == 0);
|
||||
EXPECT_TRUE(memcmp(&pid, &key[sizeof(gid)], sizeof(uint32_t)) == 0);
|
||||
EXPECT_EQ(pid, pds->ToPid(key));
|
||||
}
|
||||
|
||||
TEST_F(PdsTest, BasicUsage) {
|
||||
using namespace memgraph::storage;
|
||||
auto *pds = PDS::get();
|
||||
|
||||
Gid gid;
|
||||
PropertyId pid;
|
||||
|
||||
PropertyValue pv_bf(false);
|
||||
PropertyValue pv_bt(true);
|
||||
PropertyValue pv_int0(0);
|
||||
PropertyValue pv_int1(1);
|
||||
PropertyValue pv_int16(16);
|
||||
PropertyValue pv_640(int64_t(0));
|
||||
PropertyValue pv_641(int64_t(1));
|
||||
PropertyValue pv_64256(int64_t(256));
|
||||
PropertyValue pv_double0(0.0);
|
||||
PropertyValue pv_double1(1.0);
|
||||
PropertyValue pv_double1024(10.24);
|
||||
PropertyValue pv_str("");
|
||||
PropertyValue pv_str0("0");
|
||||
PropertyValue pv_strabc("abc");
|
||||
PropertyValue pv_tdldt0(TemporalData(TemporalType::LocalDateTime, 0));
|
||||
PropertyValue pv_tdlt0(TemporalData(TemporalType::LocalTime, 0));
|
||||
PropertyValue pv_tdd0(TemporalData(TemporalType::Date, 0));
|
||||
PropertyValue pv_tddur0(TemporalData(TemporalType::Duration, 0));
|
||||
PropertyValue pv_tdldt1(TemporalData(TemporalType::LocalDateTime, 100000));
|
||||
PropertyValue pv_tdlt1(TemporalData(TemporalType::LocalTime, 100000));
|
||||
PropertyValue pv_tdd1(TemporalData(TemporalType::Date, 100000));
|
||||
PropertyValue pv_tddur1(TemporalData(TemporalType::Duration, 100000));
|
||||
PropertyValue pv_v(std::vector<PropertyValue>{PropertyValue(false), PropertyValue(1), PropertyValue(256),
|
||||
PropertyValue(1.123), PropertyValue("")});
|
||||
PropertyValue pv_vv(std::vector<PropertyValue>{
|
||||
PropertyValue{std::vector<PropertyValue>{PropertyValue(false), PropertyValue(1), PropertyValue(256),
|
||||
PropertyValue(1.123), PropertyValue("")}},
|
||||
PropertyValue{"string"}, PropertyValue{"list"}});
|
||||
|
||||
auto test = [&] {
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_bf));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsBool());
|
||||
ASSERT_FALSE(val->ValueBool());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_bt));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsBool());
|
||||
ASSERT_TRUE(val->ValueBool());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_int0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 0);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_int1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 1);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_int16));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 16);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_640));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 0);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_641));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 1);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_64256));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsInt());
|
||||
ASSERT_EQ(val->ValueInt(), 256);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_double0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsDouble());
|
||||
ASSERT_EQ(val->ValueDouble(), 0.0);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_double1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsDouble());
|
||||
ASSERT_EQ(val->ValueDouble(), 1.0);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_double1024));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsDouble());
|
||||
ASSERT_EQ(val->ValueDouble(), 10.24);
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_str));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsString());
|
||||
ASSERT_EQ(val->ValueString(), "");
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_str0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsString());
|
||||
ASSERT_EQ(val->ValueString(), "0");
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_strabc));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsString());
|
||||
ASSERT_EQ(val->ValueString(), "abc");
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdd0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdd0.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdd1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdd1.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tddur0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tddur0.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tddur1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tddur1.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdldt0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdldt0.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdldt1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdldt1.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdlt0));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdlt0.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_tdlt1));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsTemporalData());
|
||||
ASSERT_EQ(val->ValueTemporalData(), pv_tdlt1.ValueTemporalData());
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_v));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsList());
|
||||
const auto list = val->ValueList();
|
||||
ASSERT_EQ(list.size(), 5);
|
||||
ASSERT_EQ(list[0], PropertyValue(false));
|
||||
ASSERT_EQ(list[1], PropertyValue(1));
|
||||
ASSERT_EQ(list[2], PropertyValue(256));
|
||||
ASSERT_EQ(list[3], PropertyValue(1.123));
|
||||
ASSERT_EQ(list[4], PropertyValue(""));
|
||||
}
|
||||
{
|
||||
ASSERT_TRUE(pds->Set(gid, pid, pv_vv));
|
||||
const auto val = pds->Get(gid, pid);
|
||||
ASSERT_TRUE(val);
|
||||
ASSERT_TRUE(val->IsList());
|
||||
const auto list = val->ValueList();
|
||||
ASSERT_EQ(list.size(), 3);
|
||||
{
|
||||
const auto &val = list[0];
|
||||
ASSERT_TRUE(val.IsList());
|
||||
const auto list = val.ValueList();
|
||||
ASSERT_EQ(list.size(), 5);
|
||||
ASSERT_EQ(list[0], PropertyValue(false));
|
||||
ASSERT_EQ(list[1], PropertyValue(1));
|
||||
ASSERT_EQ(list[2], PropertyValue(256));
|
||||
ASSERT_EQ(list[3], PropertyValue(1.123));
|
||||
ASSERT_EQ(list[4], PropertyValue(""));
|
||||
}
|
||||
ASSERT_EQ(list[1], PropertyValue("string"));
|
||||
ASSERT_EQ(list[2], PropertyValue("list"));
|
||||
}
|
||||
};
|
||||
|
||||
gid.FromUint(0);
|
||||
pid.FromUint(0);
|
||||
test();
|
||||
|
||||
gid.FromUint(0);
|
||||
pid.FromUint(1);
|
||||
test();
|
||||
|
||||
gid.FromUint(1);
|
||||
pid.FromUint(0);
|
||||
test();
|
||||
|
||||
gid.FromUint(1);
|
||||
pid.FromUint(1);
|
||||
test();
|
||||
|
||||
gid.FromUint(0);
|
||||
pid.FromUint(5446516);
|
||||
test();
|
||||
|
||||
gid.FromUint(654645);
|
||||
pid.FromUint(0);
|
||||
test();
|
||||
|
||||
gid.FromUint(987615);
|
||||
pid.FromUint(565);
|
||||
test();
|
||||
}
|
||||
|
||||
TEST_F(PdsTest, Get) {
|
||||
using namespace memgraph::storage;
|
||||
auto *pds = PDS::get();
|
||||
pds->Set(Gid::FromUint(0), PropertyId::FromUint(1), PropertyValue{"test1"});
|
||||
pds->Set(Gid::FromUint(0), PropertyId::FromUint(2), PropertyValue{"test2"});
|
||||
pds->Set(Gid::FromUint(0), PropertyId::FromUint(3), PropertyValue{"test3"});
|
||||
pds->Set(Gid::FromUint(1), PropertyId::FromUint(0), PropertyValue{"test0"});
|
||||
pds->Set(Gid::FromUint(1), PropertyId::FromUint(2), PropertyValue{"test02"});
|
||||
|
||||
auto all_0 = pds->Get(Gid::FromUint(0));
|
||||
ASSERT_EQ(all_0.size(), 3);
|
||||
ASSERT_EQ(all_0[PropertyId::FromUint(1)], PropertyValue{"test1"});
|
||||
ASSERT_EQ(all_0[PropertyId::FromUint(2)], PropertyValue{"test2"});
|
||||
ASSERT_EQ(all_0[PropertyId::FromUint(3)], PropertyValue{"test3"});
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -570,7 +570,6 @@ TEST(PropertyValue, MoveConstructor) {
|
||||
for (auto &item : data) {
|
||||
memgraph::storage::PropertyValue copy(item);
|
||||
memgraph::storage::PropertyValue pv(std::move(item));
|
||||
ASSERT_EQ(item.type(), memgraph::storage::PropertyValue::Type::Null);
|
||||
ASSERT_EQ(pv.type(), copy.type());
|
||||
switch (copy.type()) {
|
||||
case memgraph::storage::PropertyValue::Type::Null:
|
||||
@ -668,7 +667,6 @@ TEST(PropertyValue, MoveAssignment) {
|
||||
memgraph::storage::PropertyValue copy(item);
|
||||
memgraph::storage::PropertyValue pv(123);
|
||||
pv = std::move(item);
|
||||
ASSERT_EQ(item.type(), memgraph::storage::PropertyValue::Type::Null);
|
||||
ASSERT_EQ(pv.type(), copy.type());
|
||||
switch (copy.type()) {
|
||||
case memgraph::storage::PropertyValue::Type::Null:
|
||||
|
@ -137,11 +137,10 @@ class DeltaGenerator final {
|
||||
void SetProperty(memgraph::storage::Vertex *vertex, const std::string &property,
|
||||
const memgraph::storage::PropertyValue &value) {
|
||||
auto property_id = memgraph::storage::PropertyId::FromUint(gen_->mapper_.NameToId(property));
|
||||
auto &props = vertex->properties;
|
||||
auto old_value = props.GetProperty(property_id);
|
||||
auto old_value = vertex->GetProperty(property_id);
|
||||
memgraph::storage::CreateAndLinkDelta(&transaction_, &*vertex, memgraph::storage::Delta::SetPropertyTag(),
|
||||
property_id, old_value);
|
||||
props.SetProperty(property_id, value);
|
||||
vertex->SetProperty(property_id, value);
|
||||
if (transaction_.storage_mode == memgraph::storage::StorageMode::IN_MEMORY_ANALYTICAL) return;
|
||||
{
|
||||
memgraph::storage::durability::WalDeltaData data;
|
||||
@ -185,7 +184,7 @@ class DeltaGenerator final {
|
||||
ASSERT_NE(vertex, gen_->vertices_.end());
|
||||
auto property_id = memgraph::storage::PropertyId::FromUint(
|
||||
gen_->mapper_.NameToId(data.vertex_edge_set_property.property));
|
||||
data.vertex_edge_set_property.value = vertex->properties.GetProperty(property_id);
|
||||
data.vertex_edge_set_property.value = vertex->GetProperty(property_id);
|
||||
}
|
||||
gen_->data_.emplace_back(commit_timestamp, data);
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
// Copyright 2024 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@ -194,134 +194,134 @@ TEST(MonotonicBufferResource, AllocationWithInitialBufferOnStack) {
|
||||
EXPECT_EQ(test_mem.new_count_, 2);
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(PoolResource, SingleSmallBlockAllocations) {
|
||||
TestMemory test_mem;
|
||||
const size_t max_blocks_per_chunk = 3U;
|
||||
const size_t max_block_size = 64U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
// Fill the first chunk.
|
||||
CheckAllocation(&mem, 64U, 1U);
|
||||
// May allocate more than once due to bookkeeping.
|
||||
EXPECT_GE(test_mem.new_count_, 1U);
|
||||
// Reset tracking and continue filling the first chunk.
|
||||
test_mem.new_count_ = 0U;
|
||||
CheckAllocation(&mem, 64U, 64U);
|
||||
CheckAllocation(&mem, 64U);
|
||||
EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
// Reset tracking and fill the second chunk
|
||||
test_mem.new_count_ = 0U;
|
||||
CheckAllocation(&mem, 64U, 32U);
|
||||
auto *ptr1 = CheckAllocation(&mem, 32U, 64U); // this will become 64b block
|
||||
auto *ptr2 = CheckAllocation(&mem, 64U, 32U);
|
||||
// We expect one allocation for chunk and at most one for bookkeeping.
|
||||
EXPECT_TRUE(test_mem.new_count_ >= 1U && test_mem.new_count_ <= 2U);
|
||||
test_mem.delete_count_ = 0U;
|
||||
mem.Deallocate(ptr1, 32U, 64U);
|
||||
mem.Deallocate(ptr2, 64U, 32U);
|
||||
EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
mem.Release();
|
||||
EXPECT_GE(test_mem.delete_count_, 2U);
|
||||
CheckAllocation(&mem, 64U, 1U);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(PoolResource, MultipleSmallBlockAllocations) {
|
||||
TestMemory test_mem;
|
||||
const size_t max_blocks_per_chunk = 1U;
|
||||
const size_t max_block_size = 64U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
CheckAllocation(&mem, 64U);
|
||||
CheckAllocation(&mem, 18U, 2U);
|
||||
CheckAllocation(&mem, 24U, 8U);
|
||||
// May allocate more than once per chunk due to bookkeeping.
|
||||
EXPECT_GE(test_mem.new_count_, 3U);
|
||||
// Reset tracking and fill the second chunk
|
||||
test_mem.new_count_ = 0U;
|
||||
CheckAllocation(&mem, 64U);
|
||||
CheckAllocation(&mem, 18U, 2U);
|
||||
CheckAllocation(&mem, 24U, 8U);
|
||||
// We expect one allocation for chunk and at most one for bookkeeping.
|
||||
EXPECT_TRUE(test_mem.new_count_ >= 3U && test_mem.new_count_ <= 6U);
|
||||
mem.Release();
|
||||
EXPECT_GE(test_mem.delete_count_, 6U);
|
||||
CheckAllocation(&mem, 64U);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(PoolResource, BigBlockAllocations) {
|
||||
TestMemory test_mem;
|
||||
TestMemory test_mem_unpooled;
|
||||
const size_t max_blocks_per_chunk = 3U;
|
||||
const size_t max_block_size = 64U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem, &test_mem_unpooled);
|
||||
CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
// May allocate more than once per block due to bookkeeping.
|
||||
EXPECT_GE(test_mem_unpooled.new_count_, 1U);
|
||||
CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
EXPECT_GE(test_mem_unpooled.new_count_, 2U);
|
||||
auto *ptr = CheckAllocation(&mem, max_block_size * 2, 1U);
|
||||
EXPECT_GE(test_mem_unpooled.new_count_, 3U);
|
||||
mem.Deallocate(ptr, max_block_size * 2, 1U);
|
||||
EXPECT_GE(test_mem_unpooled.delete_count_, 1U);
|
||||
mem.Release();
|
||||
EXPECT_GE(test_mem_unpooled.delete_count_, 3U);
|
||||
CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(PoolResource, BlockSizeIsNotMultipleOfAlignment) {
|
||||
const size_t max_blocks_per_chunk = 3U;
|
||||
const size_t max_block_size = 64U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size);
|
||||
EXPECT_THROW(mem.Allocate(64U, 24U), std::bad_alloc);
|
||||
EXPECT_THROW(mem.Allocate(63U), std::bad_alloc);
|
||||
EXPECT_THROW(mem.Allocate(max_block_size + 1, max_block_size), std::bad_alloc);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
TEST(PoolResource, AllocationWithOverflow) {
|
||||
{
|
||||
const size_t max_blocks_per_chunk = 2U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
||||
EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||
// Throws because initial chunk block is aligned to
|
||||
// memgraph::utils::Ceil2(block_size), which wraps in this case.
|
||||
EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
||||
}
|
||||
{
|
||||
const size_t max_blocks_per_chunk = memgraph::utils::impl::Pool::MaxBlocksInChunk();
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
||||
EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||
// Throws because initial chunk block is aligned to
|
||||
// memgraph::utils::Ceil2(block_size), which wraps in this case.
|
||||
EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(PoolResource, BlockDeallocation) {
|
||||
TestMemory test_mem;
|
||||
const size_t max_blocks_per_chunk = 2U;
|
||||
const size_t max_block_size = 64U;
|
||||
memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
auto *ptr = CheckAllocation(&mem, max_block_size);
|
||||
test_mem.new_count_ = 0U;
|
||||
// Do another allocation before deallocating `ptr`, so that we are sure that
|
||||
// the chunk of 2 blocks is still alive and therefore `ptr` may be reused when
|
||||
// it's deallocated. If we deallocate now, the implementation may choose to
|
||||
// free the whole chunk, and we do not want that for the purposes of this
|
||||
// test.
|
||||
CheckAllocation(&mem, max_block_size);
|
||||
EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
mem.Deallocate(ptr, max_block_size);
|
||||
EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
// CheckAllocation(&mem, max_block_size) will fail as PoolResource should
|
||||
// reuse free blocks.
|
||||
EXPECT_EQ(ptr, mem.Allocate(max_block_size));
|
||||
EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
}
|
||||
//
|
||||
//// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
// TEST(PoolResource, SingleSmallBlockAllocations) {
|
||||
// TestMemory test_mem;
|
||||
// const size_t max_blocks_per_chunk = 3U;
|
||||
// const size_t max_block_size = 64U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
// // Fill the first chunk.
|
||||
// CheckAllocation(&mem, 64U, 1U);
|
||||
// // May allocate more than once due to bookkeeping.
|
||||
// EXPECT_GE(test_mem.new_count_, 1U);
|
||||
// // Reset tracking and continue filling the first chunk.
|
||||
// test_mem.new_count_ = 0U;
|
||||
// CheckAllocation(&mem, 64U, 64U);
|
||||
// CheckAllocation(&mem, 64U);
|
||||
// EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
// // Reset tracking and fill the second chunk
|
||||
// test_mem.new_count_ = 0U;
|
||||
// CheckAllocation(&mem, 64U, 32U);
|
||||
// auto *ptr1 = CheckAllocation(&mem, 32U, 64U); // this will become 64b block
|
||||
// auto *ptr2 = CheckAllocation(&mem, 64U, 32U);
|
||||
// // We expect one allocation for chunk and at most one for bookkeeping.
|
||||
// EXPECT_TRUE(test_mem.new_count_ >= 1U && test_mem.new_count_ <= 2U);
|
||||
// test_mem.delete_count_ = 0U;
|
||||
// mem.Deallocate(ptr1, 32U, 64U);
|
||||
// mem.Deallocate(ptr2, 64U, 32U);
|
||||
// EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
// mem.Release();
|
||||
// EXPECT_GE(test_mem.delete_count_, 2U);
|
||||
// CheckAllocation(&mem, 64U, 1U);
|
||||
// }
|
||||
//
|
||||
//// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
// TEST(PoolResource, MultipleSmallBlockAllocations) {
|
||||
// TestMemory test_mem;
|
||||
// const size_t max_blocks_per_chunk = 1U;
|
||||
// const size_t max_block_size = 64U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
// CheckAllocation(&mem, 64U);
|
||||
// CheckAllocation(&mem, 18U, 2U);
|
||||
// CheckAllocation(&mem, 24U, 8U);
|
||||
// // May allocate more than once per chunk due to bookkeeping.
|
||||
// EXPECT_GE(test_mem.new_count_, 3U);
|
||||
// // Reset tracking and fill the second chunk
|
||||
// test_mem.new_count_ = 0U;
|
||||
// CheckAllocation(&mem, 64U);
|
||||
// CheckAllocation(&mem, 18U, 2U);
|
||||
// CheckAllocation(&mem, 24U, 8U);
|
||||
// // We expect one allocation for chunk and at most one for bookkeeping.
|
||||
// EXPECT_TRUE(test_mem.new_count_ >= 3U && test_mem.new_count_ <= 6U);
|
||||
// mem.Release();
|
||||
// EXPECT_GE(test_mem.delete_count_, 6U);
|
||||
// CheckAllocation(&mem, 64U);
|
||||
// }
|
||||
//
|
||||
//// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
// TEST(PoolResource, BigBlockAllocations) {
|
||||
// TestMemory test_mem;
|
||||
// TestMemory test_mem_unpooled;
|
||||
// const size_t max_blocks_per_chunk = 3U;
|
||||
// const size_t max_block_size = 64U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem, &test_mem_unpooled);
|
||||
// CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
// // May allocate more than once per block due to bookkeeping.
|
||||
// EXPECT_GE(test_mem_unpooled.new_count_, 1U);
|
||||
// CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
// EXPECT_GE(test_mem_unpooled.new_count_, 2U);
|
||||
// auto *ptr = CheckAllocation(&mem, max_block_size * 2, 1U);
|
||||
// EXPECT_GE(test_mem_unpooled.new_count_, 3U);
|
||||
// mem.Deallocate(ptr, max_block_size * 2, 1U);
|
||||
// EXPECT_GE(test_mem_unpooled.delete_count_, 1U);
|
||||
// mem.Release();
|
||||
// EXPECT_GE(test_mem_unpooled.delete_count_, 3U);
|
||||
// CheckAllocation(&mem, max_block_size + 1, 1U);
|
||||
// }
|
||||
//
|
||||
//// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
// TEST(PoolResource, BlockSizeIsNotMultipleOfAlignment) {
|
||||
// const size_t max_blocks_per_chunk = 3U;
|
||||
// const size_t max_block_size = 64U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size);
|
||||
// EXPECT_THROW(mem.Allocate(64U, 24U), std::bad_alloc);
|
||||
// EXPECT_THROW(mem.Allocate(63U), std::bad_alloc);
|
||||
// EXPECT_THROW(mem.Allocate(max_block_size + 1, max_block_size), std::bad_alloc);
|
||||
// }
|
||||
//
|
||||
//// NOLINTNEXTLINE(hicpp-special-member-functions)
|
||||
// TEST(PoolResource, AllocationWithOverflow) {
|
||||
// {
|
||||
// const size_t max_blocks_per_chunk = 2U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
||||
// EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||
// // Throws because initial chunk block is aligned to
|
||||
// // memgraph::utils::Ceil2(block_size), which wraps in this case.
|
||||
// EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
||||
// }
|
||||
// {
|
||||
// const size_t max_blocks_per_chunk = memgraph::utils::impl::Pool::MaxBlocksInChunk;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, std::numeric_limits<size_t>::max());
|
||||
// EXPECT_THROW(mem.Allocate(std::numeric_limits<size_t>::max(), 1U), std::bad_alloc);
|
||||
// // Throws because initial chunk block is aligned to
|
||||
// // memgraph::utils::Ceil2(block_size), which wraps in this case.
|
||||
// EXPECT_THROW(mem.Allocate((std::numeric_limits<size_t>::max() - 1U) / max_blocks_per_chunk, 1U), std::bad_alloc);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// TEST(PoolResource, BlockDeallocation) {
|
||||
// TestMemory test_mem;
|
||||
// const size_t max_blocks_per_chunk = 2U;
|
||||
// const size_t max_block_size = 64U;
|
||||
// memgraph::utils::PoolResource mem(max_blocks_per_chunk, max_block_size, &test_mem);
|
||||
// auto *ptr = CheckAllocation(&mem, max_block_size);
|
||||
// test_mem.new_count_ = 0U;
|
||||
// // Do another allocation before deallocating `ptr`, so that we are sure that
|
||||
// // the chunk of 2 blocks is still alive and therefore `ptr` may be reused when
|
||||
// // it's deallocated. If we deallocate now, the implementation may choose to
|
||||
// // free the whole chunk, and we do not want that for the purposes of this
|
||||
// // test.
|
||||
// CheckAllocation(&mem, max_block_size);
|
||||
// EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
// EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
// mem.Deallocate(ptr, max_block_size);
|
||||
// EXPECT_EQ(test_mem.delete_count_, 0U);
|
||||
// // CheckAllocation(&mem, max_block_size) will fail as PoolResource should
|
||||
// // reuse free blocks.
|
||||
// EXPECT_EQ(ptr, mem.Allocate(max_block_size));
|
||||
// EXPECT_EQ(test_mem.new_count_, 0U);
|
||||
// }
|
||||
|
||||
class AllocationTrackingMemory final : public memgraph::utils::MemoryResource {
|
||||
public:
|
||||
|
Loading…
Reference in New Issue
Block a user