Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor serialization buffer management and support memory tracking. #5231

Merged
merged 43 commits into from
Sep 6, 2024

Conversation

teo-tsirpanis
Copy link
Member

@teo-tsirpanis teo-tsirpanis commented Aug 8, 2024

SC-50984

This PR adds support for tracking memory used by serialization buffers. The first step towards implementing this is the replacement of the Buffer class with the new SerializationBuffer class, which has the following characteristics:

  • Like Buffer, SerializationBuffer supports both owned and non-owned memory buffers. This is necessary to maintain the semantics of the tiledb_buffer_t C API. SerializationBuffer also allows swapping between owned and non-owned modes.
  • SerializationBuffer does not support the write function which incrementally adds data by growing the buffer. Instead, it supports only the assign operation, which replaces the whole existing data of the buffer. assign copies the given data to a buffer owned by SerializationBuffer, unless a special marker value is passed, which changes it to just store a span to the buffer.
    • There is also the assign_null_terminated method that sets an owned buffer and adds a null terminator at the end. This is used by JSON serialization for some reason (is it actually needed?).
  • SerializationBuffer does not store an offset and does not expose a read function that copies to a user-provided buffer and advances the offset. Instead the class is implicitly convertible to a const span of bytes, which gives access to the entire buffer at once.
  • SerializationBuffers are typically immutable between assignments, but some cases require subsequent modification. To cover these, the owned_mutable_span function returns a mutable span to the buffer, but will throw if used in a non-owned buffer.
  • SerializationBuffer supports being used with polymorphic allocators, which enables integration with the memory tracking system. A new MemoryTracker was added on ContextResources to track serialization-related allocations.

BufferList was also updated to store SerializationBuffers. Creations of serialization buffers and buffer lists were updated to pass an allocator, and C API handles for both types were likewise updated.

I also took up on the opportunity and converted all deserialization APIs that accepted a Buffer to accept a span<const char>, because they did not need any other facilities of SerializationBuffer.


TYPE: NO_HISTORY

Fixes compile errors due to redefinition of the name `span`.
And port `array_serialize` as a proof of concept.
Only `SerializationBuffer` now implicitly converts to span.
This cleans-up code structure and will allow easily flowing the allocator from the list.
`tiledb_buffer_list_t` was also updated to pass the allocator from the context's serialization memory tracker.
tiledb/sm/buffer/buffer.h Outdated Show resolved Hide resolved
Comment on lines +576 to +578
bool is_owned() const {
return buffer_.data() == buffer_owner_.data();
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thought I could make this private. It has no use cases outside of this class. It could be used to guard for calling owned_mutable_span, but you are supposed to call this function on buffers you created yourself, and you know whether they are owned.

And reminder for me to document it.

tiledb/sm/buffer/buffer.h Show resolved Hide resolved
tiledb/sm/rest/rest_client_remote.cc Outdated Show resolved Hide resolved
Comment on lines +642 to +644
if (!is_owned())
throw BufferStatusException(
"Cannot get a mutable span of a non-owned buffer.");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could just remove that or replace it with assert, which would always return an empty span for non-owned buffers.

tiledb/api/c_api/buffer/buffer_api.cc Show resolved Hide resolved
tiledb/sm/buffer/test/unit_serialization_buffer.cc Outdated Show resolved Hide resolved
tiledb/sm/buffer/test/unit_serialization_buffer.cc Outdated Show resolved Hide resolved
}

return num_read;
return buffer_list->read_at_most(dest, max_nbytes);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should catch exceptions here and return CURL_READFUNC_ABORT?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_at_most does not throw. I can further make it noexcept.

tiledb/sm/buffer/buffer.h Show resolved Hide resolved

// Read from buffer
const uint64_t bytes_in_src = src.size() - current_relative_offset_;
const uint64_t bytes_from_src = std::min(bytes_in_src, bytes_left);
// If the destination pointer is not null, then read into it
// if it is null then we are just seeking
if (dest_ptr != nullptr)
RETURN_NOT_OK(src.read(dest_ptr + dest_offset, bytes_from_src));
memcpy(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this get rid of checks that were previously performed by read to make sure we don't read OOB?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. The declaration of bytes_from_src above ensures we don't read OOB.

tiledb/sm/buffer/buffer_list.h Outdated Show resolved Hide resolved
TEST_CASE(
"SerializationBuffer: Test memory tracking",
"[serialization_buffer][memory_tracking]") {
tiledb::sm::MemoryTrackerManager memory_tracker_manager;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we test what happens to copied/moved buffers related to memory tracking?

Copy link
Member Author

@teo-tsirpanis teo-tsirpanis Aug 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote some tests locally and noticed the allocator does not propagate when copying a collection. 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! Something we need to figure out for sure.

tiledb/sm/buffer/buffer.h Outdated Show resolved Hide resolved
tiledb/sm/buffer/buffer.h Show resolved Hide resolved
tiledb/sm/array/array.h Outdated Show resolved Hide resolved
@@ -2738,23 +2729,12 @@ Status query_est_result_size_serialize(
case SerializationType::JSON: {
::capnp::JsonCodec json;
kj::String capnp_json = json.encode(est_result_size_builder);
const auto json_len = capnp_json.size();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we loose the behavior of adding \0 here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I was sure I would miss a case. Fixed, thanks!

SerializationType serialize_type,
bool clientside,
CopyState* copy_state,
Query* query,
ThreadPool* compute_tp) {
// Create an original, serialized copy of the 'query' that we will revert
// to if we are unable to deserialize 'serialized_buffer'.
BufferList original_bufferlist;
BufferList original_bufferlist(
query->resources().serialization_memory_tracker()->get_resource(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very convoluted way to get the memory tracker. We have to find a way to pass it in query_deserialize without getting it from another object.

Copy link
Member Author

@teo-tsirpanis teo-tsirpanis Sep 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getting it from another object

My understanding is that this is how allocators propagate; from "parent" to "child" objects. We backup the state of a query in a temporary buffer list, and for me it makes sense to use the query's allocator.

We could get the allocator from the context argument passed in the C API, but I'd be against using it for cases where there are alternatives.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The serialized query buffer is not a child of the query IMO. We should do the work to properly pass the context into query_deserialize. It will require us to save the context instead of the threadpool in RestClientRemote.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated, we were already using the context in tiledb_deserialize_query.

@@ -918,6 +918,13 @@ class Query {
*/
RestClient* rest_client() const;

/**
* Returns the ContextResources associated to this query.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this. It was added as a way to get the serialization memory tracker but we should be able to get it from another source.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said above, query is for me the best candidate to get the allocator.

Copy link
Member Author

@teo-tsirpanis teo-tsirpanis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KiterLuc's changes LGTM, thanks!

@KiterLuc KiterLuc merged commit ef7aba6 into dev Sep 6, 2024
62 checks passed
@KiterLuc KiterLuc deleted the teo/serialization-buffer branch September 6, 2024 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants