Skip to content

perf: [sc-60446] Enumerations loaded from schema execute generate_value_map in a background task #5501

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

rroelke
Copy link
Member

@rroelke rroelke commented Apr 15, 2025

Story details: https://app.shortcut.com/tiledb-inc/story/60446

In the story we have observed that generate_value_map can take a non-trivial amount of time to execute. This function builds a map from enumeration variant to key, so that we can detect duplicates and do fast lookups.

However, generate_value_map is always called when we construct an Enumeration. However:

  1. barring a data corruption, an Enumeration which is constructed as we load it from an ArraySchema was already validated when it was either created or extended!
  2. lookup of the key for a variant is an uncommon operation, merged in Add Enumeration::index_of API #5482 which has not appeared in a release as of this writing.

Hence all users of Enumeration are paying the potentially expensive cost of an operation whose result is never used.

This pull request adds an option to migrate the execution of generate_value_map to a background task. If the bool async flag is true then generate_value_map will be called from a thread pool task, and Enumeration::value_map() will wait for that computation to finish. We construct Enumerations with async = true when loading them for an ArraySchema and async = false when they are created by the user (in which case we want the duplicate validation to happen immediately).

If an exception is thrown due to a corrupted Enumeration for some reason, then we will only see it upon calling value_map() in which case the exception thrown while executing the future will be thrown. This is tested in the added test case Enumeration Creation Error - Async.

This requires threading a ContextResources through all of the call stacks which find their way to Enumeration::create. This represents most of the changes in this PR, which are largely encapsulated in the single commit 28f97ce, so I recommend reviewing commit by commit.

I have also added timers to measure time spent in generate_value_map and time spent waiting for the it to finish.


TYPE: IMPROVEMENT
DESC: async Enumeration::generate_value_map

Copy link
Member

@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.

I'm not sure launching background tasks without the user knowing when they would finish is the right solution to performance problems like this.

@@ -268,7 +291,18 @@ class Enumeration {
* value map of the enumeration.
*/
const tdb::pmr::unordered_map<std::string_view, uint64_t>& value_map() const {
Copy link
Member

Choose a reason for hiding this comment

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

Instead of launching a background thread upon creation of the enumeration, can we compute value_map_ upon the first call to value_map()? This would alleviate most of my concerns.

@@ -122,6 +124,7 @@ shared_ptr<const Enumeration> enumeration_from_capnp(
data_size,
offsets,
offsets_size,
true,
Copy link
Member

Choose a reason for hiding this comment

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

I would expect a capnp deserialization function to completely finish its work once it returns.

Comment on lines +200 to +207
tiledb::common::ThreadPool::Task value_map_future =
resources.compute_tp().async(
[](std::reference_wrapper<Enumeration> enumeration) {
enumeration.get().generate_value_map();
return Status::Ok();
},
std::reference_wrapper<Enumeration>(*this));
value_map_future_ = std::move(value_map_future);
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
tiledb::common::ThreadPool::Task value_map_future =
resources.compute_tp().async(
[](std::reference_wrapper<Enumeration> enumeration) {
enumeration.get().generate_value_map();
return Status::Ok();
},
std::reference_wrapper<Enumeration>(*this));
value_map_future_ = std::move(value_map_future);
value_map_future_ =
resources.compute_tp().async(
[](std::reference_wrapper<Enumeration> enumeration) {
enumeration.get().generate_value_map();
return Status::Ok();
},
std::reference_wrapper<Enumeration>(*this));

(and format)

Comment on lines +214 to 223
Enumeration::~Enumeration() {
if (value_map_future_.valid()) {
auto st = value_map_future_.wait();
if (!st.ok()) {
tiledb::common::global_logger().warn(
"Enumeration::~Enumeration(): Error computing value map: " +
st.to_string());
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

I wouldn't log the warning; it's not important to tell the user that an operation they did not care for failed.

@@ -49,6 +50,7 @@ class EnumerationException : public StatusException {
};

Enumeration::Enumeration(
const ContextResources& resources,
Copy link
Member

Choose a reason for hiding this comment

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

I don't like a "data class" like Enumeration using a "behavior class" like ContextResources; it will make removing the Context dependency on the C API harder.

@teo-tsirpanis
Copy link
Member

We can generate value_map concurrently with other steps of the loading process if we want, but we must make sure all work finishes before the C API returns to the user. Otherwise we get an illusion of performance improvement that will likely make things worse in highly concurrent environments like the server.

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