diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 294109c1b26..95f81b57bed 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,4 +18,4 @@ trigger_pipeline: TILEDB_REF: ${CI_COMMIT_REF_NAME} trigger: project: tiledb-inc/tiledb-internal - strategy: depend + strategy: depend diff --git a/CMakeLists.txt b/CMakeLists.txt index a40b0b33f14..6b8359a1c09 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -447,6 +447,7 @@ target_include_directories(local_install INTERFACE ${CMAKE_SOURCE_DIR}) enable_testing() if(TILEDB_TESTS) + find_package(rapidcheck CONFIG REQUIRED) # Add custom Catch2 entry point that suppresses message boxes on debug assertion # failures on Windows CI. find_package(Catch2 REQUIRED) diff --git a/scripts/find_heap_api_violations.py b/scripts/find_heap_api_violations.py index 044dcad0446..12a232394c3 100755 --- a/scripts/find_heap_api_violations.py +++ b/scripts/find_heap_api_violations.py @@ -104,7 +104,7 @@ "zstd_compressor.h": ["std::unique_ptr ctx_;", "std::unique_ptr ctx_;"], "posix.cc": ["std::unique_ptr", "static std::unique_ptr cwd_(getcwd(nullptr, 0), free);"], "curl.h": ["std::unique_ptr"], - "pmr.h": ["std::unique_ptr", "unique_ptr make_unique("], + "pmr.h": ["std::unique_ptr", "unique_ptr make_unique(", "unique_ptr emplace_unique("], } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 5ca5f4d0d31..ea6fcc33b34 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -236,6 +236,7 @@ target_link_libraries(tiledb_unit Catch2::Catch2 tiledb_test_support_lib configuration_definitions + rapidcheck ) target_link_libraries(tiledb_unit PRIVATE $) @@ -373,9 +374,7 @@ endif() # CI tests add_subdirectory(ci) -if(WIN32) add_subdirectory(performance) -endif() add_custom_target( check-package diff --git a/test/performance/CMakeLists.txt b/test/performance/CMakeLists.txt index ec9c519ef32..ca927114ccf 100644 --- a/test/performance/CMakeLists.txt +++ b/test/performance/CMakeLists.txt @@ -26,6 +26,8 @@ # THE SOFTWARE. # +if(WIN32) + # These options not exposed in bootstrap script. option(TILEDB_TESTS_AWS_S3_CONFIG "Use an S3 config appropriate for AWS in tests" OFF) @@ -94,3 +96,24 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Linux" AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU LINK_FLAGS "-Wl,--no-as-needed -ldl" ) endif() + +endif() + + +add_executable( + tiledb_submit_a_b EXCLUDE_FROM_ALL + $ + "tiledb_submit_a_b.cc" +) + +target_include_directories( + tiledb_submit_a_b BEFORE PRIVATE + ${TILEDB_CORE_INCLUDE_DIR} + ${TILEDB_EXPORT_HEADER_DIR} +) + +target_link_libraries(tiledb_submit_a_b + PUBLIC + TILEDB_CORE_OBJECTS_ILIB + tiledb_test_support_lib +) diff --git a/test/performance/tiledb_submit_a_b.cc b/test/performance/tiledb_submit_a_b.cc new file mode 100644 index 00000000000..d485a36b69e --- /dev/null +++ b/test/performance/tiledb_submit_a_b.cc @@ -0,0 +1,709 @@ +/** + * @file test/performance/tiledb_submit_a_b.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file is an executable whose purpose is to compare the performance + * of a tiledb query running with two different configurations. + * + * Usage: $0 [additional array URIs...] + * + * For each array in the argument list, a query is created for each + * of the configurations in "configurations". + * The `run()` function alternates taking a step (`tiledb_query_submit`) + * with a query of each of those configurations, checking that they all + * have the same results, and recording various metrics from each + * (most notably, the time spent calling `tiledb_query_submit`) + * + * After executing upon all of the arrays, the timing information is + * dumped to `/tmp/tiledb_submit_a_b.json`. + * + * The arrays must have the same schema, whose physical type is reflected + * by the `using Fragment = ` declaration in the middle of + * this file. The `using Fragment` declaration sets the types of the buffers + * which are used to read the array. + * + * The nature of different configurations are specified via the required + * argument `config.json`. The following example illustrates the currently + * supported keys: + * + * ``` + * { + * "configurations": [ + * { + * "name": "a", + * "num_user_cells": 16777216, + * "memory_budget": { + * "total": "1073741824" + * }, + * "config": { + * "sm.query.sparse_global_order.preprocess_tile_merge": 0 + * } + * }, + * { + * "name": "b", + * "num_user_cells: 16777216 + * "memory_budget": { + * "total": "1073741824" + * }, + * "config": { + * "sm.query.sparse_global_order.preprocess_tile_merge": 128 + * } + * }, + * ], + * "queries": [ + * { + * "layout": "global_order", + * "subarray": [ + * { + * "start": { + * "%": 30 + * }, + * "end": { + * "%": 50 + * } + * } + * ] + * } + * ] + * } + * ``` + * + * The "config" field of each configuration object is a set of key-value + * pairs which are set on a `tileb::Config` object for instances + * of each query. + * - The "name" field identifies the configuration. + * - The "num_user_cells" field sets the size of the user buffer. + * - The "memory_budget" field sets the query memory budget. + * - The "config" field is a list of key-value pairs which are + * passed to the query configuration options. + * + * Each item in "queries" specifies a query to run the comparison for. + * + * The "layout" field is required and sets the results order. + * + * The "subarray" path is an optional list of range specifications on + * each dimension. Currently the only supported specification is + * an object with a field "%" which adds a subarray range whose + * bounds cover the specified percent of the non-empty domain + * for that dimension. + */ +#include +#include +#include +#include + +#include "tiledb/api/c_api/array/array_api_internal.h" +#include "tiledb/api/c_api/context/context_api_internal.h" +#include "tiledb/api/c_api/query/query_api_internal.h" +#include "tiledb/sm/array_schema/array_schema.h" +#include "tiledb/sm/array_schema/attribute.h" +#include "tiledb/sm/array_schema/dimension.h" +#include "tiledb/sm/cpp_api/tiledb" +#include "tiledb/sm/misc/comparators.h" +#include "tiledb/sm/stats/duration_instrument.h" +#include "tiledb/type/apply_with_type.h" + +#include "external/include/nlohmann/json.hpp" + +#include +#include +#include + +using namespace tiledb::test; +using namespace tiledb::test::templates; + +using Asserter = AsserterRuntimeException; + +using json = nlohmann::json; + +template +json optional_json(std::optional value) { + if (value.has_value()) { + return json(value.value()); + } else { + return json(); + } +} + +struct Configuration { + std::string name_; + std::optional num_user_cells_; + tiledb::Config qconf_; + + tiledb::test::SparseGlobalOrderReaderMemoryBudget memory_; + + Configuration() { + memory_.total_budget_ = std::to_string(1024 * 1024 * 1024); + memory_.ratio_tile_ranges_ = "0.01"; + } + + void memory_budget_from_json(const json& jmem) { + if (jmem.find("total") != jmem.end()) { + memory_.total_budget_ = jmem["total"].get(); + } + // TODO: other fields as needed + memory_.apply(qconf_.ptr().get()); + } +}; + +/** + * Records durations as reported by `tiledb::sm::stats::DurationInstrument`. + */ +struct StatKeeper { + struct StatKey { + std::string uri_; + std::string qlabel_; + std::string configname_; + }; + + struct StatValue { + std::vector durations; + std::map metrics; + }; + + using Timer = tiledb::sm::stats::DurationInstrument; + + /** records statistics of each submit call by "array.query" */ + std::map>> + statistics; + + StatValue& get(const StatKey& stat) { + return statistics[stat.uri_][stat.qlabel_][stat.configname_]; + } + + Timer start_timer(const StatKey& stat) { + return tiledb::sm::stats::DurationInstrument( + *this, stat); + } + + void report_duration( + const StatKey& stat, const std::chrono::duration duration) { + get(stat).durations.push_back(duration.count()); + } + + void report_metric( + const StatKey& stat, const std::string& name, const json& value) { + get(stat).metrics[name] = value; + } + + void report_timer( + const StatKey& stat, const std::string& name, const json& value) { + get(stat).metrics[name] = value; + } + + /** + * Write durations to a file for analysis. + */ + void dump_durations(std::ostream& dump) const { + json arrays; + + for (const auto& uri : statistics) { + json array; + array["uri"] = uri.first; + + for (const auto& qlabel : uri.second) { + json run; + run["query"] = qlabel.first; + for (const auto& config : qlabel.second) { + json query; + query["first"] = config.second.durations[0]; + query["sum"] = std::accumulate( + config.second.durations.begin(), + config.second.durations.end(), + (double)(0)); + + for (const auto& metric : config.second.metrics) { + query[metric.first] = metric.second; + } + + run[config.first] = query; + } + + array["queries"].push_back(run); + } + + arrays.push_back(array); + } + + dump << std::setw(4) << arrays << std::endl; + } +}; + +struct SubarrayDimension { + std::optional start_percent_; + std::optional end_percent_; + + static SubarrayDimension from_json(const json& jdim) { + SubarrayDimension dim; + if (jdim.find("start") != jdim.end()) { + dim.start_percent_.emplace(jdim["start"]["%"].get()); + } + if (jdim.find("end") != jdim.end()) { + dim.end_percent_.emplace(jdim["end"]["%"].get()); + } + return dim; + } + + void apply( + tiledb::Array& array, unsigned dim, tiledb::Subarray& subarray) const { + // FIXME: make generic domain type + int64_t non_empty_domain[2], subarray_domain[2]; + std::tie(non_empty_domain[0], non_empty_domain[1]) = + array.non_empty_domain(dim); + + subarray_domain[0] = non_empty_domain[0]; + subarray_domain[1] = non_empty_domain[1]; + + const auto span = (non_empty_domain[1] - non_empty_domain[0] + 1); + if (start_percent_.has_value()) { + subarray_domain[0] = + non_empty_domain[0] + (span * start_percent_.value()) / 100; + } + if (end_percent_.has_value()) { + subarray_domain[1] = + non_empty_domain[0] + ((span * end_percent_.value()) / 100); + } + + subarray.add_range(dim, subarray_domain[0], subarray_domain[1]); + } +}; + +struct Query { + std::string label_; + tiledb_layout_t layout_; + std::vector> subarray_; + + static Query from_json(const json& jq) { + Query q; + q.label_ = jq["label"].get(); + + const auto layout = jq["layout"].get(); + if (layout == "global_order") { + q.layout_ = TILEDB_GLOBAL_ORDER; + } else if (layout == "row_major") { + q.layout_ = TILEDB_ROW_MAJOR; + } else if (layout == "col_major") { + q.layout_ = TILEDB_COL_MAJOR; + } else if (layout == "unordered") { + throw std::runtime_error( + "TILEDB_UNORDERED is not implemented; the unstable results order " + "means " + "we cannot compare the coordinates from interleaved submits. This " + "can " + "be implemented by buffering all of the query results and then " + "sorting " + "on the coordinates"); + } else { + throw std::runtime_error("Invalid 'layout' for query: " + layout); + } + + if (jq.find("subarray") != jq.end()) { + const auto& jsub = jq["subarray"]; + for (const auto& jdim : jsub) { + if (jdim.is_null()) { + q.subarray_.push_back(std::nullopt); + } else { + q.subarray_.push_back(SubarrayDimension::from_json(jdim)); + } + } + } + return q; + } + + /** + * Applies this configuration to a query. + */ + void apply( + tiledb::Context& ctx, tiledb::Array& array, tiledb::Query& query) const { + query.set_layout(layout_); + + if (!subarray_.empty()) { + tiledb::Subarray subarray(ctx, array); + + tiledb::ArraySchema schema = array.schema(); + + for (unsigned d = 0; d < subarray_.size(); d++) { + if (subarray_[d].has_value()) { + subarray_[d].value().apply(array, d, subarray); + } + } + + query.set_subarray(subarray); + } + } +}; + +template +tiledb::common::Status assertGlobalOrder( + const tiledb::Array& array, + const Fragment& data, + size_t num_cells, + size_t start, + size_t end) { + tiledb::sm::GlobalCellCmp globalcmp( + array.ptr()->array()->array_schema_latest().domain()); + + for (uint64_t i = start + 1; i < end; i++) { + const auto prevtuple = std::apply( + [&](const std::vector&... dims) { + return global_cell_cmp_std_tuple(std::make_tuple(dims[i - 1]...)); + }, + data.dimensions()); + const auto nexttuple = std::apply( + [&](const std::vector&... dims) { + return global_cell_cmp_std_tuple(std::make_tuple(dims[i]...)); + }, + data.dimensions()); + + if (globalcmp(nexttuple, prevtuple)) { + return tiledb::common::Status_Error( + "Out of order: start= " + std::to_string(start) + + ", pos=" + std::to_string(i) + ", end=" + std::to_string(end) + + ", num_cells=" + std::to_string(num_cells)); + } + } + return tiledb::common::Status_Ok(); +} + +template +struct require_type { + static void dimension(const tiledb::sm::Dimension& dim) { + const auto dt = dim.type(); + const bool is_same = tiledb::type::apply_with_type( + [](auto arg) { return std::is_same_v; }, dt); + if (!is_same) { + throw std::runtime_error( + "Incompatible template type for dimension '" + dim.name() + + "' of type " + datatype_str(dt)); + } + } + + static void attribute(const tiledb::sm::Attribute& att) { + const auto dt = att.type(); + const bool is_same = tiledb::type::apply_with_type( + [](auto arg) { return std::is_same_v; }, dt); + if (!is_same) { + throw std::runtime_error( + "Incompatible template type for attribute '" + att.name() + + "' of type " + datatype_str(dt)); + } + } +}; + +template +static void check_compatibility(const tiledb::Array& array) { + using DimensionTuple = decltype(std::declval().dimensions()); + using AttributeTuple = decltype(std::declval().attributes()); + + constexpr auto expect_num_dims = std::tuple_size_v; + constexpr auto max_num_atts = std::tuple_size_v; + + const auto& schema = array.ptr()->array()->array_schema_latest(); + + if (schema.domain().dim_num() != expect_num_dims) { + throw std::runtime_error( + "Expected " + std::to_string(expect_num_dims) + " dimensions, found " + + std::to_string(schema.domain().dim_num())); + } + if (schema.attribute_num() < max_num_atts) { + throw std::runtime_error( + "Expected " + std::to_string(max_num_atts) + " attributes, found " + + std::to_string(schema.attribute_num())); + } + + unsigned d = 0; + std::apply( + [&](std::vector...) { + (require_type::dimension(*schema.domain().shared_dimension(d++)), + ...); + }, + stdx::decay_tuple()); + + unsigned a = 0; + std::apply( + [&](std::vector...) { + (require_type::attribute(*schema.attribute(a++)), ...); + }, + stdx::decay_tuple()); +} + +template +static void run( + StatKeeper& stat_keeper, + const char* array_uri, + const Query& query_config, + const std::span configs) { + std::vector num_user_cells; + for (const auto& config : configs) { + num_user_cells.push_back( + config.num_user_cells_.value_or(1024 * 1024 * 128)); + } + + std::vector qdata(configs.size()); + + auto reset = [&](size_t q) { + std::apply( + [&](auto&... outfield) { (outfield.resize(num_user_cells[q]), ...); }, + std::tuple_cat(qdata[q].dimensions(), qdata[q].attributes())); + }; + for (size_t q = 0; q < qdata.size(); q++) { + reset(q); + } + + tiledb::Context ctx; + + // Open array for reading. + tiledb::Array array(ctx, array_uri, TILEDB_READ); + check_compatibility(array); + + auto dimension_name = [&](unsigned d) -> std::string { + return std::string( + array.ptr()->array_schema_latest().domain().dimension_ptr(d)->name()); + }; + auto attribute_name = [&](unsigned a) -> std::string { + return std::string(array.ptr()->array_schema_latest().attribute(a)->name()); + }; + + std::vector queries; + for (const auto& config : configs) { + tiledb::Query query(ctx, array, TILEDB_READ); + query.set_config(config.qconf_); + query_config.apply(ctx, array, query); + + queries.push_back(query); + } + + std::vector keys; + for (const auto& config : configs) { + keys.push_back(StatKeeper::StatKey{ + .uri_ = std::string(array_uri), + .qlabel_ = query_config.label_, + .configname_ = config.name_}); + } + + // helper to do basic checks on both + auto do_submit = [&](auto& key, auto& query, auto& outdata) + -> std::pair { + // make field size locations + auto dimension_sizes = + templates::query::make_field_sizes(outdata.dimensions()); + auto attribute_sizes = + templates::query::make_field_sizes(outdata.attributes()); + + // add fields to query + templates::query::set_fields( + ctx.ptr().get(), + query.ptr().get(), + dimension_sizes, + outdata.dimensions(), + dimension_name); + templates::query::set_fields( + ctx.ptr().get(), + query.ptr().get(), + attribute_sizes, + outdata.attributes(), + attribute_name); + + tiledb::Query::Status status; + { + StatKeeper::Timer qtimer = stat_keeper.start_timer(key); + status = query.submit(); + } + + const uint64_t dim_num_cells = templates::query::num_cells( + outdata.dimensions(), dimension_sizes); + const uint64_t att_num_cells = templates::query::num_cells( + outdata.attributes(), attribute_sizes); + + ASSERTER(dim_num_cells == att_num_cells); + + if (dim_num_cells < outdata.size()) { + // since the user buffer did not fill up the query must be complete + ASSERTER(status == tiledb::Query::Status::COMPLETE); + } + + return std::make_pair(status, dim_num_cells); + }; + + while (true) { + std::vector status; + std::vector num_cells; + + for (size_t q = 0; q < queries.size(); q++) { + auto result = do_submit(keys[q], queries[q], qdata[q]); + status.push_back(result.first); + num_cells.push_back(result.second); + } + + for (size_t q = 0; q < queries.size(); q++) { + std::apply( + [&](auto&... outfield) { (outfield.resize(num_cells[0]), ...); }, + std::tuple_cat(qdata[q].dimensions(), qdata[q].attributes())); + + ASSERTER(num_cells[q] <= num_user_cells[q]); + + if (q > 0) { + ASSERTER(num_cells[q] == num_cells[0]); + ASSERTER(qdata[q].dimensions() == qdata[0].dimensions()); + + // NB: this is only correct if there are no duplicate coordinates, + // in which case we need to adapt what CSparseGlobalOrderFx::run does + ASSERTER(qdata[q].attributes() == qdata[0].attributes()); + } + } + + for (size_t q = 0; q < queries.size(); q++) { + reset(q); + } + + for (size_t q = 1; q < queries.size(); q++) { + ASSERTER(status[q] == status[0]); + } + + if (queries[0].query_layout() == TILEDB_GLOBAL_ORDER) { + // assert that the results arrive in global order + // do it in parallel, it is slow + auto* tp = ctx.ptr()->context().compute_tp(); + const size_t parallel_factor = std::max( + 1, std::min(tp->concurrency_level(), num_cells[0] / 1024)); + const size_t items_per = num_cells[0] / parallel_factor; + const auto isGlobalOrder = + tiledb::sm::parallel_for(tp, 0, parallel_factor, [&](uint64_t i) { + const uint64_t mystart = i * items_per; + const uint64_t myend = + std::min((i + 1) * items_per + 1, num_cells[0]); + return assertGlobalOrder( + array, qdata[0], num_cells[0], mystart, myend); + }); + if (!isGlobalOrder.ok()) { + throw std::runtime_error(isGlobalOrder.to_string()); + } + } + + if (status[0] == tiledb::Query::Status::COMPLETE) { + break; + } + } + + // record additional stats + for (size_t q = 0; q < queries.size(); q++) { + const tiledb::sm::Query& query_internal = *queries[q].ptr().get()->query_; + const auto& stats = *query_internal.stats(); + + stat_keeper.report_metric( + keys[q], "loop_num", optional_json(stats.find_counter("loop_num"))); + stat_keeper.report_metric( + keys[q], + "internal_loop_num", + optional_json(stats.find_counter("internal_loop_num"))); + + // record parallel merge timers + stat_keeper.report_timer( + keys[q], + "preprocess_result_tile_order_compute", + optional_json( + stats.find_timer("preprocess_result_tile_order_compute.sum"))); + stat_keeper.report_timer( + keys[q], + "preprocess_result_tile_order_await", + optional_json( + stats.find_timer("preprocess_result_tile_order_await.sum"))); + } +} + +// change this to match the schema of the target arrays +using Fragment = Fragment2D; + +/** + * Reads key-value pairs from a JSON object to construct and return a + * `tiledb_config_t`. + */ +tiledb::Config json2config(const json& j) { + std::map params; + const json jconf = j["config"]; + for (auto it = jconf.begin(); it != jconf.end(); ++it) { + const auto key = it.key(); + const auto value = nlohmann::to_string(it.value()); + params[key] = nlohmann::to_string(it.value()); + } + + return tiledb::Config(params); +} + +int main(int argc, char** argv) { + json config; + { + std::ifstream configfile(argv[1]); + if (configfile.fail()) { + std::cerr << "Error opening config file: " << argv[1] << std::endl; + return -1; + } + config = json::parse( + std::istreambuf_iterator(configfile), + std::istreambuf_iterator()); + } + + std::vector qconfs; + for (const auto& jsoncfg : config["configurations"]) { + Configuration cfg; + cfg.name_ = jsoncfg["name"].get(); + cfg.qconf_ = json2config(jsoncfg); + if (jsoncfg.find("num_user_cells") != jsoncfg.end()) { + cfg.num_user_cells_.emplace(jsoncfg["num_user_cells"].get()); + } + if (jsoncfg.find("memory_budget") != jsoncfg.end()) { + cfg.memory_budget_from_json(jsoncfg["memory_budget"]); + } + qconfs.push_back(cfg); + } + + StatKeeper stat_keeper; + + std::span array_uris( + static_cast(&argv[2]), argc - 2); + + for (const auto& query : config["queries"]) { + Query qq = Query::from_json(query); + + for (const auto& array_uri : array_uris) { + try { + run(stat_keeper, array_uri, qq, qconfs); + } catch (const std::exception& e) { + std::cerr << "Error on array \"" << array_uri << "\": " << e.what() + << std::endl; + + return 1; + } + } + } + + std::ofstream out("/tmp/tiledb_submit_a_b.json"); + stat_keeper.dump_durations(out); + + return 0; +} diff --git a/test/src/unit-capi-config.cc b/test/src/unit-capi-config.cc index 37a731d0c70..14eb227d6ed 100644 --- a/test/src/unit-capi-config.cc +++ b/test/src/unit-capi-config.cc @@ -32,6 +32,7 @@ #include #include "tiledb/sm/c_api/tiledb.h" +#include "tiledb/sm/config/config.h" #include #include @@ -41,6 +42,8 @@ #include #include +using tiledb::sm::Config; + void remove_file(const std::string& filename) { // Remove file tiledb_ctx_t* ctx = nullptr; @@ -292,6 +295,8 @@ void check_save_to_file() { ss << "sm.partial_tile_offsets_loading false\n"; ss << "sm.query.dense.qc_coords_mode false\n"; ss << "sm.query.dense.reader refactored\n"; + ss << "sm.query.sparse_global_order.preprocess_tile_merge " + << Config::SM_QUERY_SPARSE_GLOBAL_ORDER_PREPROCESS_TILE_MERGE << "\n"; ss << "sm.query.sparse_global_order.reader refactored\n"; ss << "sm.query.sparse_unordered_with_dups.reader refactored\n"; ss << "sm.read_range_oob warn\n"; @@ -641,6 +646,8 @@ TEST_CASE("C API: Test config iter", "[capi][config]") { all_param_values["sm.memory_budget_var"] = "10737418240"; all_param_values["sm.query.dense.qc_coords_mode"] = "false"; all_param_values["sm.query.dense.reader"] = "refactored"; + all_param_values["sm.query.sparse_global_order.preprocess_tile_merge"] = + Config::SM_QUERY_SPARSE_GLOBAL_ORDER_PREPROCESS_TILE_MERGE; all_param_values["sm.query.sparse_global_order.reader"] = "refactored"; all_param_values["sm.query.sparse_unordered_with_dups.reader"] = "refactored"; all_param_values["sm.mem.consolidation.buffers_weight"] = "1"; diff --git a/test/src/unit-capi-incomplete.cc b/test/src/unit-capi-incomplete.cc index d45b02692f3..62a2b283c18 100644 --- a/test/src/unit-capi-incomplete.cc +++ b/test/src/unit-capi-incomplete.cc @@ -31,6 +31,7 @@ */ #include +#include "test/support/src/error_helpers.h" #include "test/support/src/helpers.h" #include "test/support/src/vfs_helpers.h" #include "tiledb/sm/c_api/tiledb.h" @@ -42,6 +43,8 @@ using namespace tiledb::test; +using Asserter = tiledb::test::AsserterCatch; + /** * Tests cases where a read query is incomplete or leads to a buffer * overflow. @@ -238,8 +241,7 @@ void IncompleteFx::create_sparse_array() { CHECK(rc == TILEDB_OK); // Create array - rc = tiledb_array_create(ctx_, sparse_array_uri_.c_str(), array_schema); - CHECK(rc == TILEDB_OK); + TRY(ctx_, tiledb_array_create(ctx_, sparse_array_uri_.c_str(), array_schema)); // Clean up tiledb_attribute_free(&a1); @@ -1179,7 +1181,7 @@ TEST_CASE_METHOD( TEST_CASE_METHOD( IncompleteFx, - "C API: Test incomplete read queries, sparse", + "C API: Test incomplete read queries, sparse force retry", "[capi][incomplete][sparse][retries][sc-49128][rest]") { // This test is testing CURL logic and only makes sense on REST-CI if (!vfs_test_setup_.is_rest()) { diff --git a/test/src/unit-capi-sparse_array.cc b/test/src/unit-capi-sparse_array.cc index b3d68d0e7b2..9494fc20145 100644 --- a/test/src/unit-capi-sparse_array.cc +++ b/test/src/unit-capi-sparse_array.cc @@ -37,6 +37,7 @@ #endif #include +#include "test/support/src/error_helpers.h" #include "test/support/src/helpers.h" #include "test/support/src/vfs_helpers.h" #ifdef _WIN32 @@ -61,6 +62,8 @@ using namespace tiledb::test; +using Asserter = tiledb::test::AsserterCatch; + const uint64_t DIM_DOMAIN[4] = {1, 4, 1, 4}; struct SparseArrayFx { @@ -3380,8 +3383,7 @@ TEST_CASE_METHOD( CHECK(coords_dim2[1] == 4); } - rc = tiledb_query_submit(ctx_, query); - CHECK(rc == TILEDB_OK); + TRY(ctx_, tiledb_query_submit(ctx_, query)); rc = tiledb_query_get_status(ctx_, query, &status); REQUIRE(rc == TILEDB_OK); REQUIRE(status == TILEDB_COMPLETED); diff --git a/test/src/unit-sparse-global-order-reader.cc b/test/src/unit-sparse-global-order-reader.cc index b219ff26b82..678ccd7112a 100644 --- a/test/src/unit-sparse-global-order-reader.cc +++ b/test/src/unit-sparse-global-order-reader.cc @@ -30,11 +30,20 @@ * Tests for the sparse global order reader. */ +#include "test/support/rapidcheck/array_templates.h" +#include "test/support/src/array_helpers.h" +#include "test/support/src/array_templates.h" +#include "test/support/src/error_helpers.h" #include "test/support/src/helpers.h" #include "test/support/src/vfs_helpers.h" +#include "tiledb/api/c_api/array/array_api_internal.h" #include "tiledb/sm/c_api/tiledb.h" #include "tiledb/sm/c_api/tiledb_struct_def.h" #include "tiledb/sm/cpp_api/tiledb" +#include "tiledb/sm/enums/datatype.h" +#include "tiledb/sm/misc/comparators.h" +#include "tiledb/sm/misc/types.h" +#include "tiledb/sm/query/readers/result_tile.h" #include "tiledb/sm/query/readers/sparse_global_order_reader.h" #ifdef _WIN32 @@ -44,31 +53,391 @@ #endif #include +#include + +#include + +#include +#include #include using namespace tiledb; using namespace tiledb::test; +using tiledb::sm::Datatype; +using tiledb::test::templates::AttributeType; +using tiledb::test::templates::DimensionType; +using tiledb::test::templates::FragmentType; + +template +using Subarray1DType = std::vector>; + +template +using Subarray2DType = std::vector>, + std::optional>>>; + +namespace rc { +Gen>> make_subarray_1d( + const templates::Domain& domain); + +Gen> make_subarray_2d( + const templates::Domain& d1, const templates::Domain& d2); +} // namespace rc + /* ********************************* */ /* STRUCT DEFINITION */ /* ********************************* */ +/** + * Options for configuring the CSparseGlobalFx default 1D array + */ +struct DefaultArray1DConfig { + uint64_t capacity_; + bool allow_dups_; + + templates::Dimension dimension_; + + DefaultArray1DConfig() + : capacity_(2) + , allow_dups_(false) { + dimension_.domain.lower_bound = 1; + dimension_.domain.upper_bound = 200; + dimension_.extent = 2; + } + + DefaultArray1DConfig with_allow_dups(bool allow_dups) const { + auto copy = *this; + copy.allow_dups_ = allow_dups; + return copy; + } +}; + +/** + * An instance of one-dimension array input to `CSparseGlobalOrderFx::run` + */ +struct FxRun1D { + using FragmentType = templates::Fragment1D; + + uint64_t num_user_cells; + std::vector fragments; + + // NB: for now this always has length 1, global order query does not + // support multi-range subarray + std::vector> subarray; + + DefaultArray1DConfig array; + SparseGlobalOrderReaderMemoryBudget memory; + + uint64_t tile_capacity() const { + return array.capacity_; + } + + bool allow_duplicates() const { + return array.allow_dups_; + } + + tiledb_layout_t tile_order() const { + // for 1D it is the same + return TILEDB_ROW_MAJOR; + } + + tiledb_layout_t cell_order() const { + // for 1D it is the same + return TILEDB_ROW_MAJOR; + } + + /** + * Add `subarray` to a read query + */ + template + capi_return_t apply_subarray( + tiledb_ctx_t* ctx, tiledb_subarray_t* subarray) const { + for (const auto& range : this->subarray) { + RETURN_IF_ERR(tiledb_subarray_add_range( + ctx, subarray, 0, &range.lower_bound, &range.upper_bound, nullptr)); + } + return TILEDB_OK; + } + + /** + * @return true if the cell at index `record` in `fragment` passes `subarray` + */ + bool accept(const FragmentType& fragment, int record) const { + if (subarray.empty()) { + return true; + } else { + const int coord = fragment.dim_[record]; + for (const auto& range : subarray) { + if (range.contains(coord)) { + return true; + } + } + return false; + } + } + + /** + * @return true if `mbr` intersects with any of the ranges in `subarray` + */ + bool intersects(const sm::NDRange& mbr) const { + auto accept = [&](const auto& range) { + const auto& untyped_mbr = mbr[0]; + const templates::Domain typed_mbr( + untyped_mbr.start_as(), untyped_mbr.end_as()); + return range.intersects(typed_mbr); + }; + return subarray.empty() || + std::any_of(subarray.begin(), subarray.end(), accept); + } + + /** + * @return a new range which is `mbr` with its upper bound "clamped" to that + * of `subarray` + */ + sm::NDRange clamp(const sm::NDRange& mbr) const { + if (subarray.empty()) { + return mbr; + } + assert(subarray.size() == 1); + if (subarray[0].upper_bound < mbr[0].end_as()) { + // in this case, the bitmap will filter out the other coords in the + // tile and it will be discarded + return std::vector{subarray[0].range()}; + } else { + return mbr; + } + } + + std::tuple&> dimensions() const { + return std::tuple&>( + array.dimension_); + } + + std::tuple attributes() const { + return std::make_tuple(Datatype::INT32); + } +}; + +struct FxRun2D { + using Coord0Type = int; + using Coord1Type = int; + using FragmentType = templates::Fragment2D; + + std::vector fragments; + std::vector>, + std::optional>>> + subarray; + + size_t num_user_cells; + + uint64_t capacity; + bool allow_dups; + tiledb_layout_t tile_order_; + tiledb_layout_t cell_order_; + templates::Dimension d1; + templates::Dimension d2; + + SparseGlobalOrderReaderMemoryBudget memory; + + FxRun2D() + : capacity(64) + , allow_dups(true) + , tile_order_(TILEDB_ROW_MAJOR) + , cell_order_(TILEDB_ROW_MAJOR) { + d1.domain = templates::Domain(1, 200); + d1.extent = 8; + d2.domain = templates::Domain(1, 200); + d2.extent = 8; + } + + uint64_t tile_capacity() const { + return capacity; + } + + bool allow_duplicates() const { + return allow_dups; + } + + tiledb_layout_t tile_order() const { + return tile_order_; + } + + tiledb_layout_t cell_order() const { + return cell_order_; + } + + /** + * Add `subarray` to a read query + */ + template + capi_return_t apply_subarray( + tiledb_ctx_t* ctx, tiledb_subarray_t* subarray) const { + for (const auto& range : this->subarray) { + if (range.first.has_value()) { + RETURN_IF_ERR(tiledb_subarray_add_range( + ctx, + subarray, + 0, + &range.first->lower_bound, + &range.first->upper_bound, + nullptr)); + } else { + RETURN_IF_ERR(tiledb_subarray_add_range( + ctx, + subarray, + 0, + &d1.domain.lower_bound, + &d1.domain.upper_bound, + nullptr)); + } + if (range.second.has_value()) { + RETURN_IF_ERR(tiledb_subarray_add_range( + ctx, + subarray, + 1, + &range.second->lower_bound, + &range.second->upper_bound, + nullptr)); + } else { + RETURN_IF_ERR(tiledb_subarray_add_range( + ctx, + subarray, + 1, + &d2.domain.lower_bound, + &d2.domain.upper_bound, + nullptr)); + } + } + return TILEDB_OK; + } + + /** + * @return true if the cell at index `record` in `fragment` passes `subarray` + */ + bool accept(const FragmentType& fragment, int record) const { + if (subarray.empty()) { + return true; + } else { + const int r = fragment.d1_[record], c = fragment.d2_[record]; + for (const auto& range : subarray) { + if (range.first.has_value() && !range.first->contains(r)) { + continue; + } else if (range.second.has_value() && !range.second->contains(c)) { + continue; + } else { + return true; + } + } + return false; + } + } + + /** + * @return true if `mbr` intersects with any of the ranges in `subarray` + */ + bool intersects(const sm::NDRange& mbr) const { + if (subarray.empty()) { + return true; + } + + const templates::Domain typed_domain0( + mbr[0].start_as(), mbr[0].end_as()); + const templates::Domain typed_domain1( + mbr[1].start_as(), mbr[1].end_as()); + + for (const auto& range : subarray) { + if (range.first.has_value() && !range.first->intersects(typed_domain0)) { + continue; + } else if ( + range.second.has_value() && + !range.second->intersects(typed_domain1)) { + continue; + } else { + return true; + } + } + return false; + } + + /** + * @return a new range which is `mbr` with its upper bound "clamped" to that + * of `subarray` + */ + sm::NDRange clamp(const sm::NDRange& mbr) const { + sm::NDRange clamped = mbr; + for (const auto& range : subarray) { + if (range.first.has_value() && + range.first->upper_bound < clamped[0].end_as()) { + clamped[0].set_end_fixed(&range.first->upper_bound); + } + if (range.second.has_value() && + range.second->upper_bound < clamped[1].end_as()) { + clamped[1].set_end_fixed(&range.second->upper_bound); + } + } + return clamped; + } + + using CoordsRefType = std::tuple< + const templates::Dimension&, + const templates::Dimension&>; + CoordsRefType dimensions() const { + return CoordsRefType(d1, d2); + } + + std::tuple attributes() const { + return std::make_tuple(Datatype::INT32); + } +}; + +/** + * Describes types which can be used with `CSparseGlobalOrderFx::run`. + */ +template +concept InstanceType = requires(const T& instance) { + { instance.tile_capacity() } -> std::convertible_to; + { instance.allow_duplicates() } -> std::same_as; + { instance.tile_order() } -> std::same_as; + + { instance.num_user_cells } -> std::convertible_to; + + instance.fragments; + instance.memory; + instance.subarray; + instance.dimensions(); + instance.attributes(); + + // also `accept(Self::FragmentType, int)`, unclear how to represent that +}; + struct CSparseGlobalOrderFx { - tiledb_ctx_t* ctx_ = nullptr; - tiledb_vfs_t* vfs_ = nullptr; - std::string temp_dir_; + VFSTestSetup vfs_test_setup_; + std::string array_name_; const char* ARRAY_NAME = "test_sparse_global_order"; tiledb_array_t* array_ = nullptr; - std::string total_budget_; - std::string ratio_tile_ranges_; - std::string ratio_array_data_; - std::string ratio_coords_; - void create_default_array_1d(bool allow_dups = false); + SparseGlobalOrderReaderMemoryBudget memory_; + + tiledb_ctx_t* context() const { + return vfs_test_setup_.ctx_c; + } + + template + void create_default_array_1d( + const DefaultArray1DConfig& config = DefaultArray1DConfig()); + void create_default_array_1d_strings(bool allow_dups = false); + + template void write_1d_fragment( int* coords, uint64_t* coords_size, int* data, uint64_t* data_size); + + template + void write_fragment(const Fragment& fragment); + void write_1d_fragment_strings( int* coords, uint64_t* coords_size, @@ -85,7 +454,7 @@ struct CSparseGlobalOrderFx { int* data, uint64_t* data_size, tiledb_query_t** query = nullptr, - tiledb_array_t** array_ret = nullptr, + CApiArray* array_ret = nullptr, std::vector subarray = {1, 10}); int32_t read_strings( bool set_subarray, @@ -101,45 +470,52 @@ struct CSparseGlobalOrderFx { void reset_config(); void update_config(); + template + void create_array(const Instance& instance); + + template + DeleteArrayGuard run_create(Instance& instance); + template + void run_execute(Instance& instance); + + /** + * Runs an input against a fresh array. + * Inserts fragments one at a time, then reads them back in global order + * and checks that what we read out matches what we put in. + */ + template + void run(Instance& instance); + + template + std::string error_if_any(CAPIReturn apirc) const; + CSparseGlobalOrderFx(); ~CSparseGlobalOrderFx(); }; +template +std::string CSparseGlobalOrderFx::error_if_any(CAPIReturn apirc) const { + return tiledb::test::error_if_any(context(), apirc); +} + CSparseGlobalOrderFx::CSparseGlobalOrderFx() { reset_config(); - // Create temporary directory based on the supported filesystem. -#ifdef _WIN32 - temp_dir_ = tiledb::sm::Win::current_dir() + "\\tiledb_test\\"; -#else - temp_dir_ = "file://" + tiledb::sm::Posix::current_dir() + "/tiledb_test/"; -#endif - create_dir(temp_dir_, ctx_, vfs_); - array_name_ = temp_dir_ + ARRAY_NAME; + array_name_ = vfs_test_setup_.array_uri("tiledb_test"); } CSparseGlobalOrderFx::~CSparseGlobalOrderFx() { - tiledb_array_free(&array_); - remove_dir(temp_dir_, ctx_, vfs_); - tiledb_ctx_free(&ctx_); - tiledb_vfs_free(&vfs_); + if (array_) { + tiledb_array_free(&array_); + } } void CSparseGlobalOrderFx::reset_config() { - total_budget_ = "1048576"; - ratio_tile_ranges_ = "0.1"; - ratio_array_data_ = "0.1"; - ratio_coords_ = "0.5"; + memory_ = SparseGlobalOrderReaderMemoryBudget(); update_config(); } void CSparseGlobalOrderFx::update_config() { - if (ctx_ != nullptr) - tiledb_ctx_free(&ctx_); - - if (vfs_ != nullptr) - tiledb_vfs_free(&vfs_); - tiledb_config_t* config; tiledb_error_t* error = nullptr; REQUIRE(tiledb_config_alloc(&config, &error) == TILEDB_OK); @@ -153,68 +529,37 @@ void CSparseGlobalOrderFx::update_config() { &error) == TILEDB_OK); REQUIRE(error == nullptr); - REQUIRE( - tiledb_config_set( - config, "sm.mem.total_budget", total_budget_.c_str(), &error) == - TILEDB_OK); - REQUIRE(error == nullptr); - - REQUIRE( - tiledb_config_set( - config, - "sm.mem.reader.sparse_global_order.ratio_tile_ranges", - ratio_tile_ranges_.c_str(), - &error) == TILEDB_OK); - REQUIRE(error == nullptr); - - REQUIRE( - tiledb_config_set( - config, - "sm.mem.reader.sparse_global_order.ratio_array_data", - ratio_array_data_.c_str(), - &error) == TILEDB_OK); - REQUIRE(error == nullptr); - - REQUIRE( - tiledb_config_set( - config, - "sm.mem.reader.sparse_global_order.ratio_coords", - ratio_coords_.c_str(), - &error) == TILEDB_OK); - REQUIRE(error == nullptr); + REQUIRE(memory_.apply(config) == nullptr); - REQUIRE(tiledb_ctx_alloc(config, &ctx_) == TILEDB_OK); - REQUIRE(error == nullptr); - REQUIRE(tiledb_vfs_alloc(ctx_, config, &vfs_) == TILEDB_OK); - tiledb_config_free(&config); + vfs_test_setup_.update_config(config); } -void CSparseGlobalOrderFx::create_default_array_1d(bool allow_dups) { - int domain[] = {1, 200}; - int tile_extent = 2; - create_array( - ctx_, +template +void CSparseGlobalOrderFx::create_default_array_1d( + const DefaultArray1DConfig& config) { + tiledb::test::create_array( + context(), array_name_, TILEDB_SPARSE, {"d"}, {TILEDB_INT32}, - {domain}, - {&tile_extent}, + std::vector{(void*)(&config.dimension_.domain.lower_bound)}, + std::vector{(void*)(&config.dimension_.extent)}, {"a"}, {TILEDB_INT32}, {1}, {tiledb::test::Compressor(TILEDB_FILTER_NONE, -1)}, TILEDB_ROW_MAJOR, TILEDB_ROW_MAJOR, - 2, - allow_dups); + config.capacity_, + config.allow_dups_); } void CSparseGlobalOrderFx::create_default_array_1d_strings(bool allow_dups) { int domain[] = {1, 200}; int tile_extent = 2; - create_array( - ctx_, + tiledb::test::create_array( + context(), array_name_, TILEDB_SPARSE, {"d"}, @@ -231,36 +576,82 @@ void CSparseGlobalOrderFx::create_default_array_1d_strings(bool allow_dups) { allow_dups); } +template void CSparseGlobalOrderFx::write_1d_fragment( int* coords, uint64_t* coords_size, int* data, uint64_t* data_size) { // Open array for writing. - tiledb_array_t* array; - auto rc = tiledb_array_alloc(ctx_, array_name_.c_str(), &array); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_open(ctx_, array, TILEDB_WRITE); - REQUIRE(rc == TILEDB_OK); + CApiArray array(context(), array_name_.c_str(), TILEDB_WRITE); // Create the query. tiledb_query_t* query; - rc = tiledb_query_alloc(ctx_, array, TILEDB_WRITE, &query); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_layout(ctx_, query, TILEDB_UNORDERED); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "a", data, data_size); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "d", coords, coords_size); - REQUIRE(rc == TILEDB_OK); + auto rc = tiledb_query_alloc(context(), array, TILEDB_WRITE, &query); + ASSERTER(rc == TILEDB_OK); + rc = tiledb_query_set_layout(context(), query, TILEDB_UNORDERED); + ASSERTER(rc == TILEDB_OK); + rc = tiledb_query_set_data_buffer(context(), query, "a", data, data_size); + ASSERTER(rc == TILEDB_OK); + rc = tiledb_query_set_data_buffer(context(), query, "d", coords, coords_size); + ASSERTER(rc == TILEDB_OK); // Submit query. - rc = tiledb_query_submit(ctx_, query); - REQUIRE(rc == TILEDB_OK); + rc = tiledb_query_submit(context(), query); + ASSERTER("" == error_if_any(rc)); - // Close array. - rc = tiledb_array_close(ctx_, array); - REQUIRE(rc == TILEDB_OK); + // Clean up. + tiledb_query_free(&query); +} + +/** + * Writes a generic `FragmentType` into the array. + */ +template +void CSparseGlobalOrderFx::write_fragment(const Fragment& fragment) { + // Open array for writing. + CApiArray array(context(), array_name_.c_str(), TILEDB_WRITE); + + const auto dimensions = fragment.dimensions(); + const auto attributes = fragment.attributes(); + + // make field size locations + auto dimension_sizes = + templates::query::make_field_sizes(dimensions); + auto attribute_sizes = + templates::query::make_field_sizes(attributes); + + // Create the query. + tiledb_query_t* query; + auto rc = tiledb_query_alloc(context(), array, TILEDB_WRITE, &query); + ASSERTER(rc == TILEDB_OK); + rc = tiledb_query_set_layout(context(), query, TILEDB_UNORDERED); + ASSERTER(rc == TILEDB_OK); + + // add dimensions to query + templates::query::set_fields( + context(), query, dimension_sizes, dimensions, [](unsigned d) { + return "d" + std::to_string(d + 1); + }); + + // add attributes to query + templates::query::set_fields( + context(), query, attribute_sizes, attributes, [](unsigned a) { + return "a" + std::to_string(a + 1); + }); + + // Submit query. + rc = tiledb_query_submit(context(), query); + ASSERTER("" == error_if_any(rc)); + + // check that sizes match what we expect + const uint64_t expect_num_cells = fragment.size(); + const uint64_t dim_num_cells = + templates::query::num_cells(dimensions, dimension_sizes); + const uint64_t att_num_cells = + templates::query::num_cells(attributes, attribute_sizes); + + ASSERTER(dim_num_cells == expect_num_cells); + ASSERTER(att_num_cells == expect_num_cells); // Clean up. - tiledb_array_free(&array); tiledb_query_free(&query); } @@ -273,30 +664,31 @@ void CSparseGlobalOrderFx::write_1d_fragment_strings( uint64_t* offsets_size) { // Open array for writing. tiledb_array_t* array; - auto rc = tiledb_array_alloc(ctx_, array_name_.c_str(), &array); + auto rc = tiledb_array_alloc(context(), array_name_.c_str(), &array); REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_open(ctx_, array, TILEDB_WRITE); + rc = tiledb_array_open(context(), array, TILEDB_WRITE); REQUIRE(rc == TILEDB_OK); // Create the query. tiledb_query_t* query; - rc = tiledb_query_alloc(ctx_, array, TILEDB_WRITE, &query); + rc = tiledb_query_alloc(context(), array, TILEDB_WRITE, &query); REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_layout(ctx_, query, TILEDB_UNORDERED); + rc = tiledb_query_set_layout(context(), query, TILEDB_UNORDERED); REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "a", data, data_size); + rc = tiledb_query_set_data_buffer(context(), query, "a", data, data_size); REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_offsets_buffer(ctx_, query, "a", offsets, offsets_size); + rc = tiledb_query_set_offsets_buffer( + context(), query, "a", offsets, offsets_size); REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "d", coords, coords_size); + rc = tiledb_query_set_data_buffer(context(), query, "d", coords, coords_size); REQUIRE(rc == TILEDB_OK); // Submit query. - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); REQUIRE(rc == TILEDB_OK); // Close array. - rc = tiledb_array_close(ctx_, array); + rc = tiledb_array_close(context(), array); REQUIRE(rc == TILEDB_OK); // Clean up. @@ -308,32 +700,32 @@ void CSparseGlobalOrderFx::write_delete_condition( char* value_to_delete, uint64_t value_size) { // Open array for delete. tiledb_array_t* array; - auto rc = tiledb_array_alloc(ctx_, array_name_.c_str(), &array); + auto rc = tiledb_array_alloc(context(), array_name_.c_str(), &array); REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_open(ctx_, array, TILEDB_DELETE); + rc = tiledb_array_open(context(), array, TILEDB_DELETE); REQUIRE(rc == TILEDB_OK); // Create the query. tiledb_query_t* query; - rc = tiledb_query_alloc(ctx_, array, TILEDB_DELETE, &query); + rc = tiledb_query_alloc(context(), array, TILEDB_DELETE, &query); REQUIRE(rc == TILEDB_OK); // Add condition. tiledb_query_condition_t* qc; - rc = tiledb_query_condition_alloc(ctx_, &qc); + rc = tiledb_query_condition_alloc(context(), &qc); CHECK(rc == TILEDB_OK); rc = tiledb_query_condition_init( - ctx_, qc, "a", value_to_delete, value_size, TILEDB_EQ); + context(), qc, "a", value_to_delete, value_size, TILEDB_EQ); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_condition(ctx_, query, qc); + rc = tiledb_query_set_condition(context(), query, qc); CHECK(rc == TILEDB_OK); // Submit query. - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); REQUIRE(rc == TILEDB_OK); // Close array. - rc = tiledb_array_close(ctx_, array); + rc = tiledb_array_close(context(), array); REQUIRE(rc == TILEDB_OK); // Clean up. @@ -350,81 +742,74 @@ int32_t CSparseGlobalOrderFx::read( int* data, uint64_t* data_size, tiledb_query_t** query_ret, - tiledb_array_t** array_ret, + CApiArray* array_ret, std::vector subarray) { // Open array for reading. - tiledb_array_t* array; - auto rc = tiledb_array_alloc(ctx_, array_name_.c_str(), &array); - CHECK(rc == TILEDB_OK); - rc = tiledb_array_open(ctx_, array, TILEDB_READ); - CHECK(rc == TILEDB_OK); + CApiArray array(context(), array_name_.c_str(), TILEDB_READ); // Create query. tiledb_query_t* query; - rc = tiledb_query_alloc(ctx_, array, TILEDB_READ, &query); + auto rc = tiledb_query_alloc(context(), array, TILEDB_READ, &query); CHECK(rc == TILEDB_OK); if (set_subarray) { // Set subarray. tiledb_subarray_t* sub; - rc = tiledb_subarray_alloc(ctx_, array, &sub); + rc = tiledb_subarray_alloc(context(), array, &sub); CHECK(rc == TILEDB_OK); - rc = tiledb_subarray_set_subarray(ctx_, sub, subarray.data()); + rc = tiledb_subarray_set_subarray(context(), sub, subarray.data()); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_subarray_t(ctx_, query, sub); + rc = tiledb_query_set_subarray_t(context(), query, sub); CHECK(rc == TILEDB_OK); tiledb_subarray_free(&sub); } if (qc_idx != 0) { tiledb_query_condition_t* query_condition = nullptr; - rc = tiledb_query_condition_alloc(ctx_, &query_condition); + rc = tiledb_query_condition_alloc(context(), &query_condition); CHECK(rc == TILEDB_OK); if (qc_idx == 1) { int32_t val = 11; rc = tiledb_query_condition_init( - ctx_, query_condition, "a", &val, sizeof(int32_t), TILEDB_LT); + context(), query_condition, "a", &val, sizeof(int32_t), TILEDB_LT); CHECK(rc == TILEDB_OK); } else if (qc_idx == 2) { // Negated query condition should produce the same results. int32_t val = 11; tiledb_query_condition_t* qc; - rc = tiledb_query_condition_alloc(ctx_, &qc); + rc = tiledb_query_condition_alloc(context(), &qc); CHECK(rc == TILEDB_OK); rc = tiledb_query_condition_init( - ctx_, qc, "a", &val, sizeof(int32_t), TILEDB_GE); + context(), qc, "a", &val, sizeof(int32_t), TILEDB_GE); CHECK(rc == TILEDB_OK); - rc = tiledb_query_condition_negate(ctx_, qc, &query_condition); + rc = tiledb_query_condition_negate(context(), qc, &query_condition); CHECK(rc == TILEDB_OK); tiledb_query_condition_free(&qc); } - rc = tiledb_query_set_condition(ctx_, query, query_condition); + rc = tiledb_query_set_condition(context(), query, query_condition); CHECK(rc == TILEDB_OK); tiledb_query_condition_free(&query_condition); } - rc = tiledb_query_set_layout(ctx_, query, TILEDB_GLOBAL_ORDER); + rc = tiledb_query_set_layout(context(), query, TILEDB_GLOBAL_ORDER); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "a", data, data_size); + rc = tiledb_query_set_data_buffer(context(), query, "a", data, data_size); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "d", coords, coords_size); + rc = tiledb_query_set_data_buffer(context(), query, "d", coords, coords_size); CHECK(rc == TILEDB_OK); // Submit query. - auto ret = tiledb_query_submit(ctx_, query); + auto ret = tiledb_query_submit(context(), query); if (query_ret == nullptr || array_ret == nullptr) { - // Clean up. - rc = tiledb_array_close(ctx_, array); - CHECK(rc == TILEDB_OK); - tiledb_array_free(&array); + // Clean up (RAII will do it for the array) tiledb_query_free(&query); } else { *query_ret = query; - *array_ret = array; + new (array_ret) CApiArray(std::move(array)); } return ret; @@ -443,42 +828,43 @@ int32_t CSparseGlobalOrderFx::read_strings( std::vector subarray) { // Open array for reading. tiledb_array_t* array; - auto rc = tiledb_array_alloc(ctx_, array_name_.c_str(), &array); + auto rc = tiledb_array_alloc(context(), array_name_.c_str(), &array); CHECK(rc == TILEDB_OK); - rc = tiledb_array_open(ctx_, array, TILEDB_READ); + rc = tiledb_array_open(context(), array, TILEDB_READ); CHECK(rc == TILEDB_OK); // Create query. tiledb_query_t* query; - rc = tiledb_query_alloc(ctx_, array, TILEDB_READ, &query); + rc = tiledb_query_alloc(context(), array, TILEDB_READ, &query); CHECK(rc == TILEDB_OK); if (set_subarray) { // Set subarray. tiledb_subarray_t* sub; - rc = tiledb_subarray_alloc(ctx_, array, &sub); + rc = tiledb_subarray_alloc(context(), array, &sub); CHECK(rc == TILEDB_OK); - rc = tiledb_subarray_set_subarray(ctx_, sub, subarray.data()); + rc = tiledb_subarray_set_subarray(context(), sub, subarray.data()); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_subarray_t(ctx_, query, sub); + rc = tiledb_query_set_subarray_t(context(), query, sub); CHECK(rc == TILEDB_OK); tiledb_subarray_free(&sub); } - rc = tiledb_query_set_layout(ctx_, query, TILEDB_GLOBAL_ORDER); + rc = tiledb_query_set_layout(context(), query, TILEDB_GLOBAL_ORDER); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "a", data, data_size); + rc = tiledb_query_set_data_buffer(context(), query, "a", data, data_size); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_offsets_buffer(ctx_, query, "a", offsets, offsets_size); + rc = tiledb_query_set_offsets_buffer( + context(), query, "a", offsets, offsets_size); CHECK(rc == TILEDB_OK); - rc = tiledb_query_set_data_buffer(ctx_, query, "d", coords, coords_size); + rc = tiledb_query_set_data_buffer(context(), query, "d", coords, coords_size); CHECK(rc == TILEDB_OK); // Submit query. - auto ret = tiledb_query_submit(ctx_, query); + auto ret = tiledb_query_submit(context(), query); if (query_ret == nullptr || array_ret == nullptr) { // Clean up. - rc = tiledb_array_close(ctx_, array); + rc = tiledb_array_close(context(), array); CHECK(rc == TILEDB_OK); tiledb_array_free(&array); tiledb_query_free(&query); @@ -490,6 +876,160 @@ int32_t CSparseGlobalOrderFx::read_strings( return ret; } +/** + * Determines whether the array fragments are "too wide" to merge with the given + * memory budget. "Too wide" means that there are overlapping tiles from + * different fragments to fit in memory, so the merge cannot progress without + * producing out-of-order data*. + * + * *there are ways to get around this but they are not implemented. + * + * An answer cannot be determined if the tile MBR lower bounds within a fragment + * are not in order. This is common for 2+ dimensions, so this won't even try. + * + * @return std::nullopt if an answer cannot be determined, true/false if it can + */ +template +static std::optional can_complete_in_memory_budget( + tiledb_ctx_t* ctx, const char* array_uri, const Instance& instance) { + if constexpr (std::tuple_size_v > 1) { + // don't even bother + return std::nullopt; + } + + CApiArray array(ctx, array_uri, TILEDB_READ); + + const auto& fragment_metadata = array->array()->fragment_metadata(); + + for (const auto& fragment : fragment_metadata) { + const_cast(fragment.get()) + ->loaded_metadata() + ->load_rtree(array->array()->get_encryption_key()); + } + + auto tiles_size = [&](unsigned f, uint64_t t) { + using BitmapType = uint8_t; + + size_t data_size = 0; + for (size_t d = 0; + d < std::tuple_size::value; + d++) { + data_size += + fragment_metadata[f]->tile_size("d" + std::to_string(d + 1), t); + } + + const size_t rt_size = sizeof(sm::GlobalOrderResultTile); + const size_t subarray_size = + (instance.subarray.empty() ? + 0 : + fragment_metadata[f]->cell_num(t) * sizeof(BitmapType)); + return data_size + rt_size + subarray_size; + }; + + const auto& domain = array->array()->array_schema_latest().domain(); + sm::GlobalCellCmp globalcmp(domain); + stdx::reverse_comparator reverseglobalcmp(globalcmp); + + using RT = sm::ResultTileId; + + auto cmp_pq_lower_bound = [&](const RT& rtl, const RT& rtr) { + const sm::RangeLowerBound l_mbr = { + .mbr = fragment_metadata[rtl.fragment_idx_]->mbr(rtl.tile_idx_)}; + const sm::RangeLowerBound r_mbr = { + .mbr = fragment_metadata[rtr.fragment_idx_]->mbr(rtr.tile_idx_)}; + return reverseglobalcmp(l_mbr, r_mbr); + }; + auto cmp_pq_upper_bound = [&](const RT& rtl, const RT& rtr) { + const sm::RangeUpperBound l_mbr = { + .mbr = fragment_metadata[rtl.fragment_idx_]->mbr(rtl.tile_idx_)}; + const sm::RangeUpperBound r_mbr = { + .mbr = fragment_metadata[rtr.fragment_idx_]->mbr(rtr.tile_idx_)}; + return reverseglobalcmp(l_mbr, r_mbr); + }; + + /** + * Simulates comparison of a loaded tile to the merge bound. + * + * @return true if `rtl.upper < rtr.lower`, i.e. we can process + * the entirety of `rtl` before any of `rtr` + */ + auto cmp_merge_bound = [&](const RT& rtl, const RT& rtr) { + const auto rtl_mbr_clamped = instance.clamp( + fragment_metadata[rtl.fragment_idx_]->mbr(rtl.tile_idx_)); + const sm::RangeUpperBound l_mbr = {.mbr = rtl_mbr_clamped}; + const sm::RangeLowerBound r_mbr = { + .mbr = fragment_metadata[rtr.fragment_idx_]->mbr(rtr.tile_idx_)}; + + const int cmp = globalcmp.compare(l_mbr, r_mbr); + if (instance.allow_duplicates()) { + return cmp <= 0; + } else { + return cmp < 0; + } + }; + + // order all tiles on their lower bound + std::priority_queue, decltype(cmp_pq_lower_bound)> + mbr_lower_bound(cmp_pq_lower_bound); + for (unsigned f = 0; f < instance.fragments.size(); f++) { + for (uint64_t t = 0; t < fragment_metadata[f]->tile_num(); t++) { + if (instance.intersects(fragment_metadata[f]->mbr(t))) { + mbr_lower_bound.push(sm::ResultTileId(f, t)); + } + } + } + + // and track tiles which are active using their upper bound + std::priority_queue, decltype(cmp_pq_upper_bound)> + mbr_upper_bound(cmp_pq_upper_bound); + + // there must be some point where the tiles have overlapping MBRs + // and take more memory + const uint64_t coords_budget = std::stoi(instance.memory.total_budget_) * + std::stod(instance.memory.ratio_coords_); + /* + * Iterate through the tiles in the same order that the sparse + * reader would process them in, tracking memory usage as we go. + */ + const uint64_t num_tiles = mbr_lower_bound.size(); + uint64_t active_tile_size = sizeof(RT) * num_tiles; + uint64_t next_tile_size = 0; + while (active_tile_size + next_tile_size < coords_budget && + !mbr_lower_bound.empty()) { + RT next_rt; + + // add new result tiles + while (!mbr_lower_bound.empty()) { + next_rt = mbr_lower_bound.top(); + + next_tile_size = tiles_size(next_rt.fragment_idx_, next_rt.tile_idx_); + if (active_tile_size + next_tile_size <= coords_budget) { + mbr_lower_bound.pop(); + active_tile_size += next_tile_size; + mbr_upper_bound.push(next_rt); + } else { + break; + } + } + + if (mbr_lower_bound.empty()) { + break; + } + + // emit from created result tiles, removing any which are exhausted + next_rt = mbr_lower_bound.top(); + while (!mbr_upper_bound.empty() && + cmp_merge_bound(mbr_upper_bound.top(), next_rt)) { + auto finish_rt = mbr_upper_bound.top(); + mbr_upper_bound.pop(); + active_tile_size -= + tiles_size(finish_rt.fragment_idx_, finish_rt.tile_idx_); + } + } + + return mbr_lower_bound.empty(); +} + /* ********************************* */ /* TESTS */ /* ********************************* */ @@ -511,8 +1051,8 @@ TEST_CASE_METHOD( // We should have one tile range (size 16) which will be bigger than budget // (10). - total_budget_ = "1000"; - ratio_tile_ranges_ = "0.01"; + memory_.total_budget_ = "1000"; + memory_.ratio_tile_ranges_ = "0.01"; update_config(); // Try to read. @@ -525,7 +1065,7 @@ TEST_CASE_METHOD( // Check we hit the correct error. tiledb_error_t* error = NULL; - rc = tiledb_ctx_get_last_error(ctx_, &error); + rc = tiledb_ctx_get_last_error(context(), &error); CHECK(rc == TILEDB_OK); const char* msg; @@ -564,11 +1104,11 @@ TEST_CASE_METHOD( // Specific relationship for failure not known, but these values // will result in failure with data being written. - total_budget_ = "10000"; + memory_.total_budget_ = "10000"; // Failure here occurs with the value of 0.1 for ratio_tile_ranges_. update_config(); - tiledb_array_t* array = nullptr; + CApiArray array; tiledb_query_t* query = nullptr; // Try to read. @@ -601,9 +1141,9 @@ TEST_CASE_METHOD( } // Check incomplete query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); if (status == TILEDB_INCOMPLETE) { - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); CHECK(rc == TILEDB_OK); } } while (status == TILEDB_INCOMPLETE); @@ -653,11 +1193,11 @@ TEST_CASE_METHOD( // specific relationship for failure not known, but these values // will result in failure with data being written. - total_budget_ = "15000"; + memory_.total_budget_ = "15000"; // Failure here occurs with the value of 0.1 for ratio_tile_ranges_. update_config(); - tiledb_array_t* array = nullptr; + CApiArray array; tiledb_query_t* query = nullptr; // Try to read. @@ -690,9 +1230,9 @@ TEST_CASE_METHOD( } // Check incomplete query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); if (status == TILEDB_INCOMPLETE) { - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); CHECK(rc == TILEDB_OK); } } while (status == TILEDB_INCOMPLETE); @@ -705,6 +1245,669 @@ TEST_CASE_METHOD( CHECK(retrieved_data == expected_correct_data); } +/** + * Tests that the reader will not yield results out of order across multiple + * iterations or `submit`s if the fragments are heavily skewed when the memory + * budget is heavily constrained. + * + * e.g. two fragments + * F0: 1-1000,1001-2000,2001-3000 + * F1: 2001-3000 + * + * If the memory budget allows only one tile per fragment at a time then there + * must be a mechanism for emitting (F0, T1) before (F1, T0) even though the + * the memory budget might not process them in the same loop. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: fragment skew", + "[sparse-global-order][rest][rapidcheck]") { + auto doit = [this]( + size_t fragment_size, + size_t num_user_cells, + int extent, + const std::vector>& subarray = + std::vector>()) { + // Write a fragment F0 with unique coordinates + templates::Fragment1D fragment0; + fragment0.dim_.resize(fragment_size); + std::iota(fragment0.dim_.begin(), fragment0.dim_.end(), 1); + + // Write a fragment F1 with lots of duplicates + // [100,100,100,100,100,101,101,101,101,101,102,102,102,102,102,...] + templates::Fragment1D fragment1; + fragment1.dim_.resize(fragment0.dim_.size()); + for (size_t i = 0; i < fragment1.dim_.size(); i++) { + fragment1.dim_[i] = + static_cast((i / 10) + (fragment0.dim_.size() / 2)); + } + + // atts are whatever + auto& f0atts = std::get<0>(fragment0.atts_); + f0atts.resize(fragment0.dim_.size()); + std::iota(f0atts.begin(), f0atts.end(), 0); + + auto& f1atts = std::get<0>(fragment1.atts_); + f1atts.resize(fragment1.dim_.size()); + std::iota(f1atts.begin(), f1atts.end(), int(fragment0.dim_.size())); + + struct FxRun1D instance; + instance.fragments.push_back(fragment0); + instance.fragments.push_back(fragment1); + instance.num_user_cells = num_user_cells; + + instance.array.dimension_.extent = extent; + instance.array.allow_dups_ = true; + + instance.memory.total_budget_ = "20000"; + instance.memory.ratio_array_data_ = "0.5"; + + instance.subarray = subarray; + + run(instance); + }; + + SECTION("Example") { + doit.operator()(200, 8, 2); + } + + SECTION("Shrink", "Some examples found by rapidcheck") { + doit.operator()( + 2, 1, 1, {templates::Domain(1, 1)}); + doit.operator()( + 2, 1, 1, {templates::Domain(1, 2)}); + doit.operator()( + 39, 1, 1, {templates::Domain(20, 21)}); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck fragment skew", [doit]() { + const size_t fragment_size = *rc::gen::inRange(2, 200); + const size_t num_user_cells = *rc::gen::inRange(1, 1024); + const int extent = *rc::gen::inRange(1, 200); + const auto subarray = + *rc::make_subarray_1d(templates::Domain(1, 200)); + doit.operator()( + fragment_size, num_user_cells, extent, subarray); + }); + } +} + +/** + * Tests that the reader will not yield results out of order across multiple + * iterations or `submit`s if the tile MBRs across different fragments are + * interleaved. + * + * The test sets up data with two fragments so that each tile overlaps with + * two tiles from the other fragment. This way when the tiles are arranged + * in global order the only way to ensure that we don't emit out of order + * results with a naive implementation is to have *all* the tiles loaded + * in one pass, which is not practical. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: fragment interleave", + "[sparse-global-order][rest][rapidcheck]") { + // NB: the tile extent is 2 + auto doit = [this]( + size_t fragment_size, + size_t num_user_cells, + const std::vector>& subarray = {}) { + templates::Fragment1D fragment0; + templates::Fragment1D fragment1; + + // Write a fragment F0 with tiles [1,3][3,5][5,7][7,9]... + fragment0.dim_.resize(fragment_size); + fragment0.dim_[0] = 1; + for (size_t i = 1; i < fragment0.dim_.size(); i++) { + fragment0.dim_[i] = static_cast(1 + 2 * ((i + 1) / 2)); + } + + // Write a fragment F1 with tiles [2,4][4,6][6,8][8,10]... + fragment1.dim_.resize(fragment0.dim_.size()); + for (size_t i = 0; i < fragment1.dim_.size(); i++) { + fragment1.dim_[i] = fragment0.dim_[i] + 1; + } + + // atts don't really matter + auto& f0atts = std::get<0>(fragment0.atts_); + f0atts.resize(fragment0.dim_.size()); + std::iota(f0atts.begin(), f0atts.end(), 0); + + auto& f1atts = std::get<0>(fragment1.atts_); + f1atts.resize(fragment1.dim_.size()); + std::iota(f1atts.begin(), f1atts.end(), int(f0atts.size())); + + struct FxRun1D instance; + instance.fragments.push_back(fragment0); + instance.fragments.push_back(fragment1); + instance.num_user_cells = num_user_cells; + instance.subarray = subarray; + instance.memory.total_budget_ = "20000"; + instance.memory.ratio_array_data_ = "0.5"; + instance.array.allow_dups_ = true; + + run(instance); + }; + + SECTION("Example") { + doit.operator()(196, 8); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck fragment interleave", [doit]() { + const size_t fragment_size = *rc::gen::inRange(2, 196); + const size_t num_user_cells = *rc::gen::inRange(1, 1024); + const auto subarray = + *rc::make_subarray_1d(templates::Domain(1, 200)); + doit.operator()( + fragment_size, num_user_cells, subarray); + }); + } +} + +/** + * Tests that the reader correctly returns an error if it cannot + * make progress due to being unable to make progress. + * The reader can stall if there is a point where F fragments + * fit in memory, but `F + 1` or more fragments overlap + * + * For example, suppose 2 tiles fit in memory. If the tiles are: + * [0, 1, 2, 3, 4] + * [1, 2, 3, 4, 5] + * [2, 3, 4, 5, 6] + * + * Then we will process the first two tiles first after ordering + * them on their lower bound. We cannot emit anything past "2" + * because it would be out of order with the third tile. + * + * After the first iteration, the state is + * [_, _, _, 3, 4] + * [_, _, 3, 4, 5] + * [2, 3, 4, 5, 6] + * + * We could make progress by un-loading a tile and then loading + * the third tile, but instead we will just error out because + * that's complicated. + * + * The user's recourse to that is to either: + * 1) increase memory budget; or + * 2) consolidate some fragments. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: fragment wide overlap", + "[sparse-global-order][rest][rapidcheck]") { + auto doit = [this]( + size_t num_fragments, + size_t fragment_size, + size_t num_user_cells, + bool allow_dups, + const std::vector>& subarray = + std::vector>()) { + const uint64_t total_budget = 100000; + const double ratio_coords = 0.2; + + FxRun1D instance; + instance.array.capacity_ = num_fragments * 2; + instance.array.dimension_.extent = int(num_fragments) * 2; + instance.array.allow_dups_ = allow_dups; + instance.subarray = subarray; + + instance.memory.total_budget_ = std::to_string(total_budget); + instance.memory.ratio_coords_ = std::to_string(ratio_coords); + instance.memory.ratio_array_data_ = "0.6"; + + for (size_t f = 0; f < num_fragments; f++) { + templates::Fragment1D fragment; + fragment.dim_.resize(fragment_size); + std::iota( + fragment.dim_.begin(), + fragment.dim_.end(), + instance.array.dimension_.domain.lower_bound + static_cast(f)); + + auto& atts = std::get<0>(fragment.atts_); + atts.resize(fragment_size); + std::iota( + atts.begin(), + atts.end(), + static_cast(fragment_size * num_fragments)); + + instance.fragments.push_back(fragment); + } + + instance.num_user_cells = num_user_cells; + + run(instance); + }; + + SECTION("Example") { + doit.operator()(16, 100, 64, true); + } + + SECTION("Shrink", "Some examples found by rapidcheck") { + doit.operator()( + 10, 2, 64, false, Subarray1DType{templates::Domain(1, 3)}); + doit.operator()( + 12, + 15, + 1024, + false, + Subarray1DType{templates::Domain(1, 12)}); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck fragment wide overlap", [doit]() { + const size_t num_fragments = *rc::gen::inRange(10, 24); + const size_t fragment_size = *rc::gen::inRange(2, 200 - 64); + const size_t num_user_cells = 1024; + const bool allow_dups = *rc::gen::arbitrary(); + const auto subarray = + *rc::make_subarray_1d(templates::Domain(1, 200)); + doit.operator()( + num_fragments, fragment_size, num_user_cells, allow_dups, subarray); + }); + } +} + +/** + * Tests that the reader will not yield duplicate coordinates if + * coordinates in loaded tiles are equal to the merge bound, + * and the merge bound is an actual datum in yet-to-be-loaded tiles. + * + * Example: + * many fragments which overlap at the ends + * [0, 1000], [1000, 2000], [2000, 3000] + * If we can load the tiles of the first two fragments but not + * the third, then the merge bound will be 2000. But it is only + * correct to emit the 2000 from the second fragment if + * duplicates are allowed. + * + * This test illustrates that the merge bound must not be an + * inclusive bound (unless duplicates are allowed). + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: merge bound duplication", + "[sparse-global-order][rest][rapidcheck]") { + auto doit = [this]( + size_t num_fragments, + size_t fragment_size, + size_t num_user_cells, + size_t tile_capacity, + bool allow_dups, + const Subarray1DType& subarray = {}) { + FxRun1D instance; + instance.num_user_cells = num_user_cells; + instance.subarray = subarray; + instance.memory.total_budget_ = "50000"; + instance.memory.ratio_array_data_ = "0.5"; + instance.array.capacity_ = tile_capacity; + instance.array.allow_dups_ = allow_dups; + instance.array.dimension_.domain.lower_bound = 0; + instance.array.dimension_.domain.upper_bound = + static_cast(num_fragments * fragment_size); + + for (size_t f = 0; f < num_fragments; f++) { + templates::Fragment1D fragment; + fragment.dim_.resize(fragment_size); + std::iota( + fragment.dim_.begin(), + fragment.dim_.end(), + static_cast(f * (fragment_size - 1))); + + auto& atts = std::get<0>(fragment.atts_); + atts.resize(fragment_size); + std::iota(atts.begin(), atts.end(), static_cast(f * fragment_size)); + + instance.fragments.push_back(fragment); + } + + instance.num_user_cells = num_user_cells; + + run(instance); + }; + + SECTION("Example") { + doit.operator()(16, 16, 1024, 16, false); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck merge bound duplication", [doit]() { + const size_t num_fragments = *rc::gen::inRange(4, 32); + const size_t fragment_size = *rc::gen::inRange(2, 256); + const size_t num_user_cells = 1024; + const size_t tile_capacity = *rc::gen::inRange(1, fragment_size); + const bool allow_dups = *rc::gen::arbitrary(); + const auto subarray = *rc::make_subarray_1d(templates::Domain( + 0, static_cast(num_fragments * fragment_size))); + doit.operator()( + num_fragments, + fragment_size, + num_user_cells, + tile_capacity, + allow_dups, + subarray); + }); + } +} + +/** + * Tests that the reader will not yield out of order when + * the tile MBRs in a fragment are not in the global order. + * + * The coordinates in the fragment are in global order, but + * the tile MBRs may not be. This is because the MBR uses + * the minimum coordinate value *in each dimension*, which + * for two dimensions and higher is not always the same as + * the minimum coordinate in the tile. + * + * In this test, we have tile extents of 4 in both dimensions. + * Let's say the attribute value is the index of the cell + * as enumerated in row-major order and we have a densely + * populated array. + * + * c-T1 c-T2 c-T3 + * | | | | + * ---+----+---+---+----+----+---+---+----+----+---+---+----+--- + * | 1 2 3 4 | 5 6 7 8 | 9 10 11 12 | + * r-T1 | 201 202 203 204 | 205 206 207 208 | 209 210 211 212 | ... + * | 401 402 403 404 | 405 406 407 408 | 409 410 411 412 | + * | 601 602 603 604 | 605 606 607 608 | 609 610 611 612 | + * ---+----+---+---+----+----+---+---+----+----+---+---+----+--- + * + * If the capacity is 17, then: + * Data tile 1 holds + * [1, 2, 3, 4, 201, 202, 203, 204, 401, 402, 403, 404, 601, 602, 603, 604, 5]. + * Data tile 2 holds + * [6, 7, 8, 205, 206, 207, 208, 405, 406, 407, 408, 605, 606, 607, 608, 9, 10]. + * + * Data tile 1 has a MBR of [(1, 1), (5, 4)]. + * Data tile 2 has a MBR of [(5, 1), (10, 4)]. + * + * The lower bound of data tile 2's MBR is less than the upper bound + * of data tile 1's MBR. Hence the coordinates of the tiles are in global + * order but the MBRs are not. + * + * In a non-dense setting, this happens if the data tile boundary + * happens in the middle of a space tile which has coordinates on both + * sides, AND the space tile after the boundary contains a coordinate + * which is lesser in the second dimension. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: out-of-order MBRs", + "[sparse-global-order][rest][rapidcheck]") { + auto doit = [this]( + tiledb_layout_t tile_order, + tiledb_layout_t cell_order, + size_t num_fragments, + size_t fragment_size, + size_t num_user_cells, + size_t tile_capacity, + bool allow_dups, + const Subarray2DType& subarray = {}) { + FxRun2D instance; + instance.num_user_cells = num_user_cells; + instance.capacity = tile_capacity; + instance.allow_dups = allow_dups; + instance.tile_order_ = tile_order; + instance.cell_order_ = cell_order; + instance.subarray = subarray; + + auto row = [&](size_t f, size_t i) -> int { + return 1 + + static_cast( + ((num_fragments * i) + f) / instance.d1.domain.upper_bound); + }; + auto col = [&](size_t f, size_t i) -> int { + return 1 + + static_cast( + ((num_fragments * i) + f) % instance.d1.domain.upper_bound); + }; + + for (size_t f = 0; f < num_fragments; f++) { + templates::Fragment2D fdata; + fdata.d1_.reserve(fragment_size); + fdata.d2_.reserve(fragment_size); + std::get<0>(fdata.atts_).reserve(fragment_size); + + for (size_t i = 0; i < fragment_size; i++) { + fdata.d1_.push_back(row(f, i)); + fdata.d2_.push_back(col(f, i)); + std::get<0>(fdata.atts_) + .push_back(static_cast(f * fragment_size + i)); + } + + instance.fragments.push_back(fdata); + } + + auto guard = run_create(instance); + + // validate that we have set up the condition we claim, + // i.e. some fragment has out-of-order MBRs + { + CApiArray array(context(), array_name_.c_str(), TILEDB_READ); + + const auto& fragment_metadata = array->array()->fragment_metadata(); + for (const auto& fragment : fragment_metadata) { + const_cast(fragment.get()) + ->loaded_metadata() + ->load_rtree(array->array()->get_encryption_key()); + } + + sm::GlobalCellCmp globalcmp( + array->array()->array_schema_latest().domain()); + + // check that we actually have out-of-order MBRs + // (disable on REST where we have no metadata) + // (disable with rapidcheck where this may not be guaranteed) + if (!vfs_test_setup_.is_rest() && + std::is_same::value) { + bool any_out_of_order = false; + for (size_t f = 0; !any_out_of_order && f < fragment_metadata.size(); + f++) { + for (size_t t = 1; + !any_out_of_order && t < fragment_metadata[f]->tile_num(); + t++) { + const sm::RangeUpperBound lt = { + .mbr = fragment_metadata[f]->mbr(t - 1)}; + const sm::RangeLowerBound rt = { + .mbr = fragment_metadata[f]->mbr(t)}; + if (globalcmp(rt, lt)) { + any_out_of_order = true; + } + } + } + ASSERTER(any_out_of_order); + } + } + + run_execute(instance); + }; + + SECTION("Example") { + doit.operator()( + TILEDB_ROW_MAJOR, TILEDB_ROW_MAJOR, 4, 100, 32, 6, false); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck out-of-order MBRs", [doit](bool allow_dups) { + const auto tile_order = + *rc::gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + const auto cell_order = + *rc::gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + const size_t num_fragments = *rc::gen::inRange(2, 8); + const size_t fragment_size = *rc::gen::inRange(32, 400); + const size_t num_user_cells = *rc::gen::inRange(1, 1024); + const size_t tile_capacity = *rc::gen::inRange(4, 32); + const auto subarray = *rc::make_subarray_2d( + templates::Domain(1, 200), templates::Domain(1, 200)); + + doit.operator()( + tile_order, + cell_order, + num_fragments, + fragment_size, + num_user_cells, + tile_capacity, + allow_dups, + subarray); + }); + } +} + +/** + * Similar to the "fragment skew" test, but in two dimensions. + * In 2+ dimensions the lower bounds of the tile MBRs are not + * necessarily in order for each fragment. + * + * This test verifies the need to set the merge bound to + * the MBR lower bound of fragments which haven't had any tiles loaded. + * + * Let's use an example with dimension extents and tile capacities of 4. + * + * c-T1 c-T2 c-T3 + * | | | | + * ---+----+---+---+----+----+---+---+----+----+---+---+----+--- + * | 1 1 1 1 | 1 | 1 | + * r-T1 | 1 1 1 1 | | | + * | 1 1 1 1 | | | + * | 1 1 | 2 1 1 | | + * ---+----+---+---+----+----+---+---+----+----+---+---+----+--- + * + * Fragment 1 MBR lower bounds: [(1, 1), (2, 1), (3, 1), (1, 5), (1, 7)] + * Fragment 2 MBR lower bounds: [(4, 6)] + * + * What if the memory budget here permits loading just four tiles? + * + * The un-loaded tiles have bounds (1, 7) and (4, 6), but the + * coordinate from (4, 6) has a lesser value. The merge bound + * is necessary to ensure we don't emit out-of-order coordinates. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: fragment skew 2d merge bound", + "[sparse-global-order][rest][rapidcheck]") { + auto doit = [this]( + tiledb_layout_t tile_order, + tiledb_layout_t cell_order, + size_t approximate_memory_tiles, + size_t num_user_cells, + size_t num_fragments, + size_t tile_capacity, + bool allow_dups, + const Subarray2DType& subarray = {}) { + FxRun2D instance; + instance.tile_order_ = tile_order; + instance.cell_order_ = cell_order; + instance.d1.extent = 4; + instance.d2.extent = 4; + instance.capacity = tile_capacity; + instance.allow_dups = allow_dups; + instance.num_user_cells = num_user_cells; + instance.subarray = subarray; + + const size_t tile_size_estimate = (1600 + instance.capacity * sizeof(int)); + const double coords_ratio = + (1.02 / (std::stold(instance.memory.total_budget_) / + tile_size_estimate / approximate_memory_tiles)); + instance.memory.ratio_coords_ = std::to_string(coords_ratio); + + int att = 0; + + // for each fragment, make one fragment which is just a point + // so that its MBR is equal to its coordinate (and thus more likely + // to be out of order with respect to the MBRs of tiles from other + // fragments) + for (size_t f = 0; f < num_fragments; f++) { + FxRun2D::FragmentType fdata; + // make one mostly dense space tile + const int trow = instance.d1.domain.lower_bound + + static_cast(f * instance.d1.extent); + const int tcol = instance.d2.domain.lower_bound + + static_cast(f * instance.d2.extent); + for (int i = 0; i < instance.d1.extent * instance.d2.extent - 2; i++) { + fdata.d1_.push_back(trow + i / instance.d1.extent); + fdata.d2_.push_back(tcol + i % instance.d1.extent); + std::get<0>(fdata.atts_).push_back(att++); + } + + // then some sparse coords in the next space tile, + // fill the data tile (if the capacity is 4), we'll call it T + fdata.d1_.push_back(trow); + fdata.d2_.push_back(tcol + instance.d2.extent); + std::get<0>(fdata.atts_).push_back(att++); + fdata.d1_.push_back(trow + instance.d1.extent - 1); + fdata.d2_.push_back(tcol + instance.d2.extent + 2); + std::get<0>(fdata.atts_).push_back(att++); + + // then begin a new data tile "Tnext" which straddles the bounds of that + // space tile. this will have a low MBR. + fdata.d1_.push_back(trow + instance.d1.extent - 1); + fdata.d2_.push_back(tcol + instance.d2.extent + 3); + std::get<0>(fdata.atts_).push_back(att++); + fdata.d1_.push_back(trow); + fdata.d2_.push_back(tcol + 2 * instance.d2.extent); + std::get<0>(fdata.atts_).push_back(att++); + + // then add a point P which is less than the lower bound of Tnext's MBR, + // and also between the last two coordinates of T + FxRun2D::FragmentType fpoint; + fpoint.d1_.push_back(trow + instance.d1.extent - 1); + fpoint.d2_.push_back(tcol + instance.d1.extent + 1); + std::get<0>(fpoint.atts_).push_back(att++); + + instance.fragments.push_back(fdata); + instance.fragments.push_back(fpoint); + } + + run(instance); + }; + + SECTION("Example") { + doit.operator()( + TILEDB_ROW_MAJOR, TILEDB_ROW_MAJOR, 4, 1024, 1, 4, false); + } + + SECTION("Shrink", "Some examples found by rapidcheck") { + doit.operator()( + TILEDB_ROW_MAJOR, + TILEDB_ROW_MAJOR, + 2, + 9, + 1, + 2, + false, + {std::make_pair(templates::Domain(2, 2), std::nullopt)}); + } + + SECTION("Rapidcheck") { + rc::prop("rapidcheck fragment skew", [doit]() { + const auto tile_order = + *rc::gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + const auto cell_order = + *rc::gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + const size_t approximate_memory_tiles = *rc::gen::inRange(2, 64); + const size_t num_user_cells = *rc::gen::inRange(1, 1024); + const size_t num_fragments = *rc::gen::inRange(1, 8); + const size_t tile_capacity = *rc::gen::inRange(1, 16); + const bool allow_dups = *rc::gen::arbitrary(); + const auto subarray = *rc::make_subarray_2d( + templates::Domain(1, 200), templates::Domain(1, 200)); + doit.operator()( + tile_order, + cell_order, + approximate_memory_tiles, + num_user_cells, + num_fragments, + tile_capacity, + allow_dups, + subarray); + }); + } +} + TEST_CASE_METHOD( CSparseGlobalOrderFx, "Sparse global order reader: tile offsets budget exceeded", @@ -726,8 +1929,8 @@ TEST_CASE_METHOD( // We should have 100 tiles (tile offset size 800) which will be bigger than // leftover budget. - total_budget_ = "3000"; - ratio_array_data_ = "0.5"; + memory_.total_budget_ = "3000"; + memory_.ratio_array_data_ = "0.5"; update_config(); // Try to read. @@ -740,7 +1943,7 @@ TEST_CASE_METHOD( // Check we hit the correct error. tiledb_error_t* error = NULL; - rc = tiledb_ctx_get_last_error(ctx_, &error); + rc = tiledb_ctx_get_last_error(context(), &error); CHECK(rc == TILEDB_OK); const char* msg; @@ -762,10 +1965,10 @@ TEST_CASE_METHOD( create_default_array_1d(); bool use_subarray = false; - SECTION("- No subarray") { + SECTION("No subarray") { use_subarray = false; } - SECTION("- Subarray") { + SECTION("Subarray") { use_subarray = true; } @@ -789,13 +1992,14 @@ TEST_CASE_METHOD( write_1d_fragment(coords, &coords_size, data, &data_size); } + // FIXME: there is no per fragment budget anymore // Two result tile (2 * (~3000 + 8) will be bigger than the per fragment // budget (1000). - total_budget_ = "35000"; - ratio_coords_ = "0.11"; + memory_.total_budget_ = "35000"; + memory_.ratio_coords_ = "0.11"; update_config(); - tiledb_array_t* array = nullptr; + CApiArray array; tiledb_query_t* query = nullptr; uint32_t rc; @@ -828,7 +2032,7 @@ TEST_CASE_METHOD( // Check incomplete query status. tiledb_query_status_t status; - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_COMPLETED); CHECK(40 == data_r_size); @@ -840,9 +2044,8 @@ TEST_CASE_METHOD( CHECK(!std::memcmp(data_c, data_r, data_r_size)); // Clean up. - rc = tiledb_array_close(ctx_, array); + rc = tiledb_array_close(context(), array); CHECK(rc == TILEDB_OK); - tiledb_array_free(&array); tiledb_query_free(&query); } @@ -870,8 +2073,8 @@ TEST_CASE_METHOD( write_1d_fragment(coords, &coords_size, data, &data_size); // One result tile (8 + ~440) will be bigger than the budget (400). - total_budget_ = "19000"; - ratio_coords_ = "0.04"; + memory_.total_budget_ = "19000"; + memory_.ratio_coords_ = "0.04"; update_config(); // Try to read. @@ -885,7 +2088,7 @@ TEST_CASE_METHOD( // Check we hit the correct error. tiledb_error_t* error = NULL; - rc = tiledb_ctx_get_last_error(ctx_, &error); + rc = tiledb_ctx_get_last_error(context(), &error); CHECK(rc == TILEDB_OK); const char* msg; @@ -907,27 +2110,27 @@ TEST_CASE_METHOD( bool use_subarray = false; int tile_idx = 0; int qc_idx = GENERATE(1, 2); - SECTION("- No subarray") { + SECTION("No subarray") { use_subarray = false; - SECTION("- First tile") { + SECTION("First tile") { tile_idx = 0; } - SECTION("- Second tile") { + SECTION("Second tile") { tile_idx = 1; } - SECTION("- Last tile") { + SECTION("Last tile") { tile_idx = 2; } } - SECTION("- Subarray") { + SECTION("Subarray") { use_subarray = true; - SECTION("- First tile") { + SECTION("First tile") { tile_idx = 0; } - SECTION("- Second tile") { + SECTION("Second tile") { tile_idx = 1; } - SECTION("- Last tile") { + SECTION("Last tile") { tile_idx = 2; } } @@ -944,7 +2147,7 @@ TEST_CASE_METHOD( uint64_t coords_size = sizeof(coords_1); uint64_t data_size = sizeof(data_1); - // Create the aray so the removed tile is at the correct index. + // Create the array so the removed tile is at the correct index. switch (tile_idx) { case 0: write_1d_fragment(coords_3, &coords_size, data_3, &data_size); @@ -998,7 +2201,7 @@ TEST_CASE_METHOD( bool extra_fragment = GENERATE(true, false); int qc_idx = GENERATE(1, 2); - create_default_array_1d(dups); + create_default_array_1d(DefaultArray1DConfig().with_allow_dups(dups)); int coords_1[] = {1, 2, 3}; int data_1[] = {2, 2, 2}; @@ -1095,7 +2298,7 @@ TEST_CASE_METHOD( "[sparse-global-order][merge][subarray][dups]") { // Create default array. reset_config(); - create_default_array_1d(true); + create_default_array_1d(DefaultArray1DConfig().with_allow_dups(true)); bool use_subarray = false; int qc_idx = GENERATE(1, 2); @@ -1109,7 +2312,7 @@ TEST_CASE_METHOD( uint64_t coords_size = sizeof(coords_1); uint64_t data_size = sizeof(data_1); - // Create the aray. + // Create the array. write_1d_fragment(coords_1, &coords_size, data_1, &data_size); write_1d_fragment(coords_2, &coords_size, data_2, &data_size); @@ -1133,12 +2336,13 @@ TEST_CASE_METHOD( CHECK(!std::memcmp(data_c, data_r, data_r_size)); } -TEST_CASE( +TEST_CASE_METHOD( + CSparseGlobalOrderFx, "Sparse global order reader: user buffer cannot fit single cell", "[sparse-global-order][user-buffer][too-small][rest]") { - VFSTestSetup vfs_test_setup; - std::string array_name = vfs_test_setup.array_uri("test_sparse_global_order"); - auto ctx = vfs_test_setup.ctx(); + std::string array_name = + vfs_test_setup_.array_uri("test_sparse_global_order"); + auto ctx = vfs_test_setup_.ctx(); // Create array with var-sized attribute. Domain dom(ctx); @@ -1203,14 +2407,15 @@ TEST_CASE( array2.close(); } -TEST_CASE( +TEST_CASE_METHOD( + CSparseGlobalOrderFx, "Sparse global order reader: attribute copy memory limit", "[sparse-global-order][attribute-copy][memory-limit][rest]") { Config config; config["sm.mem.total_budget"] = "20000"; - VFSTestSetup vfs_test_setup(config.ptr().get()); - std::string array_name = vfs_test_setup.array_uri("test_sparse_global_order"); - auto ctx = vfs_test_setup.ctx(); + std::string array_name = + vfs_test_setup_.array_uri("test_sparse_global_order"); + auto ctx = vfs_test_setup_.ctx(); // Create array with var-sized attribute. Domain dom(ctx); @@ -1285,10 +2490,10 @@ TEST_CASE_METHOD( create_default_array_1d(); bool use_subarray = false; - SECTION("- No subarray") { + SECTION("No subarray") { use_subarray = false; } - SECTION("- Subarray") { + SECTION("Subarray") { use_subarray = true; } @@ -1312,13 +2517,14 @@ TEST_CASE_METHOD( write_1d_fragment(coords, &coords_size, data, &data_size); } + // FIXME: there is no per fragment budget anymore // Two result tile (2 * (~4000 + 8) will be bigger than the per fragment // budget (1000). - total_budget_ = "40000"; - ratio_coords_ = "0.22"; + memory_.total_budget_ = "40000"; + memory_.ratio_coords_ = "0.22"; update_config(); - tiledb_array_t* array = nullptr; + CApiArray array; tiledb_query_t* query = nullptr; // Try to read. @@ -1337,7 +2543,7 @@ TEST_CASE_METHOD( &query, &array); CHECK(rc == TILEDB_OK); - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_INCOMPLETE); uint64_t loop_idx = 1; @@ -1347,9 +2553,10 @@ TEST_CASE_METHOD( CHECK(!std::memcmp(&loop_idx, data_r, data_r_size)); loop_idx++; - while (status == TILEDB_INCOMPLETE && rc == TILEDB_OK) { - rc = tiledb_query_submit(ctx_, query); - tiledb_query_get_status(ctx_, query, &status); + while (status == TILEDB_INCOMPLETE) { + rc = tiledb_query_submit(context(), query); + CHECK("" == error_if_any(rc)); + tiledb_query_get_status(context(), query, &status); CHECK(4 == data_r_size); CHECK(4 == coords_r_size); CHECK(!std::memcmp(&loop_idx, coords_r, coords_r_size)); @@ -1360,9 +2567,6 @@ TEST_CASE_METHOD( CHECK(rc == TILEDB_OK); // Clean up. - rc = tiledb_array_close(ctx_, array); - CHECK(rc == TILEDB_OK); - tiledb_array_free(&array); tiledb_query_free(&query); } @@ -1371,7 +2575,7 @@ TEST_CASE_METHOD( "Sparse global order reader: correct read state on duplicates", "[sparse-global-order][no-dups][read-state]") { bool dups = GENERATE(false, true); - create_default_array_1d(dups); + create_default_array_1d(DefaultArray1DConfig().with_allow_dups(dups)); // Write one fragment in coordinates 1-10 with data 1-10. int coords[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; @@ -1385,7 +2589,7 @@ TEST_CASE_METHOD( uint64_t data2_size = sizeof(data2); write_1d_fragment(coords, &coords_size, data2, &data2_size); - tiledb_array_t* array = nullptr; + CApiArray array; tiledb_query_t* query = nullptr; // Read with buffers that can only fit one cell. @@ -1408,10 +2612,10 @@ TEST_CASE_METHOD( for (int i = 3; i <= 21; i++) { // Check incomplete query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_INCOMPLETE); - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); CHECK(rc == TILEDB_OK); CHECK(coords_r[0] == i / 2); @@ -1422,10 +2626,10 @@ TEST_CASE_METHOD( for (int i = 2; i <= 10; i++) { // Check incomplete query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_INCOMPLETE); - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); CHECK(rc == TILEDB_OK); CHECK(coords_r[0] == i); @@ -1434,13 +2638,10 @@ TEST_CASE_METHOD( } // Check completed query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_COMPLETED); // Clean up. - rc = tiledb_array_close(ctx_, array); - CHECK(rc == TILEDB_OK); - tiledb_array_free(&array); tiledb_query_free(&query); } @@ -1522,7 +2723,7 @@ TEST_CASE_METHOD( CHECK(!std::memcmp(offsets_c, offsets_r, offsets_r_size)); // Check completed query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_INCOMPLETE); // Reset buffer sizes. @@ -1531,7 +2732,7 @@ TEST_CASE_METHOD( offsets_r_size = sizeof(offsets_r); // Submit query. - rc = tiledb_query_submit(ctx_, query); + rc = tiledb_query_submit(context(), query); REQUIRE(rc == TILEDB_OK); // Validate the second read. @@ -1546,12 +2747,789 @@ TEST_CASE_METHOD( CHECK(!std::memcmp(offsets_c2, offsets_r, offsets_r_size)); // Check completed query status. - tiledb_query_get_status(ctx_, query, &status); + tiledb_query_get_status(context(), query, &status); CHECK(status == TILEDB_COMPLETED); // Clean up. - rc = tiledb_array_close(ctx_, array); + rc = tiledb_array_close(context(), array); CHECK(rc == TILEDB_OK); tiledb_array_free(&array); tiledb_query_free(&query); } + +/** + * Creates an array with a schema whose dimensions and attributes + * come from `Instance`. + */ +template +void CSparseGlobalOrderFx::create_array(const Instance& instance) { + const auto dimensions = instance.dimensions(); + const auto attributes = instance.attributes(); + + std::vector dimension_names; + std::vector dimension_types; + std::vector dimension_ranges; + std::vector dimension_extents; + auto add_dimension = [&]( + const templates::Dimension& dimension) { + using CoordType = templates::Dimension::value_type; + dimension_names.push_back("d" + std::to_string(dimension_names.size() + 1)); + dimension_types.push_back(static_cast(D)); + dimension_ranges.push_back( + const_cast(&dimension.domain.lower_bound)); + dimension_extents.push_back(const_cast(&dimension.extent)); + }; + std::apply( + [&](const templates::Dimension&... dimension) { + (add_dimension(dimension), ...); + }, + dimensions); + + std::vector attribute_names; + std::vector attribute_types; + std::vector attribute_cell_val_nums; + std::vector> attribute_compressors; + auto add_attribute = [&](Datatype attribute) { + attribute_names.push_back("a" + std::to_string(attribute_names.size() + 1)); + attribute_types.push_back(static_cast(attribute)); + attribute_cell_val_nums.push_back(1); + attribute_compressors.push_back(std::make_pair(TILEDB_FILTER_NONE, -1)); + }; + std::apply( + [&](As... attribute) { (add_attribute(attribute), ...); }, + attributes); + + tiledb::test::create_array( + context(), + array_name_, + TILEDB_SPARSE, + dimension_names, + dimension_types, + dimension_ranges, + dimension_extents, + attribute_names, + attribute_types, + attribute_cell_val_nums, + attribute_compressors, + instance.tile_order(), + instance.cell_order(), + instance.tile_capacity(), + instance.allow_duplicates()); +} + +/** + * Runs a correctness check upon `instance`. + * + * Inserts all of the fragments, then submits a global order read + * query and compares the results of the query against the + * expected result order computed from the input data. + */ +template +void CSparseGlobalOrderFx::run(Instance& instance) { + reset_config(); + + auto tmparray = run_create(instance); + run_execute(instance); +} + +template +DeleteArrayGuard CSparseGlobalOrderFx::run_create(Instance& instance) { + ASSERTER(instance.num_user_cells > 0); + + reset_config(); + + memory_ = instance.memory; + update_config(); + + // the tile extent is 2 + // create_default_array_1d(instance.array); + create_array(instance); + + DeleteArrayGuard arrayguard(context(), array_name_.c_str()); + + // write all fragments + for (auto& fragment : instance.fragments) { + write_fragment(fragment); + } + + return arrayguard; +} + +template +void CSparseGlobalOrderFx::run_execute(Instance& instance) { + ASSERTER(instance.num_user_cells > 0); + + std::decay_t expect; + for (const auto& fragment : instance.fragments) { + auto expect_dimensions = expect.dimensions(); + auto expect_attributes = expect.attributes(); + + if (instance.subarray.empty()) { + stdx::extend(expect_dimensions, fragment.dimensions()); + stdx::extend(expect_attributes, fragment.attributes()); + } else { + std::vector accept; + for (uint64_t i = 0; i < fragment.size(); i++) { + if (instance.accept(fragment, i)) { + accept.push_back(i); + } + } + const auto fdimensions = + stdx::select(fragment.dimensions(), std::span(accept)); + const auto fattributes = + stdx::select(fragment.attributes(), std::span(accept)); + stdx::extend(expect_dimensions, stdx::reference_tuple(fdimensions)); + stdx::extend(expect_attributes, stdx::reference_tuple(fattributes)); + } + } + + // Open array for reading. + CApiArray array(context(), array_name_.c_str(), TILEDB_READ); + + // sort for naive comparison + { + std::vector idxs(expect.size()); + std::iota(idxs.begin(), idxs.end(), 0); + + sm::GlobalCellCmp globalcmp(array->array()->array_schema_latest().domain()); + + auto icmp = [&](uint64_t ia, uint64_t ib) -> bool { + return std::apply( + [&globalcmp, ia, ib](const std::vector&... dims) { + const auto l = std::make_tuple(dims[ia]...); + const auto r = std::make_tuple(dims[ib]...); + return globalcmp( + templates::global_cell_cmp_std_tuple(l), + templates::global_cell_cmp_std_tuple(r)); + }, + expect.dimensions()); + }; + + std::sort(idxs.begin(), idxs.end(), icmp); + + if (!instance.allow_duplicates()) { + std::set dedup(icmp); + for (const auto& idx : idxs) { + dedup.insert(idx); + } + + idxs.clear(); + idxs.insert(idxs.end(), dedup.begin(), dedup.end()); + } + + expect.dimensions() = stdx::select( + stdx::reference_tuple(expect.dimensions()), std::span(idxs)); + expect.attributes() = stdx::select( + stdx::reference_tuple(expect.attributes()), std::span(idxs)); + } + + // Create query + tiledb_query_t* query; + auto rc = tiledb_query_alloc(context(), array, TILEDB_READ, &query); + ASSERTER(rc == TILEDB_OK); + rc = tiledb_query_set_layout(context(), query, TILEDB_GLOBAL_ORDER); + ASSERTER(rc == TILEDB_OK); + + if (!instance.subarray.empty()) { + tiledb_subarray_t* subarray; + TRY(context(), tiledb_subarray_alloc(context(), array, &subarray)); + TRY(context(), + instance.template apply_subarray(context(), subarray)); + TRY(context(), tiledb_query_set_subarray_t(context(), query, subarray)); + tiledb_subarray_free(&subarray); + } + + // Prepare output buffer + std::decay_t out; + + auto outdims = out.dimensions(); + auto outatts = out.attributes(); + std::apply( + [&](auto&... field) { + (field.resize(std::max(1, expect.size()), 0), ...); + }, + std::tuple_cat(outdims, outatts)); + + // Query loop + uint64_t outcursor = 0; + while (true) { + // make field size locations + auto dimension_sizes = templates::query::make_field_sizes( + outdims, instance.num_user_cells); + auto attribute_sizes = templates::query::make_field_sizes( + outatts, instance.num_user_cells); + + // add fields to query + templates::query::set_fields( + context(), + query, + dimension_sizes, + outdims, + [](unsigned d) { return "d" + std::to_string(d + 1); }, + outcursor); + templates::query::set_fields( + context(), + query, + attribute_sizes, + outatts, + [](unsigned a) { return "a" + std::to_string(a + 1); }, + outcursor); + + rc = tiledb_query_submit(context(), query); + { + const auto err = error_if_any(rc); + if (err.find("Cannot load enough tiles to emit results from all " + "fragments in global order") != std::string::npos) { + if (!vfs_test_setup_.is_rest()) { + // skip for REST since we will not have access to tile sizes + const auto can_complete = + can_complete_in_memory_budget( + context(), array_name_.c_str(), instance); + if (can_complete.has_value()) { + ASSERTER(!can_complete.value()); + } + } + tiledb_query_free(&query); + return; + } + if constexpr (std::is_same_v) { + if (err.find( + "Cannot allocate space for preprocess result tile ID list")) { + // not enough memory to determine tile order + // we can probably make some assertions about what this should have + // looked like but for now we'll let it go + tiledb_query_free(&query); + return; + } + if (err.find("Cannot load tile offsets") != std::string::npos) { + // not enough memory budget for tile offsets, don't bother asserting + // about it (for now?) + tiledb_query_free(&query); + return; + } + } + ASSERTER("" == err); + } + + tiledb_query_status_t status; + rc = tiledb_query_get_status(context(), query, &status); + ASSERTER(rc == TILEDB_OK); + + const uint64_t dim_num_cells = + templates::query::num_cells(outdims, dimension_sizes); + const uint64_t att_num_cells = + templates::query::num_cells(outatts, attribute_sizes); + + ASSERTER(dim_num_cells == att_num_cells); + + const uint64_t num_cells_bound = + std::min(instance.num_user_cells, expect.size()); + if (dim_num_cells < num_cells_bound) { + ASSERTER(status == TILEDB_COMPLETED); + } else { + ASSERTER(dim_num_cells == num_cells_bound); + } + + outcursor += dim_num_cells; + ASSERTER(outcursor <= expect.size()); + + if (status == TILEDB_COMPLETED) { + break; + } + } + + // Clean up. + tiledb_query_free(&query); + + std::apply( + [outcursor](auto&... outfield) { (outfield.resize(outcursor), ...); }, + std::tuple_cat(outdims, outatts)); + + ASSERTER(expect.dimensions() == outdims); + + // Checking attributes is more complicated because: + // 1) when dups are off, equal coords will choose the attribute from one + // fragment. 2) when dups are on, the attributes may manifest in any order. + // Identify the runs of equal coords and then compare using those + size_t attcursor = 0; + size_t runlength = 1; + + auto viewtuple = [&](const auto& atttuple, size_t i) { + return std::apply( + [&](const auto&... att) { return std::make_tuple(att[i]...); }, + atttuple); + }; + + for (size_t i = 1; i < out.size(); i++) { + if (std::apply( + [&](const auto&... outdim) { + return (... && (outdim[i] == outdim[i - 1])); + }, + outdims)) { + runlength++; + } else if (instance.allow_duplicates()) { + std::set outattsrun; + std::set expectattsrun; + + for (size_t j = attcursor; j < attcursor + runlength; j++) { + outattsrun.insert(viewtuple(outatts, j)); + expectattsrun.insert(viewtuple(expect.attributes(), j)); + } + + ASSERTER(outattsrun == expectattsrun); + + attcursor += runlength; + runlength = 1; + } else { + REQUIRE(runlength == 1); + + const auto out = viewtuple(expect.attributes(), i); + + // at least all the attributes will come from the same fragment + if (out != viewtuple(expect.attributes(), i)) { + // the found attribute values should match at least one of the fragments + bool matched = false; + for (size_t f = 0; !matched && f < instance.fragments.size(); f++) { + for (size_t ic = 0; !matched && ic < instance.fragments[f].size(); + ic++) { + if (viewtuple(instance.fragments[f].dimensions(), ic) == + viewtuple(expect.dimensions(), ic)) { + matched = + (viewtuple(instance.fragments[f].attributes(), ic) == out); + } + } + } + ASSERTER(matched); + } + + attcursor += runlength; + runlength = 1; + } + } + + // lastly, check the correctness of our memory budgeting function + // (skip for REST since we will not have access to tile sizes) + if (!vfs_test_setup_.is_rest()) { + const auto can_complete = can_complete_in_memory_budget( + context(), array_name_.c_str(), instance); + if (can_complete.has_value()) { + ASSERTER(can_complete.has_value()); + } + } +} + +// rapidcheck generators and Arbitrary specializations +namespace rc { + +/** + * @return a generator of valid subarrays within `domain` + */ +Gen>> make_subarray_1d( + const templates::Domain& domain) { + // NB: when (if) multi-range subarray is supported for global order + // (or if this is used for non-global order) + // change `num_ranges` to use the weighted element version + std::optional> num_ranges; + if (true) { + num_ranges = gen::just(1); + } else { + num_ranges = gen::weightedElement( + {{50, 1}, {25, 2}, {13, 3}, {7, 4}, {4, 5}, {1, 6}}); + } + + return gen::mapcat(*num_ranges, [domain](int num_ranges) { + return gen::container>>( + num_ranges, rc::make_range(domain)); + }); +} + +template <> +struct Arbitrary { + static Gen arbitrary() { + constexpr Datatype DIMENSION_TYPE = Datatype::INT32; + using CoordType = tiledb::type::datatype_traits::value_type; + + auto dimension = gen::arbitrary>(); + auto allow_dups = gen::arbitrary(); + + auto domain = + gen::suchThat(gen::pair(allow_dups, dimension), [](const auto& domain) { + bool allow_dups; + templates::Dimension dimension; + std::tie(allow_dups, dimension) = domain; + if (allow_dups) { + return true; + } else { + // need to ensure that rapidcheck uniqueness can generate enough + // cases + return dimension.domain.num_cells() >= 256; + } + }); + + auto fragments = gen::mapcat( + domain, [](std::pair> arg) { + bool allow_dups; + templates::Dimension dimension; + std::tie(allow_dups, dimension) = arg; + + auto fragment = rc::make_fragment_1d( + allow_dups, dimension.domain); + + return gen::tuple( + gen::just(allow_dups), + gen::just(dimension), + make_subarray_1d(dimension.domain), + gen::nonEmpty( + gen::container>>( + fragment))); + }); + + auto num_user_cells = gen::inRange(1, 8 * 1024 * 1024); + + return gen::apply( + [](std::tuple< + bool, + templates::Dimension, + std::vector>, + std::vector>> fragments, + int num_user_cells) { + FxRun1D instance; + std::tie( + instance.array.allow_dups_, + instance.array.dimension_, + instance.subarray, + instance.fragments) = fragments; + + instance.num_user_cells = num_user_cells; + + return instance; + }, + fragments, + num_user_cells); + } +}; + +/** + * @return a generator of valid subarrays within the domains `d1` and `d2` + */ +Gen> make_subarray_2d( + const templates::Domain& d1, const templates::Domain& d2) { + // NB: multi-range subarray is not supported (yet?) for global order read + + return gen::apply( + [](auto d1, auto d2) { + std::optional d1opt; + std::optional d2opt; + if (d1) { + d1opt.emplace(*d1); + } + if (d2) { + d2opt.emplace(*d2); + } + return std::vector>{ + std::make_pair(d1opt, d2opt)}; + }, + gen::maybe(rc::make_range(d1)), + gen::maybe(rc::make_range(d2))); +} + +template <> +struct Arbitrary { + static Gen arbitrary() { + constexpr Datatype Dim0Type = Datatype::INT32; + constexpr Datatype Dim1Type = Datatype::INT32; + using Coord0Type = FxRun2D::Coord0Type; + using Coord1Type = FxRun2D::Coord1Type; + + static_assert(std::is_same_v< + tiledb::type::datatype_traits::value_type, + Coord0Type>); + static_assert(std::is_same_v< + tiledb::type::datatype_traits::value_type, + Coord1Type>); + + auto allow_dups = gen::arbitrary(); + auto d0 = gen::arbitrary>(); + auto d1 = gen::arbitrary>(); + + auto domain = + gen::suchThat(gen::tuple(allow_dups, d0, d1), [](const auto& arg) { + bool allow_dups; + templates::Dimension d0; + templates::Dimension d1; + std::tie(allow_dups, d0, d1) = arg; + if (allow_dups) { + return true; + } else { + // need to ensure that rapidcheck uniqueness can generate enough + // cases + return (d0.domain.num_cells() + d1.domain.num_cells()) >= 12; + } + }); + + auto fragments = gen::mapcat(domain, [](auto arg) { + bool allow_dups; + templates::Dimension d0; + templates::Dimension d1; + std::tie(allow_dups, d0, d1) = arg; + + auto fragment = rc::make_fragment_2d( + allow_dups, d0.domain, d1.domain); + return gen::tuple( + gen::just(allow_dups), + gen::just(d0), + gen::just(d1), + make_subarray_2d(d0.domain, d1.domain), + gen::nonEmpty( + gen::container>(fragment))); + }); + + auto num_user_cells = gen::inRange(1, 8 * 1024 * 1024); + auto tile_order = gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + auto cell_order = gen::element(TILEDB_ROW_MAJOR, TILEDB_COL_MAJOR); + + return gen::apply( + [](auto fragments, + int num_user_cells, + tiledb_layout_t tile_order, + tiledb_layout_t cell_order) { + FxRun2D instance; + std::tie( + instance.allow_dups, + instance.d1, + instance.d2, + instance.subarray, + instance.fragments) = fragments; + + // TODO: capacity + instance.num_user_cells = num_user_cells; + instance.tile_order_ = tile_order; + instance.cell_order_ = cell_order; + + return instance; + }, + fragments, + num_user_cells, + tile_order, + cell_order); + } +}; + +/** + * Specializes `show` to print the final test case after shrinking + */ +template <> +void show(const FxRun1D& instance, std::ostream& os) { + size_t f = 0; + + os << "{" << std::endl; + os << "\t\"fragments\": [" << std::endl; + for (const auto& fragment : instance.fragments) { + os << "\t\t{" << std::endl; + os << "\t\t\t\"coords\": [" << std::endl; + os << "\t\t\t\t"; + show(fragment.dim_, os); + os << std::endl; + os << "\t\t\t], " << std::endl; + os << "\t\t\t\"atts\": [" << std::endl; + os << "\t\t\t\t"; + show(std::get<0>(fragment.atts_), os); + os << std::endl; + os << "\t\t\t] " << std::endl; + os << "\t\t}"; + if ((f++) + 1 < instance.fragments.size()) { + os << ", " << std::endl; + } else { + os << std::endl; + } + } + os << "\t]," << std::endl; + os << "\t\"num_user_cells\": " << instance.num_user_cells << std::endl; + os << "\t\"array\": {" << std::endl; + os << "\t\t\"allow_dups\": " << instance.array.allow_dups_ << std::endl; + os << "\t\t\"domain\": [" << instance.array.dimension_.domain.lower_bound + << ", " << instance.array.dimension_.domain.upper_bound << "]," + << std::endl; + os << "\t\t\"extent\": " << instance.array.dimension_.extent << std::endl; + os << "\t}," << std::endl; + os << "\t\"memory\": {" << std::endl; + os << "\t\t\"total_budget\": " << instance.memory.total_budget_ << ", " + << std::endl; + os << "\t\t\"ratio_tile_ranges\": " << instance.memory.ratio_tile_ranges_ + << ", " << std::endl; + os << "\t\t\"ratio_array_data\": " << instance.memory.ratio_array_data_ + << ", " << std::endl; + os << "\t\t\"ratio_coords\": " << instance.memory.ratio_coords_ << std::endl; + os << "\t}" << std::endl; + os << "}"; +} + +/** + * Specializes `show` to print the final test case after shrinking + */ +template <> +void show(const FxRun2D& instance, std::ostream& os) { + size_t f = 0; + + os << "{" << std::endl; + os << "\t\"fragments\": [" << std::endl; + for (const auto& fragment : instance.fragments) { + os << "\t\t{" << std::endl; + os << "\t\t\t\"d1\": [" << std::endl; + os << "\t\t\t\t"; + show(fragment.d1_, os); + os << std::endl; + os << "\t\t\t\"d2\": [" << std::endl; + os << "\t\t\t\t"; + show(fragment.d2_, os); + os << std::endl; + os << "\t\t\t], " << std::endl; + os << "\t\t\t\"atts\": [" << std::endl; + os << "\t\t\t\t"; + show(std::get<0>(fragment.atts_), os); + os << std::endl; + os << "\t\t\t] " << std::endl; + os << "\t\t}"; + if ((f++) + 1 < instance.fragments.size()) { + os << ", " << std::endl; + } else { + os << std::endl; + } + } + os << "\t]," << std::endl; + os << "\t\"num_user_cells\": " << instance.num_user_cells << std::endl; + os << "\t\"array\": {" << std::endl; + os << "\t\t\"allow_dups\": " << instance.allow_dups << std::endl; + os << "\t\t\"dimensions\": [" << std::endl; + os << "\t\t\t{" << std::endl; + os << "\t\t\t\t\"domain\": [" << instance.d1.domain.lower_bound << ", " + << instance.d1.domain.upper_bound << "]," << std::endl; + os << "\t\t\t\t\"extent\": " << instance.d1.extent << "," << std::endl; + os << "\t\t\t}," << std::endl; + os << "\t\t\t{" << std::endl; + os << "\t\t\t\t\"domain\": [" << instance.d2.domain.lower_bound << ", " + << instance.d2.domain.upper_bound << "]," << std::endl; + os << "\t\t\t\t\"extent\": " << instance.d2.extent << "," << std::endl; + os << "\t\t\t}" << std::endl; + os << "\t\t]" << std::endl; + + os << "\t}," << std::endl; + os << "\t\"memory\": {" << std::endl; + os << "\t\t\"total_budget\": " << instance.memory.total_budget_ << ", " + << std::endl; + os << "\t\t\"ratio_tile_ranges\": " << instance.memory.ratio_tile_ranges_ + << ", " << std::endl; + os << "\t\t\"ratio_array_data\": " << instance.memory.ratio_array_data_ + << ", " << std::endl; + os << "\t\t\"ratio_coords\": " << instance.memory.ratio_coords_ << std::endl; + os << "\t}" << std::endl; + os << "}"; +} + +} // namespace rc + +/** + * Applies `::run` to completely arbitrary 1D input. + * + * `NonShrinking` is used because the shrink space is very large, + * and rapidcheck does not appear to give up. Hence if an instance + * fails, it will shrink for an arbitrarily long time, which is + * not appropriate for CI. If this happens, copy the seed and open a story, + * and whoever investigates can remove the `NonShrinking` part and let + * it run for... well who knows how long, really. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: rapidcheck 1d", + "[sparse-global-order][rest][rapidcheck]") { + SECTION("Rapidcheck") { + rc::prop( + "rapidcheck arbitrary 1d", [this](rc::NonShrinking instance) { + run(instance); + }); + } +} + +/** + * Applies `::run` to completely arbitrary 2D input. + * + * `NonShrinking` is used because the shrink space is very large, + * and rapidcheck does not appear to give up. Hence if an instance + * fails, it will shrink for an arbitrarily long time, which is + * not appropriate for CI. If this happens, copy the seed and open a story, + * and whoever investigates can remove the `NonShrinking` part and let + * it run for... well who knows how long, really. + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: rapidcheck 2d", + "[sparse-global-order][rest][rapidcheck]") { + SECTION("rapidcheck") { + rc::prop( + "rapidcheck arbitrary 2d", [this](rc::NonShrinking instance) { + run(instance); + }); + } +} + +/** + * This test will fail if multi-range subarrays become supported + * for global order. + * When that happens please update all the related `NB` comments + * (and also feel free to remove this at that time). + */ +TEST_CASE_METHOD( + CSparseGlobalOrderFx, + "Sparse global order reader: multi-range subarray signal", + "[sparse-global-order]") { + using Asserter = AsserterCatch; + + // Create default array. + reset_config(); + create_default_array_1d(); + + int coords[5]; + uint64_t coords_size = sizeof(coords); + + // Open array + CApiArray array(context(), array_name_.c_str(), TILEDB_READ); + + // Create query. + tiledb_query_t* query; + TRY(context(), tiledb_query_alloc(context(), array, TILEDB_READ, &query)); + TRY(context(), + tiledb_query_set_layout(context(), query, TILEDB_GLOBAL_ORDER)); + TRY(context(), + tiledb_query_set_data_buffer( + context(), query, "d", &coords[0], &coords_size)); + + // Apply subarray + const int lower_1 = 4, upper_1 = 8; + const int lower_2 = 16, upper_2 = 32; + tiledb_subarray_t* sub; + TRY(context(), tiledb_subarray_alloc(context(), array, &sub)); + TRY(context(), + tiledb_subarray_add_range( + context(), sub, 0, &lower_1, &upper_1, nullptr)); + TRY(context(), + tiledb_subarray_add_range( + context(), sub, 0, &lower_2, &upper_2, nullptr)); + TRY(context(), tiledb_query_set_subarray_t(context(), query, sub)); + tiledb_subarray_free(&sub); + + auto rc = tiledb_query_submit(context(), query); + tiledb_query_free(&query); + REQUIRE(rc == TILEDB_ERR); + + tiledb_error_t* error = nullptr; + rc = tiledb_ctx_get_last_error(context(), &error); + REQUIRE(rc == TILEDB_OK); + + const char* msg; + rc = tiledb_error_message(error, &msg); + REQUIRE(rc == TILEDB_OK); + REQUIRE( + std::string(msg).find( + "Multi-range reads are not supported on a global order query") != + std::string::npos); +} diff --git a/test/support/CMakeLists.txt b/test/support/CMakeLists.txt index 92f1ed59449..b950caebc27 100644 --- a/test/support/CMakeLists.txt +++ b/test/support/CMakeLists.txt @@ -36,6 +36,7 @@ list(APPEND TILEDB_CORE_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/tiledb/sm/c_api") # Gather the test source files set(TILEDB_TEST_SUPPORT_SOURCES + src/array_helpers.cc src/array_schema_helpers.cc src/ast_helpers.h src/ast_helpers.cc diff --git a/test/support/assert_helpers.h b/test/support/assert_helpers.h new file mode 100644 index 00000000000..bf403e8fbd0 --- /dev/null +++ b/test/support/assert_helpers.h @@ -0,0 +1,138 @@ +/** + * @file assert_helpers.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2022-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * Provides definitions of macros which enable dispatching assertion + * failures to library code, depending on the `Asserter` + * type in the scope of the caller. + * + * This is desirable for writing code which might want to be + * invoked in the context of multiple different libraries, such + * as a helper function that could be called from a Catch2 TEST_CASE + * or a rapidcheck property. + */ + +#ifndef TILEDB_ASSERT_HELPERS_H +#define TILEDB_ASSERT_HELPERS_H + +#include +#include + +#include + +namespace tiledb::test { + +/** + * Marker that a template is instantiated by top-level CATCH code + */ +struct AsserterCatch {}; + +/** + * Marker that a template is instantiated by a rapidcheck property + */ +struct AsserterRapidcheck {}; + +/** + * Marker that a template is instantiated by application code + */ +struct AsserterRuntimeException {}; + +} // namespace tiledb::test + +#define __STR_(x) #x +#define __STR_VA_(...) __STR_(__VA_ARGS__) + +/** + * Helper macro for running an assert in a context + * where we might want to dispatch assertion failure behavior + * to one of a few possible different libraries depending + * on the caller + * (e.g. a function which can be called from either a Catch2 + * TEST_CASE or from a rapidcheck property wants to use REQUIRE + * and RC_ASSERT respectively). + * + * Expects a type named `Asserter` to be either `AsserterCatch` or + * `AsserterRapidcheck`. + * + * This expands to REQUIRE for `AsserterCatch` and RC_ASSERT for + * `AsserterRapidcheck`. For both type markers this will throw an exception. + */ +#define ASSERTER(...) \ + do { \ + static_assert( \ + std::is_same::value || \ + std::is_same::value || \ + std::is_same:: \ + value); \ + if (std::is_same::value) { \ + REQUIRE(__VA_ARGS__); \ + } else if (std::is_same:: \ + value) { \ + RC_ASSERT(__VA_ARGS__); \ + } else { \ + RT_ASSERT(__VA_ARGS__); \ + } \ + } while (0) + +/** + * Helper macro for asserting that an expression throws an exception + * in a context where we might want to dispatch assertion failure behavior + * to one of a few possible different libraries depending on the caller + * (e.g. a function which can be called from either a Catch2 + * TEST_CASE or from a rapidcheck property wants to use REQUIRE + * and RC_ASSERT respectively). + * + * Expects a type named `Asserter` to be either `AsserterCatch` or + * `AsserterRapidcheck`. + * + * This expands to REQUIRE_THROWS for `AsserterCatch` and RC_ASSERT_THROWS for + * `AsserterRapidcheck`. For both type markers this will throw an exception. + */ +#define ASSERTER_THROWS(...) \ + do { \ + static_assert( \ + std::is_same::value || \ + std::is_same::value); \ + if (std::is_same::value) { \ + REQUIRE_THROWS(__VA_ARGS__); \ + } else { \ + RC_ASSERT_THROWS(__VA_ARGS__); \ + } \ + } while (0) + +/** Assert which throws a runtime exception upon failure */ +#define RT_ASSERT(...) \ + do { \ + if (!(__VA_ARGS__)) { \ + throw std::runtime_error( \ + std::string("Assertion failed: ") + \ + std::string(__STR_VA_(__VA_ARGS__))); \ + } \ + } while (0) + +#endif diff --git a/test/support/rapidcheck/array_templates.h b/test/support/rapidcheck/array_templates.h new file mode 100644 index 00000000000..95f8b671030 --- /dev/null +++ b/test/support/rapidcheck/array_templates.h @@ -0,0 +1,242 @@ +/** + * @file test/support/rapidcheck/array_templates.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2022-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines rapidcheck generators for the structures + * defined in test/support/src/array_templates.h. + */ + +#ifndef TILEDB_RAPIDCHECK_ARRAY_H +#define TILEDB_RAPIDCHECK_ARRAY_H + +#include +#include +#include + +namespace rc { + +using namespace tiledb::test; +using namespace tiledb::test::templates; + +template +struct Arbitrary> { + static Gen> arbitrary() { + // NB: `gen::inRange` is exclusive at the upper end but tiledb domain is + // inclusive. So we have to use `int64_t` to avoid overflow. + auto bounds = gen::mapcat(gen::arbitrary(), [](D lb) { + if (std::is_same::value) { + return gen::pair( + gen::just(lb), gen::inRange(lb, std::numeric_limits::max())); + } else if (std::is_same::value) { + return gen::pair( + gen::just(lb), gen::inRange(lb, std::numeric_limits::max())); + } else { + auto ub_limit = int64_t(std::numeric_limits::max()) + 1; + return gen::pair( + gen::just(lb), gen::cast(gen::inRange(int64_t(lb), ub_limit))); + } + }); + + return gen::map(bounds, [](std::pair bounds) { + return templates::Domain(bounds.first, bounds.second); + }); + } +}; + +/** + * @return `a - b` if it does not overflow, `std::nullopt` if it does + */ +template +std::optional checked_sub(T a, T b) { + if (!std::is_signed::value) { + return (b > a ? std::nullopt : std::optional(a - b)); + } else if (b < 0) { + return ( + std::numeric_limits::max() + b < a ? std::nullopt : + std::optional(a - b)); + } else { + return ( + std::numeric_limits::min() - b > a ? std::nullopt : + std::optional(a - b)); + } +} + +template +Gen make_extent(const templates::Domain& domain) { + // upper bound on all possible extents to avoid unreasonably + // huge tile sizes + static constexpr D extent_limit = static_cast( + std::is_signed::value ? + std::min( + static_cast(std::numeric_limits::max()), + static_cast(1024 * 16)) : + std::min( + static_cast(std::numeric_limits::max()), + static_cast(1024 * 16))); + + // NB: `gen::inRange` is exclusive at the upper end but tiledb domain is + // inclusive. So we have to be careful to avoid overflow. + + D extent_lower_bound = 1; + D extent_upper_bound; + + const auto bound_distance = + checked_sub(domain.upper_bound, domain.lower_bound); + if (bound_distance.has_value()) { + extent_upper_bound = + (bound_distance.value() < extent_limit ? bound_distance.value() + 1 : + extent_limit); + } else { + extent_upper_bound = extent_limit; + } + + return gen::inRange(extent_lower_bound, extent_upper_bound + 1); +} + +template +struct Arbitrary> { + static Gen> arbitrary() { + using CoordType = templates::Dimension::value_type; + auto tup = gen::mapcat( + gen::arbitrary>(), [](Domain domain) { + return gen::pair(gen::just(domain), make_extent(domain)); + }); + + return gen::map(tup, [](std::pair, CoordType> tup) { + return templates::Dimension{.domain = tup.first, .extent = tup.second}; + }); + } +}; + +template +Gen make_coordinate(const templates::Domain& domain) { + // `gen::inRange` does an exclusive upper bound, + // whereas the domain upper bound is inclusive. + // As a result some contortion is required to deal + // with numeric_limits. + if (std::is_signed::value) { + if (int64_t(domain.upper_bound) < std::numeric_limits::max()) { + return gen::cast(gen::inRange( + int64_t(domain.lower_bound), int64_t(domain.upper_bound + 1))); + } else { + return gen::inRange(domain.lower_bound, domain.upper_bound); + } + } else { + if (uint64_t(domain.upper_bound) < std::numeric_limits::max()) { + return gen::cast(gen::inRange( + uint64_t(domain.lower_bound), uint64_t(domain.upper_bound + 1))); + } else { + return gen::inRange(domain.lower_bound, domain.upper_bound); + } + } +} + +template +Gen> make_range(const templates::Domain& domain) { + return gen::apply( + [](D p1, D p2) { return templates::Domain(p1, p2); }, + make_coordinate(domain), + make_coordinate(domain)); +} + +template +Gen> make_fragment_1d( + bool allow_duplicates, const Domain& d) { + auto coord = make_coordinate(d); + + auto cell = gen::tuple(coord, gen::arbitrary()...); + + using Cell = std::tuple; + + auto uniqueCoords = [](const Cell& cell) { return std::get<0>(cell); }; + + auto cells = gen::nonEmpty( + allow_duplicates ? gen::container>(cell) : + gen::uniqueBy>(cell, uniqueCoords)); + + return gen::map(cells, [](std::vector cells) { + std::vector coords; + std::tuple...> atts; + + std::apply( + [&](std::vector tup_d1, auto... tup_atts) { + coords = tup_d1; + atts = std::make_tuple(tup_atts...); + }, + stdx::transpose(cells)); + + return Fragment1D{.dim_ = coords, .atts_ = atts}; + }); +} + +template +Gen> make_fragment_2d( + bool allow_duplicates, + const Domain& d1, + const templates::Domain& d2) { + auto coord_d1 = make_coordinate(d1); + auto coord_d2 = make_coordinate(d2); + + using Cell = std::tuple; + + auto cell = gen::tuple(coord_d1, coord_d2, gen::arbitrary()...); + + auto uniqueCoords = [](const Cell& cell) { + return std::make_pair(std::get<0>(cell), std::get<1>(cell)); + }; + + auto cells = gen::nonEmpty( + allow_duplicates ? gen::container>(cell) : + gen::uniqueBy>(cell, uniqueCoords)); + + return gen::map(cells, [](std::vector cells) { + std::vector coords_d1; + std::vector coords_d2; + std::tuple...> atts; + + std::apply( + [&](std::vector tup_d1, std::vector tup_d2, auto... tup_atts) { + coords_d1 = tup_d1; + coords_d2 = tup_d2; + atts = std::make_tuple(tup_atts...); + }, + stdx::transpose(cells)); + + return Fragment2D{ + .d1_ = coords_d1, .d2_ = coords_d2, .atts_ = atts}; + }); +} + +template <> +void show>(const templates::Domain& domain, std::ostream& os) { + os << "[" << domain.lower_bound << ", " << domain.upper_bound << "]"; +} + +} // namespace rc + +#endif diff --git a/test/support/src/array_helpers.cc b/test/support/src/array_helpers.cc new file mode 100644 index 00000000000..22218405ff0 --- /dev/null +++ b/test/support/src/array_helpers.cc @@ -0,0 +1,42 @@ +#include + +namespace tiledb::test { + +tiledb_error_t* SparseGlobalOrderReaderMemoryBudget::apply( + tiledb_config_t* config) { + tiledb_error_t* error; + + if (tiledb_config_set( + config, "sm.mem.total_budget", total_budget_.c_str(), &error) != + TILEDB_OK) { + return error; + } + + if (tiledb_config_set( + config, + "sm.mem.reader.sparse_global_order.ratio_tile_ranges", + ratio_tile_ranges_.c_str(), + &error) != TILEDB_OK) { + return error; + } + + if (tiledb_config_set( + config, + "sm.mem.reader.sparse_global_order.ratio_array_data", + ratio_array_data_.c_str(), + &error) != TILEDB_OK) { + return error; + } + + if (tiledb_config_set( + config, + "sm.mem.reader.sparse_global_order.ratio_coords", + ratio_coords_.c_str(), + &error) != TILEDB_OK) { + return error; + } + + return nullptr; +} + +} // namespace tiledb::test diff --git a/test/support/src/array_helpers.h b/test/support/src/array_helpers.h new file mode 100644 index 00000000000..7d5e221018e --- /dev/null +++ b/test/support/src/array_helpers.h @@ -0,0 +1,159 @@ +/** + * @file array_schema.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file declares some array test suite helper functions. + */ + +#ifndef TILEDB_TEST_ARRAY_HELPERS_H +#define TILEDB_TEST_ARRAY_HELPERS_H + +#include "test/support/src/error_helpers.h" +#include "tiledb/sm/c_api/tiledb.h" + +#include + +namespace tiledb::test { + +/** + * RAII to make sure an array is deleted before exiting a scope. + * + * This is useful within rapidcheck properties which may + * want to use the same temp directory and array name + * for each instance of a test. + */ +struct DeleteArrayGuard { + DeleteArrayGuard(tiledb_ctx_t* ctx, const char* array_uri) + : ctx_(ctx) + , array_uri_(array_uri) { + } + + DeleteArrayGuard(DeleteArrayGuard&& movefrom) + : ctx_(movefrom.ctx_) + , array_uri_(movefrom.array_uri_) { + movefrom.release(); + } + + ~DeleteArrayGuard() { + del(); + } + + capi_return_t del() { + if (ctx_ && array_uri_) { + return tiledb_array_delete(ctx_, array_uri_); + } else { + return TILEDB_OK; + } + } + + void release() { + ctx_ = nullptr; + array_uri_ = nullptr; + } + + tiledb_ctx_t* ctx_; + const char* array_uri_; +}; + +/** + * RAII to make sure we close our arrays so that keeping the same array URI + * open from one test to the next doesn't muck things up + * (this is especially important for rapidcheck) + */ +struct CApiArray { + tiledb_ctx_t* ctx_; + tiledb_array_t* array_; + + CApiArray() + : ctx_(nullptr) + , array_(nullptr) { + } + + CApiArray(tiledb_ctx_t* ctx, const char* uri, tiledb_query_type_t mode) + : ctx_(ctx) + , array_(nullptr) { + throw_if_error(ctx, tiledb_array_alloc(ctx, uri, &array_)); + throw_if_error(ctx, tiledb_array_open(ctx, array_, mode)); + } + + CApiArray(CApiArray&& from) + : ctx_(from.ctx_) + , array_(from.movefrom()) { + } + + ~CApiArray() { + if (array_) { + // yes this may std::terminate but hey it's test code + throw_if_error(ctx_, tiledb_array_close(ctx_, array_)); + tiledb_array_free(&array_); + } + } + + tiledb_array_t* movefrom() { + auto array = array_; + array_ = nullptr; + return array; + } + + operator tiledb_array_t*() const { + return array_; + } + + tiledb_array_t* operator->() const { + return array_; + } +}; + +/** + * Encapsulates memory budget configuration parameters for the sparse global + * order reader + */ +struct SparseGlobalOrderReaderMemoryBudget { + std::string total_budget_; + std::string ratio_tile_ranges_; + std::string ratio_array_data_; + std::string ratio_coords_; + + SparseGlobalOrderReaderMemoryBudget() + : total_budget_("1048576") + , ratio_tile_ranges_("0.1") + , ratio_array_data_("0.1") + , ratio_coords_("0.5") { + } + + /** + * Apply this memory budget to `config`. + * + * @return an error if one occurred, or nullptr if successful + */ + tiledb_error_t* apply(tiledb_config_t* config); +}; + +} // namespace tiledb::test + +#endif diff --git a/test/support/src/array_templates.h b/test/support/src/array_templates.h new file mode 100644 index 00000000000..d70d2217b20 --- /dev/null +++ b/test/support/src/array_templates.h @@ -0,0 +1,408 @@ +/** + * @file test/support/src/array_templates.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2022-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file provides templates for generic programming with respect + * to array schema, data types, etc. + */ + +#ifndef TILEDB_ARRAY_TEMPLATES_H +#define TILEDB_ARRAY_TEMPLATES_H + +#include "tiledb.h" +#include "tiledb/type/datatype_traits.h" +#include "tiledb/type/range/range.h" + +#include +#include + +#include +#include +#include + +namespace tiledb::sm { +class Dimension; +} + +namespace tiledb::test::templates { + +/** + * Adapts a `std::tuple` whose fields are all `GlobalCellCmp` + * to itself be `GlobalCellCmp`. + */ +template +struct global_cell_cmp_std_tuple { + global_cell_cmp_std_tuple(const StdTuple& tup) + : tup_(tup) { + } + + tiledb::common::UntypedDatumView dimension_datum( + const tiledb::sm::Dimension&, unsigned dim_idx) const { + return std::apply( + [&](const auto&... field) { + size_t sizes[] = {sizeof(std::decay_t)...}; + const void* const ptrs[] = { + static_cast(std::addressof(field))...}; + return UntypedDatumView(ptrs[dim_idx], sizes[dim_idx]); + }, + tup_); + } + + const void* coord(unsigned dim) const { + return std::apply( + [&](const auto&... field) { + const void* const ptrs[] = { + static_cast(std::addressof(field))...}; + return ptrs[dim]; + }, + tup_); + } + + StdTuple tup_; +}; + +/** + * Constrains types which can be used as the physical type of a dimension. + */ +template +concept DimensionType = requires(const D& coord) { + typename std::is_signed; + { coord < coord } -> std::same_as; + { D(int64_t(coord)) } -> std::same_as; +}; + +/** + * Constrains types which can be used as the physical type of an attribute. + * + * Right now this doesn't constrain anything, it is just a marker for + * readability, and someday we might want it do require something. + */ +template +concept AttributeType = true; + +/** + * Constrains types which can be used as columnar data fragment input. + * + * Methods `dimensions` and `attributes` return tuples whose fields are each + * `std::vector` and `std::vector` + * respectively. + */ +template +concept FragmentType = requires(const T& fragment) { + { fragment.size() } -> std::convertible_to; + + // not sure how to specify "returns any tuple whose elements decay to + // std::vector" + fragment.dimensions(); + fragment.attributes(); +} and requires(T& fragment) { + // non-const versions + fragment.dimensions(); + fragment.attributes(); +}; + +/** + * A generic, statically-typed range which is inclusive on both ends. + */ +template +struct Domain { + D lower_bound; + D upper_bound; + + Domain() { + } + + Domain(D d1, D d2) + : lower_bound(std::min(d1, d2)) + , upper_bound(std::max(d1, d2)) { + } + + uint64_t num_cells() const { + // FIXME: this is incorrect for 64-bit domains which need to check overflow + if (std::is_signed::value) { + return static_cast(upper_bound) - + static_cast(lower_bound) + 1; + } else { + return static_cast(upper_bound) - + static_cast(lower_bound) + 1; + } + } + + bool contains(D point) const { + return lower_bound <= point && point <= upper_bound; + } + + bool intersects(const Domain& other) const { + return (other.lower_bound <= lower_bound && + lower_bound <= other.upper_bound) || + (other.lower_bound <= upper_bound && + upper_bound <= other.upper_bound) || + (lower_bound <= other.lower_bound && + other.lower_bound <= upper_bound) || + (lower_bound <= other.upper_bound && + other.upper_bound <= upper_bound); + } + + tiledb::type::Range range() const { + return tiledb::type::Range(lower_bound, upper_bound); + } +}; + +/** + * A description of a dimension as it pertains to its datatype. + */ +template +struct Dimension { + using value_type = tiledb::type::datatype_traits::value_type; + + Domain domain; + value_type extent; +}; + +/** + * Data for a one-dimensional array + */ +template +struct Fragment1D { + std::vector dim_; + std::tuple...> atts_; + + uint64_t size() const { + return dim_.size(); + } + + std::tuple&> dimensions() const { + return std::tuple&>(dim_); + } + + std::tuple&...> attributes() const { + return std::apply( + [](const std::vector&... attribute) { + return std::tuple&...>(attribute...); + }, + atts_); + } + + std::tuple&> dimensions() { + return std::tuple&>(dim_); + } + + std::tuple&...> attributes() { + return std::apply( + [](std::vector&... attribute) { + return std::tuple&...>(attribute...); + }, + atts_); + } +}; + +/** + * Data for a two-dimensional array + */ +template +struct Fragment2D { + std::vector d1_; + std::vector d2_; + std::tuple...> atts_; + + uint64_t size() const { + return d1_.size(); + } + + std::tuple&, const std::vector&> dimensions() + const { + return std::tuple&, const std::vector&>(d1_, d2_); + } + + std::tuple&, std::vector&> dimensions() { + return std::tuple&, std::vector&>(d1_, d2_); + } + + std::tuple&...> attributes() const { + return std::apply( + [](const std::vector&... attribute) { + return std::tuple&...>(attribute...); + }, + atts_); + } + + std::tuple&...> attributes() { + return std::apply( + [](std::vector&... attribute) { + return std::tuple&...>(attribute...); + }, + atts_); + } +}; + +/** + * Binds variadic field data to a tiledb query + */ +template +struct query_applicator { + /** + * @return a tuple containing the size of each input field + */ + static auto make_field_sizes( + const std::tuple&...> fields, + uint64_t cell_limit = std::numeric_limits::max()) { + std::optional num_cells; + auto make_field_size = [&](const std::vector& field) { + const uint64_t field_cells = + std::min(cell_limit, static_cast(field.size())); + const uint64_t field_size = field_cells * sizeof(T); + if (num_cells.has_value()) { + // precondition: each field must have the same number of cells + ASSERTER(field_cells == num_cells.value()); + } else { + num_cells.emplace(field_cells); + } + return field_size; + }; + + return std::apply( + [make_field_size](const auto&... field) { + return std::make_tuple(make_field_size(field)...); + }, + fields); + } + + /** + * Sets buffers on `query` for the variadic `fields` and `fields_sizes` + */ + static void set( + tiledb_ctx_t* ctx, + tiledb_query_t* query, + auto& field_sizes, + std::tuple&...> fields, + std::function fieldname, + uint64_t cell_offset = 0) { + auto set_data_buffer = + [&](const std::string& name, auto& field, uint64_t& field_size) { + auto ptr = const_cast( + static_cast(&field.data()[cell_offset])); + auto rc = tiledb_query_set_data_buffer( + ctx, query, name.c_str(), ptr, &field_size); + ASSERTER("" == error_if_any(ctx, rc)); + }; + + unsigned d = 0; + std::apply( + [&](const auto&... field) { + std::apply( + [&](Us&... field_size) { + (set_data_buffer(fieldname(d++), field, field_size), ...); + }, + field_sizes); + }, + fields); + } + + /** + * @return the number of cells written into `fields` by a read query + */ + static uint64_t num_cells(const auto& fields, const auto& field_sizes) { + std::optional num_cells; + + auto check_field = [&]( + const std::vector& field, uint64_t field_size) { + ASSERTER(field_size % sizeof(T) == 0); + ASSERTER(field_size <= field.size() * sizeof(T)); + if (num_cells.has_value()) { + ASSERTER(num_cells.value() == field_size / sizeof(T)); + } else { + num_cells.emplace(field_size / sizeof(T)); + } + }; + + std::apply( + [&](const auto&... field) { + std::apply( + [&](const auto&... field_size) { + (check_field(field, field_size), ...); + }, + field_sizes); + }, + fields); + + return num_cells.value(); + } +}; + +/** + * Helper namespace for actually using the `query_applicator`. + * Functions in this namespace help to deduce the template + * instantiation of `query_applicator`. + */ +namespace query { + +/** + * @return a tuple containing the size of each input field + */ +template +auto make_field_sizes( + const auto& fields, + uint64_t cell_limit = std::numeric_limits::max()) { + return [cell_limit](std::tuple fields) { + return query_applicator::make_field_sizes( + fields, cell_limit); + }(fields); +} + +/** + * Set buffers on `query` for the tuple of field columns + */ +template +void set_fields( + tiledb_ctx_t* ctx, + tiledb_query_t* query, + auto& field_sizes, + auto fields, + std::function field_name, + uint64_t cell_offset = 0) { + [&](std::tuple fields) { + query_applicator::set( + ctx, query, field_sizes, fields, field_name, cell_offset); + }(fields); +} + +/** + * @return the number of cells written into `fields` by a read query + */ +template +uint64_t num_cells(const auto& fields, const auto& field_sizes) { + return [&](auto fields) { + return query_applicator::num_cells(fields, field_sizes); + }(fields); +} + +} // namespace query + +} // namespace tiledb::test::templates + +#endif diff --git a/test/support/src/error_helpers.h b/test/support/src/error_helpers.h new file mode 100644 index 00000000000..0fd212a9dca --- /dev/null +++ b/test/support/src/error_helpers.h @@ -0,0 +1,104 @@ +/** + * @file error_helpers.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2017-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file declares helper functions, macros, etc for reporting errors. + */ + +#ifndef TILEDB_TEST_ERROR_HELPERS_H +#define TILEDB_TEST_ERROR_HELPERS_H + +#include "tiledb.h" + +#include + +#include +#include + +/** + * Calls a C API function and returns its status if it is not TILEDB_OK. + */ +#define RETURN_IF_ERR(thing) \ + do { \ + auto rc = (thing); \ + if (rc != TILEDB_OK) { \ + return rc; \ + } \ + } while (0) + +/** + * Asserts that a C API call does not return error. + */ +#define TRY(ctx, thing) \ + do { \ + auto rc = (thing); \ + ASSERTER("" == tiledb::test::error_if_any(ctx, rc)); \ + } while (0) + +namespace tiledb::test { + +/** + * Helper function for not just reporting the return code of a C API call + * but also the error message. + * + * Usage: + * ``` + * auto rc = c_api_invocation(); + * REQUIRE("" == error_if_any(rc)); + * ``` + */ +std::string error_if_any(tiledb_ctx_t* ctx, auto apirc) { + if (apirc == TILEDB_OK) { + return ""; + } + + tiledb_error_t* error = NULL; + if (tiledb_ctx_get_last_error(ctx, &error) != TILEDB_OK) { + return "Internal error: tiledb_ctx_get_last_error"; + } else if (error == nullptr) { + // probably should be unreachable + return ""; + } + + const char* msg; + if (tiledb_error_message(error, &msg) != TILEDB_OK) { + return "Internal error: tiledb_error_message"; + } else { + return std::string(msg); + } +} + +/** + * Throws a `std::runtime_error` if the operation returning `thing` + * did not return `TILEDB_OK`. + */ +void throw_if_error(tiledb_ctx_t* ctx, capi_return_t thing); + +} // namespace tiledb::test + +#endif diff --git a/test/support/src/helpers.cc b/test/support/src/helpers.cc index 3b6cce07384..8bac0df5d2c 100644 --- a/test/support/src/helpers.cc +++ b/test/support/src/helpers.cc @@ -39,6 +39,7 @@ #endif #include +#include "error_helpers.h" #include "helpers.h" #include "serialization_wrappers.h" #include "tiledb/api/c_api/array/array_api_internal.h" @@ -172,6 +173,13 @@ void require_tiledb_ok(tiledb_ctx_t* ctx, int rc) { REQUIRE(rc == TILEDB_OK); } +void throw_if_error(tiledb_ctx_t* ctx, capi_return_t thing) { + auto err = error_if_any(ctx, thing); + if (err != "") { + throw std::runtime_error(err); + } +} + int store_g_vfs(std::string&& vfs, std::vector vfs_fs) { if (!vfs.empty()) { if (std::find(vfs_fs.begin(), vfs_fs.end(), vfs) == vfs_fs.end()) { @@ -194,7 +202,7 @@ bool use_refactored_dense_reader() { REQUIRE(err == nullptr); rc = tiledb_config_get(cfg, "sm.query.dense.reader", &value, &err); - CHECK(rc == TILEDB_OK); + REQUIRE(rc == TILEDB_OK); CHECK(err == nullptr); bool use_refactored_readers = strcmp(value, "refactored") == 0; @@ -214,7 +222,7 @@ bool use_refactored_sparse_global_order_reader() { rc = tiledb_config_get( cfg, "sm.query.sparse_global_order.reader", &value, &err); - CHECK(rc == TILEDB_OK); + REQUIRE(rc == TILEDB_OK); CHECK(err == nullptr); bool use_refactored_readers = strcmp(value, "refactored") == 0; @@ -234,7 +242,7 @@ bool use_refactored_sparse_unordered_with_dups_reader() { rc = tiledb_config_get( cfg, "sm.query.sparse_unordered_with_dups.reader", &value, &err); - CHECK(rc == TILEDB_OK); + REQUIRE(rc == TILEDB_OK); CHECK(err == nullptr); bool use_refactored_readers = strcmp(value, "refactored") == 0; @@ -461,70 +469,72 @@ void create_array( // Create array schema tiledb_array_schema_t* array_schema; - int rc = tiledb_array_schema_alloc(ctx, array_type, &array_schema); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_capacity(ctx, array_schema, capacity); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_allows_dups(ctx, array_schema, (int)allows_dups); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_alloc(ctx, array_type, &array_schema)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_capacity(ctx, array_schema, capacity)); + require_tiledb_ok( + ctx, + tiledb_array_schema_set_allows_dups(ctx, array_schema, (int)allows_dups)); // Create dimensions and domain tiledb_domain_t* domain; - rc = tiledb_domain_alloc(ctx, &domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_domain_alloc(ctx, &domain)); for (size_t i = 0; i < dim_num; ++i) { tiledb_dimension_t* d; - rc = tiledb_dimension_alloc( + require_tiledb_ok( ctx, - dim_names[i].c_str(), - dim_types[i], - dim_domains[i], - tile_extents[i], - &d); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_domain_add_dimension(ctx, domain, d); - REQUIRE(rc == TILEDB_OK); + tiledb_dimension_alloc( + ctx, + dim_names[i].c_str(), + dim_types[i], + dim_domains[i], + tile_extents[i], + &d)); + require_tiledb_ok(ctx, tiledb_domain_add_dimension(ctx, domain, d)); tiledb_dimension_free(&d); } // Set domain to schema - rc = tiledb_array_schema_set_domain(ctx, array_schema, domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_set_domain(ctx, array_schema, domain)); tiledb_domain_free(&domain); // Create attributes for (size_t i = 0; i < attr_num; ++i) { tiledb_attribute_t* a; - rc = tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a); - REQUIRE(rc == TILEDB_OK); - rc = set_attribute_compression_filter( - ctx, a, compressors[i].first, compressors[i].second); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i]); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a)); + require_tiledb_ok( + ctx, + set_attribute_compression_filter( + ctx, a, compressors[i].first, compressors[i].second)); + require_tiledb_ok( + ctx, tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i])); if (nullable != nullopt) { - rc = tiledb_attribute_set_nullable(ctx, a, nullable.value()[i]); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_attribute_set_nullable(ctx, a, nullable.value()[i])); } - rc = tiledb_array_schema_add_attribute(ctx, array_schema, a); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_add_attribute(ctx, array_schema, a)); tiledb_attribute_free(&a); } // Check array schema - rc = tiledb_array_schema_check(ctx, array_schema); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_array_schema_check(ctx, array_schema)); // Create array - rc = tiledb_array_create_serialization_wrapper( - ctx, array_name, array_schema, serialize_array_schema); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_array_create_serialization_wrapper( + ctx, array_name, array_schema, serialize_array_schema)); // Clean up tiledb_array_schema_free(&array_schema); @@ -561,76 +571,80 @@ void create_array( // Create array schema tiledb_array_schema_t* array_schema; - int rc = tiledb_array_schema_alloc(ctx, array_type, &array_schema); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_capacity(ctx, array_schema, capacity); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_alloc(ctx, array_type, &array_schema)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_capacity(ctx, array_schema, capacity)); // Create dimensions and domain tiledb_domain_t* domain; - rc = tiledb_domain_alloc(ctx, &domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_domain_alloc(ctx, &domain)); for (size_t i = 0; i < dim_num; ++i) { tiledb_dimension_t* d; - rc = tiledb_dimension_alloc( + require_tiledb_ok( ctx, - dim_names[i].c_str(), - dim_types[i], - dim_domains[i], - tile_extents[i], - &d); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_domain_add_dimension(ctx, domain, d); - REQUIRE(rc == TILEDB_OK); + tiledb_dimension_alloc( + ctx, + dim_names[i].c_str(), + dim_types[i], + dim_domains[i], + tile_extents[i], + &d)); + require_tiledb_ok(ctx, tiledb_domain_add_dimension(ctx, domain, d)); tiledb_dimension_free(&d); } // Set domain to schema - rc = tiledb_array_schema_set_domain(ctx, array_schema, domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_set_domain(ctx, array_schema, domain)); tiledb_domain_free(&domain); // Create attributes for (size_t i = 0; i < attr_num; ++i) { tiledb_attribute_t* a; - rc = tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a); - REQUIRE(rc == TILEDB_OK); - rc = set_attribute_compression_filter( - ctx, a, compressors[i].first, compressors[i].second); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i]); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_add_attribute(ctx, array_schema, a); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a)); + require_tiledb_ok( + ctx, + set_attribute_compression_filter( + ctx, a, compressors[i].first, compressors[i].second)); + require_tiledb_ok( + ctx, tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i])); + require_tiledb_ok( + ctx, tiledb_array_schema_add_attribute(ctx, array_schema, a)); tiledb_attribute_free(&a); } // Check array schema - rc = tiledb_array_schema_check(ctx, array_schema); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_array_schema_check(ctx, array_schema)); // Create array tiledb_config_t* config; tiledb_error_t* error = nullptr; - rc = tiledb_config_alloc(&config, &error); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_config_alloc(&config, &error)); REQUIRE(error == nullptr); std::string encryption_type_string = encryption_type_str((tiledb::sm::EncryptionType)enc_type); - rc = tiledb_config_set( - config, "sm.encryption_type", encryption_type_string.c_str(), &error); + require_tiledb_ok( + ctx, + tiledb_config_set( + config, + "sm.encryption_type", + encryption_type_string.c_str(), + &error)); REQUIRE(error == nullptr); - rc = tiledb_config_set(config, "sm.encryption_key", key, &error); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_config_set(config, "sm.encryption_key", key, &error)); REQUIRE(error == nullptr); tiledb_ctx_t* ctx_array; REQUIRE(tiledb_ctx_alloc(config, &ctx_array) == TILEDB_OK); - rc = tiledb_array_create(ctx_array, array_name.c_str(), array_schema); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_create(ctx_array, array_name.c_str(), array_schema)); // Clean up tiledb_array_schema_free(&array_schema); @@ -666,59 +680,60 @@ tiledb_array_schema_t* create_array_schema( // Create array schema tiledb_array_schema_t* array_schema; - int rc = tiledb_array_schema_alloc(ctx, array_type, &array_schema); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_capacity(ctx, array_schema, capacity); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_set_allows_dups(ctx, array_schema, (int)allows_dups); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_alloc(ctx, array_type, &array_schema)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_cell_order(ctx, array_schema, cell_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_tile_order(ctx, array_schema, tile_order)); + require_tiledb_ok( + ctx, tiledb_array_schema_set_capacity(ctx, array_schema, capacity)); + require_tiledb_ok( + ctx, + tiledb_array_schema_set_allows_dups(ctx, array_schema, (int)allows_dups)); // Create dimensions and domain tiledb_domain_t* domain; - rc = tiledb_domain_alloc(ctx, &domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_domain_alloc(ctx, &domain)); for (size_t i = 0; i < dim_num; ++i) { tiledb_dimension_t* d; - rc = tiledb_dimension_alloc( + require_tiledb_ok( ctx, - dim_names[i].c_str(), - dim_types[i], - dim_domains[i], - tile_extents[i], - &d); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_domain_add_dimension(ctx, domain, d); - REQUIRE(rc == TILEDB_OK); + tiledb_dimension_alloc( + ctx, + dim_names[i].c_str(), + dim_types[i], + dim_domains[i], + tile_extents[i], + &d)); + require_tiledb_ok(ctx, tiledb_domain_add_dimension(ctx, domain, d)); tiledb_dimension_free(&d); } // Set domain to schema - rc = tiledb_array_schema_set_domain(ctx, array_schema, domain); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_schema_set_domain(ctx, array_schema, domain)); tiledb_domain_free(&domain); // Create attributes for (size_t i = 0; i < attr_num; ++i) { tiledb_attribute_t* a; - rc = tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a); - REQUIRE(rc == TILEDB_OK); - rc = set_attribute_compression_filter( - ctx, a, compressors[i].first, compressors[i].second); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i]); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_array_schema_add_attribute(ctx, array_schema, a); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_attribute_alloc(ctx, attr_names[i].c_str(), attr_types[i], &a)); + require_tiledb_ok( + ctx, + set_attribute_compression_filter( + ctx, a, compressors[i].first, compressors[i].second)); + require_tiledb_ok( + ctx, tiledb_attribute_set_cell_val_num(ctx, a, cell_val_num[i])); + require_tiledb_ok( + ctx, tiledb_array_schema_add_attribute(ctx, array_schema, a)); tiledb_attribute_free(&a); } // Check array schema - rc = tiledb_array_schema_check(ctx, array_schema); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_array_schema_check(ctx, array_schema)); // Clean up return array_schema; @@ -732,11 +747,11 @@ void create_s3_bucket( if (s3_supported) { // Create bucket if it does not exist int is_bucket = 0; - int rc = tiledb_vfs_is_bucket(ctx, vfs, bucket_name.c_str(), &is_bucket); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_vfs_is_bucket(ctx, vfs, bucket_name.c_str(), &is_bucket)); if (!is_bucket) { - rc = tiledb_vfs_create_bucket(ctx, vfs, bucket_name.c_str()); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_vfs_create_bucket(ctx, vfs, bucket_name.c_str())); } } } @@ -749,12 +764,12 @@ void create_azure_container( if (azure_supported) { // Create container if it does not exist int is_container = 0; - int rc = - tiledb_vfs_is_bucket(ctx, vfs, container_name.c_str(), &is_container); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_vfs_is_bucket(ctx, vfs, container_name.c_str(), &is_container)); if (!is_container) { - rc = tiledb_vfs_create_bucket(ctx, vfs, container_name.c_str()); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_vfs_create_bucket(ctx, vfs, container_name.c_str())); } } } @@ -823,22 +838,21 @@ void create_subarray( tiledb_subarray_t** subarray, bool coalesce_ranges) { (void)layout; - int32_t rc; tiledb_array_t tdb_array = *tiledb_array_t::make_handle(array); - rc = tiledb_subarray_alloc(ctx, &tdb_array, subarray); - REQUIRE(rc == TILEDB_OK); - if (rc == TILEDB_OK) { - rc = tiledb_subarray_set_coalesce_ranges(ctx, *subarray, coalesce_ranges); - REQUIRE(rc == TILEDB_OK); - - auto dim_num = (unsigned)ranges.size(); - for (unsigned d = 0; d < dim_num; ++d) { - auto dim_range_num = ranges[d].size() / 2; - for (size_t j = 0; j < dim_range_num; ++j) { - rc = tiledb_subarray_add_range( - ctx, *subarray, d, &ranges[d][2 * j], &ranges[d][2 * j + 1], 0); - REQUIRE(rc == TILEDB_OK); - } + require_tiledb_ok(ctx, tiledb_subarray_alloc(ctx, &tdb_array, subarray)); + + require_tiledb_ok( + ctx, + tiledb_subarray_set_coalesce_ranges(ctx, *subarray, coalesce_ranges)); + + auto dim_num = (unsigned)ranges.size(); + for (unsigned d = 0; d < dim_num; ++d) { + auto dim_range_num = ranges[d].size() / 2; + for (size_t j = 0; j < dim_range_num; ++j) { + require_tiledb_ok( + ctx, + tiledb_subarray_add_range( + ctx, *subarray, d, &ranges[d][2 * j], &ranges[d][2 * j + 1], 0)); } } } @@ -906,17 +920,14 @@ int set_attribute_compression_filter( return TILEDB_OK; tiledb_filter_t* filter; - int rc = tiledb_filter_alloc(ctx, compressor, &filter); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_filter_set_option(ctx, filter, TILEDB_COMPRESSION_LEVEL, &level); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_filter_alloc(ctx, compressor, &filter)); + require_tiledb_ok( + ctx, + tiledb_filter_set_option(ctx, filter, TILEDB_COMPRESSION_LEVEL, &level)); tiledb_filter_list_t* list; - rc = tiledb_filter_list_alloc(ctx, &list); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_filter_list_add_filter(ctx, list, filter); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_attribute_set_filter_list(ctx, attr, list); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_filter_list_alloc(ctx, &list)); + require_tiledb_ok(ctx, tiledb_filter_list_add_filter(ctx, list, filter)); + require_tiledb_ok(ctx, tiledb_attribute_set_filter_list(ctx, attr, list)); tiledb_filter_free(&filter); tiledb_filter_list_free(&list); @@ -1076,22 +1087,22 @@ void write_array( REQUIRE(tiledb_config_alloc(&cfg, &err) == TILEDB_OK); REQUIRE(err == nullptr); - rc = tiledb_array_set_open_timestamp_end(ctx, array, timestamp); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_array_set_open_timestamp_end(ctx, array, timestamp)); // Open array if (encryption_type != TILEDB_NO_ENCRYPTION) { std::string encryption_type_string = encryption_type_str((tiledb::sm::EncryptionType)encryption_type); - rc = tiledb_config_set( - cfg, "sm.encryption_type", encryption_type_string.c_str(), &err); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, + tiledb_config_set( + cfg, "sm.encryption_type", encryption_type_string.c_str(), &err)); REQUIRE(err == nullptr); - rc = tiledb_config_set(cfg, "sm.encryption_key", key, &err); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_config_set(cfg, "sm.encryption_key", key, &err)); REQUIRE(err == nullptr); - rc = tiledb_array_set_config(ctx, array, cfg); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_array_set_config(ctx, array, cfg)); } rc = tiledb_array_open(ctx, array, TILEDB_WRITE); CHECK(rc == TILEDB_OK); @@ -1102,12 +1113,9 @@ void write_array( CHECK(rc == TILEDB_OK); tiledb_subarray_t* subarray; if (sub != nullptr) { - rc = tiledb_subarray_alloc(ctx, array, &subarray); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_subarray_set_subarray(ctx, subarray, sub); - REQUIRE(rc == TILEDB_OK); - rc = tiledb_query_set_subarray_t(ctx, query, subarray); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_subarray_alloc(ctx, array, &subarray)); + require_tiledb_ok(ctx, tiledb_subarray_set_subarray(ctx, subarray, sub)); + require_tiledb_ok(ctx, tiledb_query_set_subarray_t(ctx, query, subarray)); } rc = tiledb_query_set_layout(ctx, query, layout); CHECK(rc == TILEDB_OK); @@ -1150,13 +1158,12 @@ void write_array( // Get fragment uri const char* temp_uri; - rc = tiledb_query_get_fragment_uri(ctx, query, 0, &temp_uri); - REQUIRE(rc == TILEDB_OK); + require_tiledb_ok( + ctx, tiledb_query_get_fragment_uri(ctx, query, 0, &temp_uri)); *uri = std::string(temp_uri); // Close array - rc = tiledb_array_close(ctx, array); - CHECK(rc == TILEDB_OK); + require_tiledb_ok(ctx, tiledb_array_close(ctx, array)); // Clean up tiledb_array_free(&array); diff --git a/test/support/src/vfs_helpers.cc b/test/support/src/vfs_helpers.cc index e488ac5833e..7935f1d4253 100644 --- a/test/support/src/vfs_helpers.cc +++ b/test/support/src/vfs_helpers.cc @@ -84,7 +84,11 @@ std::vector> vfs_test_get_fs_vec() { } if (supports_rest_s3) { - fs_vec.emplace_back(std::make_unique(true)); + if (tiledb::sm::filesystem::s3_enabled) { + fs_vec.emplace_back(std::make_unique(true)); + } else { + throw tiledb::sm::filesystem::BuiltWithout("S3"); + } } fs_vec.emplace_back(std::make_unique()); diff --git a/test/support/stdx/optional.h b/test/support/stdx/optional.h new file mode 100644 index 00000000000..3b51fd6cb4d --- /dev/null +++ b/test/support/stdx/optional.h @@ -0,0 +1,52 @@ +/** + * @file test/support/stdx/optional.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines functions for manipulating `std::optional`. + */ + +#ifndef TILEDB_TEST_SUPPORT_OPTIONAL_H +#define TILEDB_TEST_SUPPORT_OPTIONAL_H + +namespace stdx { + +template +struct is_optional { + static constexpr bool value = false; +}; + +template +struct is_optional> { + static constexpr bool value = true; +}; + +template +concept is_optional_v = is_optional::value; +} // namespace stdx + +#endif diff --git a/test/support/stdx/tuple.h b/test/support/stdx/tuple.h new file mode 100644 index 00000000000..953c041b258 --- /dev/null +++ b/test/support/stdx/tuple.h @@ -0,0 +1,134 @@ +/** + * @file test/support/stdx/tuple.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines functions for manipulating `std::tuple`. + */ + +#ifndef TILEDB_TEST_SUPPORT_TUPLE_H +#define TILEDB_TEST_SUPPORT_TUPLE_H + +namespace stdx { + +template +constexpr auto decay_types(std::tuple const&) + -> std::tuple...>; + +template +using decay_tuple = decltype(decay_types(std::declval())); + +/** + * @return the transposition of row-oriented tuples into column-oriented tuples + */ +template +std::tuple...> transpose(std::vector> rows) { + std::tuple...> cols; + + std::apply( + [&](std::vector&... col) { (col.reserve(rows.size()), ...); }, cols); + + for (size_t i = 0; i < rows.size(); i++) { + std::apply( + [&](std::vector&... col) { + std::apply([&](Ts... cell) { (col.push_back(cell), ...); }, rows[i]); + }, + cols); + } + + return cols; +} + +/** + * @return a tuple whose fields are const references to the argument's fields + */ +template +std::tuple&...> reference_tuple( + const std::tuple& tuple) { + return std::apply( + [](const std::decay_t&... field) { + return std::forward_as_tuple(field...); + }, + tuple); +} + +/** + * Given two tuples of vectors, extends each of the fields of `dst` + * with the corresponding field of `src`. + */ +template +void extend( + std::tuple&...>& dest, + std::tuple&...> src) { + std::apply( + [&](std::vector&... dest_col) { + std::apply( + [&](const std::vector&... src_col) { + (dest_col.reserve(dest_col.size() + src_col.size()), ...); + (dest_col.insert(dest_col.end(), src_col.begin(), src_col.end()), + ...); + }, + src); + }, + dest); +} + +/** + * Selects the positions given by `idx` from each field of `records` to + * construct a new tuple. + * + * @return a new tuple containing just the selected positions + */ +template +std::tuple...> select( + std::tuple&...> records, + std::span idxs) { + std::tuple...> selected; + + auto select_into = [&idxs]( + std::vector& dest, std::span src) { + dest.reserve(idxs.size()); + for (auto i : idxs) { + dest.push_back(src[i]); + } + }; + + std::apply( + [&](std::vector&... sel) { + std::apply( + [&](const std::vector&... col) { + (select_into.template operator()(sel, std::span(col)), ...); + }, + records); + }, + selected); + + return selected; +} + +} // namespace stdx +#endif diff --git a/test/support/tdb_rapidcheck.h b/test/support/tdb_rapidcheck.h new file mode 100644 index 00000000000..2f7907de580 --- /dev/null +++ b/test/support/tdb_rapidcheck.h @@ -0,0 +1,123 @@ +/** + * @file tdb_rapidcheck.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2022-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file wraps and for convenience + * (and compatibility due to a snag with ) + * and contains other definitions which may be useful for writing + * rapidcheck properties. + */ + +#ifndef TILEDB_MISC_TDB_RAPIDCHECK_H +#define TILEDB_MISC_TDB_RAPIDCHECK_H + +/* + * `catch` must be included first to define + * `CATCH_TEST_MACROS_HPP_INCLUDED` + * which is the bridge between v2 and v3 compatibility for + */ +#include +#include + +// forward declarations of `showValue` overloads +// (these must be declared prior to including `rapidcheck/Show.hpp` for some +// reason) +namespace rc { +namespace detail { + +/** + * Specializes `show` for `std::optional` to print the final test case after + * shrinking + */ +template +void showValue(const T& value, std::ostream& os); + +} // namespace detail +} // namespace rc + +#include +#include + +#include + +namespace rc { + +/** + * Wrapper struct whose `Arbitrary` specialization returns + * a non-shrinking generator. + * + * This is meant to be used for generators which have a + * very very large shrinking space, such that by default + * we do not want to shrink (e.g. in CI - instead we want + * to capture the seed immediately and file a bug report + * where the assignee can kick off the shrinking). + */ +template +struct NonShrinking { + NonShrinking(T&& inner) + : inner_(inner) { + } + + T inner_; + + operator T&() { + return inner_; + } + + operator const T&() const { + return inner_; + } +}; + +template +struct Arbitrary> { + static Gen> arbitrary() { + auto inner = gen::noShrink(gen::arbitrary()); + return gen::apply( + [](T inner) { return NonShrinking(std::move(inner)); }, inner); + } +}; + +namespace detail { + +template +void showValue(const T& value, std::ostream& os) { + if (value.has_value()) { + os << "Some("; + show(value.value(), os); + os << ")"; + } else { + os << "None"; + } +} + +} // namespace detail + +} // namespace rc + +#endif // TILEDB_MISC_TDB_RAPIDCHECK_H diff --git a/tiledb/api/c_api/config/config_api_external.h b/tiledb/api/c_api/config/config_api_external.h index ed4b7f69c58..212e16906fe 100644 --- a/tiledb/api/c_api/config/config_api_external.h +++ b/tiledb/api/c_api/config/config_api_external.h @@ -257,6 +257,16 @@ TILEDB_EXPORT void tiledb_config_free(tiledb_config_t** config) TILEDB_NOEXCEPT; * Which reader to use for sparse global order queries. "refactored" * or "legacy".
* **Default**: refactored + * - `sm.query.sparse_global_order.preprocess_tile_merge`
+ * **Experimental for testing purposes, do not use.**
+ * Performance configuration for sparse global order read queries. + * If nonzero, prior to loading the first tiles, the reader will run + * a preprocessing step to arrange tiles from all fragments in a single + * globally ordered list. This is expected to improve performance when + * there are many fragments or when the distribution in space of the + * tiles amongst the fragments is skewed. The value of the parameter + * specifies the amount of work per parallel task. + * **Default**: "32768" * - `sm.query.sparse_unordered_with_dups.reader`
* Which reader to use for sparse unordered with dups queries. * "refactored" or "legacy".
diff --git a/tiledb/common/CMakeLists.txt b/tiledb/common/CMakeLists.txt index 032e2030482..e67cb2b4331 100644 --- a/tiledb/common/CMakeLists.txt +++ b/tiledb/common/CMakeLists.txt @@ -39,6 +39,7 @@ endif() # # Subdirectories # +add_subdirectory(algorithm) add_subdirectory(dynamic_memory) add_subdirectory(evaluator) add_subdirectory(exception) diff --git a/tiledb/common/algorithm/CMakeLists.txt b/tiledb/common/algorithm/CMakeLists.txt new file mode 100644 index 00000000000..95c1bd5a48b --- /dev/null +++ b/tiledb/common/algorithm/CMakeLists.txt @@ -0,0 +1,43 @@ +# +# tiledb/common/algorithm/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2024 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(common NO_POLICY_SCOPE) +include(object_library) + +list(APPEND SOURCES + parallel_merge.cc +) +gather_sources(${SOURCES}) + +# +# `algorithm` object library +# +commence(object_library algorithm) + this_target_sources(${SOURCES}) + this_target_object_libraries(thread_pool) +conclude(object_library) + +add_test_subdirectory() diff --git a/tiledb/common/algorithm/parallel_merge.cc b/tiledb/common/algorithm/parallel_merge.cc new file mode 100644 index 00000000000..54d2e1d321b --- /dev/null +++ b/tiledb/common/algorithm/parallel_merge.cc @@ -0,0 +1,118 @@ +/** + * @file parallel_merge.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2018-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file contains non-template definition used in the parallel merge + * algorithm. + */ + +#include "tiledb/common/algorithm/parallel_merge.h" + +using namespace tiledb::common; + +namespace tiledb::algorithm { + +ParallelMergeFuture::ParallelMergeFuture( + ParallelMergeMemoryResources& memory, size_t parallel_factor) + : memory_(memory) + , merge_bounds_(&memory.control) + , merge_cursor_(0) { + merge_bounds_.reserve(parallel_factor); + for (size_t p = 0; p < parallel_factor; p++) { + merge_bounds_.push_back(MergeUnit(memory.control)); + } +} + +ParallelMergeFuture::~ParallelMergeFuture() { + // make sure all threads are finished because they reference + // data which is contractually expected to outlive `this`, + // but makes no guarantee after that + while (true) { + [[maybe_unused]] const auto m = merge_cursor_; + try { + if (!await().has_value()) { + break; + } + } catch (...) { + // Swallow it. Yes, really. + // 1) exceptions cannot propagate out of destructors. + // 2) the tasks only throw on internal error, i.e. we wrote bad code. + // 3) the user did not wait for all the tasks to finish, so we had better + // do so else we risk undefined behavior. + // 4) the user did not wait for all the tasks to finish, ergo they don't + // care about the result, ergo they don't care if there is an error + // here. Most likely we are tearing down the stack to propagate + // a different exception anyway, we don't want to mask that up + // by signaling. + // + // If this is happening *not* due to an exception, then either: + // 1) the developer is responsible for blocking prior to destruction + // so as to correctly handle any errors + // 2) the developer is responsible for not caring about any + // results beyond what was previously consumed + } + + // however we definitely do want to avoid an infinite loop here, + // so we had better have made progress. + assert(merge_cursor_ > m); + } +} + +bool ParallelMergeFuture::finished() const { + return merge_cursor_ == merge_bounds_.size(); +} + +std::optional ParallelMergeFuture::valid_output_bound() const { + if (merge_cursor_ > 0) { + return merge_bounds_[merge_cursor_ - 1].output_end(); + } else { + return std::nullopt; + } +} + +std::optional ParallelMergeFuture::await() { + auto maybe_task = merge_tasks_.pop(); + if (maybe_task.has_value()) { + const auto m = merge_cursor_++; + + // we must have FIFO + assert(m == maybe_task->p_); + + throw_if_not_ok(maybe_task->task_.wait()); + return merge_bounds_[m].output_end(); + } else { + return std::nullopt; + } +} + +void ParallelMergeFuture::block() { + while (await().has_value()) + ; +} + +} // namespace tiledb::algorithm diff --git a/tiledb/common/algorithm/parallel_merge.h b/tiledb/common/algorithm/parallel_merge.h new file mode 100644 index 00000000000..3009a59b9ab --- /dev/null +++ b/tiledb/common/algorithm/parallel_merge.h @@ -0,0 +1,688 @@ +/** + * @file parallel_merge.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2018-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines an implementation of a parallel merge algorithm for + * in-memory data. A merge combines the data from one or more sorted streams + * and produces a single sorted stream. + * + * Let `K` be the number of input buffers, and `P` be the available parallelism. + * + * A K-way tournament merge does not present obvious parallelization + * opportunities. Each level of the tournament waits for its inputs to finish + * before yielding its own winner. Even if the only single-threaded critical + * path is the final level, each level feeding in can only leverage half of the + * available tasks relative to its input. And then each task is small: a single + * comparison. + * + * Another choice is to have a critical path as the *first* phase of the merge + * algorithm. Instead of parallelizing one merge, we can identify P + * non-overlapping merges from the input, and run each of the P merges fully in + * parallel. This is feasible if we can buffer a large enough amount of data + * from each stream. + * + * This implementation chooses the latter. + * + * The parallel merge algorithm runs in two phases: + * 1) identify merge units + * 2) run tournament merges + * + * We define a "merge unit" to be a set of bounds on each stream [L_k, U_k] + * such that all tuples inside the unit will occur contiguously in the output + * stream. Because the tuples inside the unit all occur contiguously, a merge + * unit can run and produce output without coordinating with other merge units. + * + * The "Identify Merge Units" phase is the sequential critical path. + * Given a total input size of N, we want to identify P merge units which each + * have approximately N/P tuples. We do this by reduction to + * "find a merge unit with N tuples which starts at position 0 in each input". + * Then, merge unit p is the difference between the bounds from that algorithm + * on (p * N/P) and ((p + 1) * N/P). + * + * How do we find a merge unit with N tuples which starts at position 0 in each + * input? Choose the "split point" as the midpoint of a stream k. Count the + * number of tuples in M each stream which are less than the split point. If M + * == N, then we can infer bounds on each stream and are done. If M < N, then + * we accumulate the bounds from each stream, advance the split point to stream + * k + 1, and try again with N - M tuples. And if M > N, then we shrink the + * views on each stream and try again using stream k + 1 for the new split + * point. + * + * One nice feature of this reduction is that we can "yield" merge units as we + * identify them, and simultaneously spawn the tasks to run the tournament merge + * as well as identify the next merge unit. + * + * The "Run Tournament Merges" phases proceeds in one parallel task for each of + * the identified merge units. Each task is a sequential merge of the data + * ranges specified by the merge unit bounds. The current implementation is + * fairly naive, just using a priority queue, but we could imagine doing + * something more interesting here. + * + * Our implementation assumes a single contiguous output buffer. + * Each merge unit can use its bounds to determine which positions in the output + * buffer it is meant to write to. + */ + +#ifndef TILEDB_PARALLEL_MERGE_H +#define TILEDB_PARALLEL_MERGE_H + +#include "tiledb/common/memory_tracker.h" +#include "tiledb/common/pmr.h" +#include "tiledb/common/status.h" +#include "tiledb/common/thread_pool/producer_consumer_queue.h" +#include "tiledb/common/thread_pool/thread_pool.h" + +#include +#include +#include +#include + +namespace tiledb::algorithm { + +class ParallelMergeException : public tiledb::common::StatusException { + public: + explicit ParallelMergeException(const std::string& detail) + : tiledb::common::StatusException("ParallelMerge", detail) { + } +}; + +/** + * Description of data which can be parallel-merged, + * which is any random-access collection of contiguous items + * (e.g. `std::vector>` for some `T`). + */ +template +concept ParallelMergeable = requires(const I& spans, size_t idx) { + typename I::value_type::value_type; + { spans.size() } -> std::convertible_to; + { + spans[idx] + } -> std::convertible_to>; +}; + +/** + * Options for running the parallel merge. + */ +struct ParallelMergeOptions { + // Maximum number of parallel tasks to submit. + uint64_t parallel_factor; + + // Minimum number of items to merge in each parallel task. + uint64_t min_merge_items; +}; + +struct ParallelMergeMemoryResources { + // Memory resource for allocating parallel merge control structures. + tdb::pmr::memory_resource& control; + + ParallelMergeMemoryResources(tiledb::sm::MemoryTracker& memory_tracker) + : control(*memory_tracker.get_resource( + tiledb::sm::MemoryType::PARALLEL_MERGE_CONTROL)) { + } +}; + +struct MergeUnit; + +/** + * The output future of the parallel merge. + * + * Provides methods for waiting on the incremental asynchronous output + * of the merge operation. + * + * The caller is responsible for ensuring that the input data, output data, + * and comparator all out-live the `ParallelMergeFuture`. + */ +struct ParallelMergeFuture { + ParallelMergeFuture( + ParallelMergeMemoryResources& memory, size_t parallel_factor); + + ~ParallelMergeFuture(); + + /** + * @return memory resource used for parallel merge control structures + */ + tdb::pmr::memory_resource& control_memory() const { + return memory_.control; + } + + /** + * @return true if the merge has completed + */ + bool finished() const; + + /** + * @return the position in the output up to which the merge has completed + */ + std::optional valid_output_bound() const; + + /** + * Wait for more data to finish merging. + * + * @return The bound in the output buffer up to which the merge has completed + * @throws If a task finished with error status. + * If this happens then this future is left in an invalid state + * and should not be used. + */ + std::optional await(); + + /** + * Wait for all data to finish merging. + * + * @throws If a task finished with error status. + * If this happens then this future is left in an invalid state + * and should not be used. + */ + void block(); + + private: + ParallelMergeMemoryResources memory_; + + struct MergeUnitTask { + uint64_t p_; + tiledb::common::ThreadPool::Task task_; + }; + + tdb::pmr::vector merge_bounds_; + tiledb::common:: + ProducerConsumerQueue> + merge_tasks_; + + // index of the next expected item in `merge_bounds_` + uint64_t merge_cursor_; + + template + friend class ParallelMerge; +}; + +template < + ParallelMergeable I, + class Compare = std::less> +tdb::pmr::unique_ptr parallel_merge( + tiledb::common::ThreadPool& pool, + const ParallelMergeOptions& options, + const I& streams, + std::remove_cv_t* output); + +/** + * Represents one sequential unit of the parallel merge. + * + * Merges values in the ranges for each stream `s`, [starts[s], ends[s]]. + * This unit writes to the output in the range [sum(starts), sum(ends)]. + */ +struct MergeUnit { + tdb::pmr::vector starts; + tdb::pmr::vector ends; + + MergeUnit(tdb::pmr::memory_resource& resource) + : starts(&resource) + , ends(&resource) { + } + + MergeUnit( + tdb::pmr::memory_resource& resource, + std::initializer_list starts, + std::initializer_list ends) + : starts(starts, &resource) + , ends(ends, &resource) { + } + + /** + * @return the number of data items contained inside this merge unit + */ + uint64_t num_items() const { + uint64_t total_items = 0; + for (size_t i = 0; i < starts.size(); i++) { + total_items += (ends[i] - starts[i]); + } + return total_items; + } + + /** + * @return the starting position in the output where this merge unit writes to + */ + uint64_t output_start() const { + uint64_t total_bound = 0; + for (size_t i = 0; i < ends.size(); i++) { + total_bound += starts[i]; + } + return total_bound; + } + + /** + * @return the upper bound position in the output where this merge unit writes + * to + */ + uint64_t output_end() const { + uint64_t total_bound = 0; + for (size_t i = 0; i < ends.size(); i++) { + total_bound += ends[i]; + } + return total_bound; + } + + bool operator==(const MergeUnit& other) const { + return (starts == other.starts) && (ends == other.ends); + } +}; + +// forward declarations of friend classes for testing +template +struct VerifySplitPointStream; +template +struct VerifyIdentifyMergeUnit; +template +struct VerifyTournamentMerge; + +template < + ParallelMergeable I, + class Compare = std::less> +class ParallelMerge { + public: + using T = typename I::value_type::value_type; + + protected: + /** + * Comparator for std::span which defers comparison to the first + * element of the span. + */ + struct span_greater { + span_greater(Compare& cmp) + : cmp_(cmp) { + } + + bool operator()( + const std::span l, const std::span r) const { + // note that this flips the comparison as it is used in a max heap but we + // want min + return cmp_(r.front(), l.front()); + } + + Compare cmp_; + }; + + /** + * Runs a single-threaded tournament merge of the ranges of `streams` + * identified by `unit`. Writes results to the positions of `output` + * identified by `unit`. + */ + static Status tournament_merge( + const I& streams, + Compare* cmp, + const MergeUnit& unit, + std::remove_cv_t* output) { + std::vector> container; + container.reserve(streams.size()); + + // NB: we can definitely make a more optimized implementation + // which does a bunch of buffering for each battle in the tournament + // but this is straightforward + std::priority_queue< + std::span, + std::vector>, + span_greater> + tournament(span_greater(*cmp), container); + + for (size_t i = 0; i < streams.size(); i++) { + if (unit.starts[i] != unit.ends[i]) { + tournament.push( + std::span(streams[i]) + .subspan(unit.starts[i], unit.ends[i] - unit.starts[i])); + } + } + + size_t o = unit.output_start(); + + while (!tournament.empty()) { + auto stream = tournament.top(); + tournament.pop(); + + // empty streams are not put on the priority queue + assert(!stream.empty()); + + output[o++] = stream.front(); + + if (stream.size() > 1) { + tournament.push(stream.subspan(1)); + } + } + + if (o == unit.output_end()) { + return tiledb::common::Status::Ok(); + } else { + return tiledb::common::Status_Error("Internal error in parallel merge"); + } + } + + /** + * Identifies the upper bounds in each of `streams` where the items + * are less than the split point. + * + * `which` is the index of the stream we want to use for the split point. + */ + static MergeUnit split_point_stream_bounds( + const I& streams, + Compare& cmp, + tdb::pmr::memory_resource& memory, + uint64_t which, + const MergeUnit& search_bounds) { + const auto split_point_idx = + (search_bounds.starts[which] + search_bounds.ends[which] + 1) / 2 - 1; + const T& split_point = streams[which][split_point_idx]; + + MergeUnit output(memory); + output.starts = search_bounds.starts; + output.ends.reserve(streams.size()); + + for (uint64_t i = 0; i < streams.size(); i++) { + if (i == which) { + output.starts[i] = search_bounds.starts[i]; + output.ends.push_back(split_point_idx + 1); + } else { + std::span substream( + streams[i].begin() + search_bounds.starts[i], + streams[i].begin() + search_bounds.ends[i]); + + auto lower_bound = + std::lower_bound( + substream.begin(), substream.end(), split_point, cmp); + output.ends.push_back( + output.starts[i] + std::distance(substream.begin(), lower_bound)); + } + } + + return output; + } + + enum class SearchStep { Stalled, MadeProgress, Converged }; + + /** + * Holds state for searching for a merge unit of size `target_items`. + */ + struct SearchMergeBoundary { + const I& streams_; + Compare& cmp_; + tdb::pmr::memory_resource& memory_; + uint64_t split_point_stream_; + uint64_t remaining_items_; + MergeUnit search_bounds_; + + SearchMergeBoundary( + const I& streams, + Compare& cmp, + tdb::pmr::memory_resource& memory, + uint64_t target_items) + : streams_(streams) + , cmp_(cmp) + , memory_(memory) + , split_point_stream_(0) + , remaining_items_(target_items) + , search_bounds_(memory) { + search_bounds_.starts.reserve(streams.size()); + search_bounds_.ends.reserve(streams.size()); + for (const auto& stream : streams) { + search_bounds_.starts.push_back(0); + search_bounds_.ends.push_back(stream.size()); + } + } + + MergeUnit current() const { + MergeUnit m(memory_); + m.starts.resize(search_bounds_.starts.size(), 0); + m.ends = search_bounds_.ends; + return m; + } + + SearchStep step() { + if (remaining_items_ == 0) { + return SearchStep::Converged; + } + + advance_split_point_stream(); + + MergeUnit split_point_bounds(memory_); + { + if (search_bounds_.starts[split_point_stream_] >= + search_bounds_.ends[split_point_stream_]) { + throw ParallelMergeException("Internal error: invalid split point"); + } + + split_point_bounds = split_point_stream_bounds( + streams_, cmp_, memory_, split_point_stream_, search_bounds_); + } + + const uint64_t num_split_point_items = split_point_bounds.num_items(); + if (num_split_point_items == remaining_items_) { + search_bounds_ = split_point_bounds; + remaining_items_ = 0; + return SearchStep::Converged; + } else if (num_split_point_items < remaining_items_) { + // the split point has too few tuples + // we will include everything we found and advance + assert(search_bounds_.num_items() > 0); + + if (search_bounds_.num_items() == 0) { + throw ParallelMergeException( + "Internal error: split point found zero tuples"); + } + + remaining_items_ -= num_split_point_items; + search_bounds_.starts = split_point_bounds.ends; + return SearchStep::MadeProgress; + } else { + // this split point has too many tuples + // discard the items greater than the split point, and advance to a new + // split point + if (split_point_bounds == search_bounds_) { + return SearchStep::Stalled; + } else { + search_bounds_.ends = split_point_bounds.ends; + return SearchStep::MadeProgress; + } + } + } + + private: + uint64_t next_split_point_stream() const { + return (split_point_stream_ + 1) % streams_.size(); + } + + void advance_split_point_stream() { + for (unsigned i = 0; i < streams_.size(); i++) { + split_point_stream_ = next_split_point_stream(); + if (search_bounds_.starts[split_point_stream_] == + search_bounds_.ends[split_point_stream_]) { + continue; + } else { + return; + } + } + throw ParallelMergeException( + "Internal error: advance_split_point_stream"); + } + }; + + /** + * @return a MergeUnit of size `target_items` whose starting positions are + * zero for each stream + */ + static MergeUnit identify_merge_unit( + const I& streams, + Compare* cmp, + tdb::pmr::memory_resource& memory, + uint64_t target_items) { + SearchMergeBoundary search(streams, *cmp, memory, target_items); + uint64_t stalled = 0; + + while (true) { + auto step = search.step(); + switch (step) { + case SearchStep::Stalled: + stalled++; + if (stalled >= streams.size()) { + throw ParallelMergeException( + "Internal error: no split point shrinks bounds"); + } + continue; + case SearchStep::MadeProgress: + stalled = 0; + continue; + case SearchStep::Converged: + return search.current(); + } + } + } + + /** + * Identifies the next merge unit and then spawns tasks + * to begin the tournament merge of that unit, and also identify + * the next merge unit if there is another. + */ + static Status spawn_next_merge_unit( + tiledb::common::ThreadPool* pool, + const I& streams, + Compare* cmp, + uint64_t parallel_factor, + uint64_t total_items, + uint64_t target_unit_size, + uint64_t p, + std::remove_cv_t* output, + ParallelMergeFuture* future) { + const uint64_t output_end = + std::min(total_items, (p + 1) * target_unit_size); + + auto accumulated_stream_bounds = + identify_merge_unit(streams, cmp, future->control_memory(), output_end); + + if (p == 0) { + future->merge_bounds_[p] = accumulated_stream_bounds; + auto unit_future = pool->execute( + tournament_merge, streams, cmp, future->merge_bounds_[p], output); + + future->merge_tasks_.push(ParallelMergeFuture::MergeUnitTask{ + .p_ = p, .task_ = std::move(unit_future)}); + } else { + future->merge_bounds_[p].starts = future->merge_bounds_[p - 1].ends; + future->merge_bounds_[p].ends = accumulated_stream_bounds.ends; + + auto unit_future = pool->execute( + tournament_merge, streams, cmp, future->merge_bounds_[p], output); + future->merge_tasks_.push(ParallelMergeFuture::MergeUnitTask{ + .p_ = p, .task_ = std::move(unit_future)}); + } + + if (p < parallel_factor - 1) { + pool->execute( + spawn_next_merge_unit, + pool, + streams, + cmp, + parallel_factor, + total_items, + target_unit_size, + p + 1, + output, + future); + } else { + future->merge_tasks_.drain(); + } + + return tiledb::common::Status::Ok(); + } + + static void spawn_merge_units( + tiledb::common::ThreadPool& pool, + size_t parallel_factor, + uint64_t total_items, + const I& streams, + Compare& cmp, + std::remove_cv_t* output, + ParallelMergeFuture& future) { + // NB: round up, if there is a shorter merge unit it will be the last one. + const uint64_t target_unit_size = + (total_items + (parallel_factor - 1)) / parallel_factor; + + pool.execute( + spawn_next_merge_unit, + &pool, + streams, + &cmp, + parallel_factor, + total_items, + target_unit_size, + static_cast(0), + output, + &future); + } + + // friend declarations for testing + friend struct VerifySplitPointStream; + friend struct VerifyIdentifyMergeUnit; + friend struct VerifyTournamentMerge; + + public: + static tdb::pmr::unique_ptr start( + tiledb::common::ThreadPool& pool, + ParallelMergeMemoryResources& memory, + const ParallelMergeOptions& options, + const I& streams, + Compare& cmp, + std::remove_cv_t* output) { + uint64_t total_items = 0; + for (const auto& stream : streams) { + total_items += stream.size(); + } + + const uint64_t parallel_factor = std::clamp( + total_items / options.min_merge_items, + static_cast(1), + options.parallel_factor); + + tdb::pmr::unique_ptr future = + tdb::pmr::emplace_unique( + &memory.control, memory, parallel_factor); + ParallelMerge::spawn_merge_units( + pool, parallel_factor, total_items, streams, cmp, output, *future); + return future; + } +}; + +template +tdb::pmr::unique_ptr parallel_merge( + tiledb::common::ThreadPool& pool, + ParallelMergeMemoryResources& memory, + const ParallelMergeOptions& options, + const I& streams, + Compare& cmp, + std::remove_cv_t* output) { + return ParallelMerge::start( + pool, memory, options, streams, cmp, output); +} + +} // namespace tiledb::algorithm + +#endif diff --git a/tiledb/common/algorithm/test/CMakeLists.txt b/tiledb/common/algorithm/test/CMakeLists.txt new file mode 100644 index 00000000000..f200b3c8ad3 --- /dev/null +++ b/tiledb/common/algorithm/test/CMakeLists.txt @@ -0,0 +1,34 @@ +# +# tiledb/common/algorithm/test/CMakeLists.txt +# +# The MIT License +# +# Copyright (c) 2024 TileDB, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +include(unit_test) + +commence(unit_test algorithm) + this_target_object_libraries(algorithm) + this_target_sources(main.cc unit_parallel_merge.cc) + this_target_link_libraries(tiledb_test_support_lib) + this_target_link_libraries(rapidcheck) +conclude(unit_test) diff --git a/tiledb/common/algorithm/test/compile_algorithm_main.cc b/tiledb/common/algorithm/test/compile_algorithm_main.cc new file mode 100644 index 00000000000..57aab5bd7a7 --- /dev/null +++ b/tiledb/common/algorithm/test/compile_algorithm_main.cc @@ -0,0 +1,34 @@ +/** + * @file compile_algorithm_main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "../parallel_merge.h" + +int main() { + (void)sizeof(tiledb::algorithm::ParallelMergeOptions); + return 0; +} diff --git a/tiledb/common/algorithm/test/main.cc b/tiledb/common/algorithm/test/main.cc new file mode 100644 index 00000000000..f878400f85d --- /dev/null +++ b/tiledb/common/algorithm/test/main.cc @@ -0,0 +1,34 @@ +/** + * @file tiledb/common/test/main.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file defines a test `main()` + */ + +#define CATCH_CONFIG_MAIN +#include diff --git a/tiledb/common/algorithm/test/unit_parallel_merge.cc b/tiledb/common/algorithm/test/unit_parallel_merge.cc new file mode 100644 index 00000000000..24411fc72be --- /dev/null +++ b/tiledb/common/algorithm/test/unit_parallel_merge.cc @@ -0,0 +1,1449 @@ +/** + * @file unit_parallel_merge.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2018-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + */ + +#include "tiledb/common/algorithm/parallel_merge.h" + +#include +#include +#include + +using namespace tiledb::algorithm; +using namespace tiledb::common; +using namespace tiledb::sm; +using namespace tiledb::test; + +namespace tiledb::algorithm { + +template +using Streams = std::vector>; + +const Streams EXAMPLE_STREAMS = { + {123, 456, 789, 890, 901}, + {135, 357, 579, 791, 913}, + {24, 246, 468, 680, 802}, + {100, 200, 300, 400, 500}}; + +template < + ParallelMergeable I, + class Compare = std::less> +struct ParallelMergePublic : public ParallelMerge { + using ParallelMerge::split_point_stream_bounds; +}; + +/** + * Illustrates the steps of the split point algorithm + */ +TEST_CASE( + "parallel merge split point stream bounds example", + "[algorithm][parallel_merge]") { + using PM = ParallelMergePublic; + + auto cmp = + std::less{}; + + auto memory_tracker = get_test_memory_tracker(); + auto& resource = + *memory_tracker->get_resource(MemoryType::PARALLEL_MERGE_CONTROL); + + // below illustrates the steps the algorithm takes to identify + // 10 tuples from `EXAMPLE_STREAMS` which comprise a merge unit + + // first step, choose stream 0 as the split point stream, 789 is the split + // point + const MergeUnit search_0(resource, {0, 0, 0, 0}, {5, 5, 5, 5}); + const MergeUnit expect_0(resource, {0, 0, 0, 0}, {3, 3, 4, 5}); + const MergeUnit result_0 = PM::split_point_stream_bounds( + EXAMPLE_STREAMS, cmp, resource, 0, search_0); + REQUIRE(expect_0 == result_0); + + // we found 15 tuples less than 789, discard positions and try again + // using stream 1 midpoint 357 as the split point + const MergeUnit search_1(resource, {0, 0, 0, 0}, {3, 3, 4, 5}); + const MergeUnit expect_1(resource, {0, 0, 0, 0}, {1, 2, 2, 3}); + const MergeUnit result_1 = PM::split_point_stream_bounds( + EXAMPLE_STREAMS, cmp, resource, 1, search_1); + REQUIRE(expect_1 == result_1); + + // we found 8 tuples, add positions to result and then continue + // using stream 2 midpoint 468 as the next split point + const MergeUnit search_2(resource, {1, 2, 2, 3}, {3, 3, 4, 5}); + const MergeUnit expect_2(resource, {1, 2, 2, 3}, {2, 2, 3, 4}); + const MergeUnit result_2 = PM::split_point_stream_bounds( + EXAMPLE_STREAMS, cmp, resource, 2, search_2); + REQUIRE(expect_2 == result_2); + + // that found 3 more tuples which is too many, discard above bounds + // and advance to stream 4 midpoint 400 as split point + const MergeUnit search_3(resource, {1, 2, 2, 3}, {2, 2, 3, 4}); + const MergeUnit expect_3(resource, {1, 2, 2, 3}, {1, 2, 2, 4}); + const MergeUnit result_3 = PM::split_point_stream_bounds( + EXAMPLE_STREAMS, cmp, resource, 3, search_3); + REQUIRE(expect_3 == result_3); + + // that only found itself, add to bounds and then wrap around + // to stream 0 for the next split point 456 + const MergeUnit search_4(resource, {1, 2, 2, 4}, {2, 2, 3, 4}); + const MergeUnit expect_4(resource, {1, 2, 2, 4}, {2, 2, 2, 4}); + const MergeUnit result_4 = PM::split_point_stream_bounds( + EXAMPLE_STREAMS, cmp, resource, 0, search_4); + REQUIRE(expect_4 == result_4); + + // now the algorithm can yield {{0, 0, 0, 0}, {2, 2, 2, 4}} which is exactly + // 10 tuples +} + +/** + * An instance of an input to the `split_point_stream_bounds` + * function. + * + * The `verify` method checks correctness of `split_point_stream_bounds` + * with respect to the input. + */ +template +struct VerifySplitPointStream { + std::shared_ptr memory; + + Streams streams; + uint64_t which; + MergeUnit search_bounds; + + void verify() { + RC_ASSERT(streams.size() == search_bounds.starts.size()); + RC_ASSERT(search_bounds.starts.size() == search_bounds.ends.size()); + + RC_ASSERT(search_bounds.starts[which] < search_bounds.ends[which]); + + std::vector> spans; + for (auto& stream : streams) { + spans.push_back(std::span(stream)); + } + + auto cmp = std::less{}; + auto result = ParallelMerge::split_point_stream_bounds( + spans, + cmp, + *memory->get_resource(MemoryType::PARALLEL_MERGE_CONTROL), + which, + search_bounds); + + RC_ASSERT(result.ends[which] > 0); + + const auto& split_point = streams[which][search_bounds.ends[which] - 1]; + + for (size_t s = 0; s < streams.size(); s++) { + RC_ASSERT(result.starts[s] == search_bounds.starts[s]); + RC_ASSERT(result.ends[s] <= search_bounds.ends[s]); + + for (size_t i = result.starts[s]; i < result.ends[s]; i++) { + RC_ASSERT(streams[s][i] <= split_point); + } + } + } +}; + +template +struct VerifyIdentifyMergeUnit { + Streams streams; + uint64_t target_items; + + void verify() { + std::vector> spans; + for (auto& stream : streams) { + spans.push_back(std::span(stream)); + } + + auto memory_tracker = get_test_memory_tracker(); + auto& memory = + *memory_tracker->get_resource(MemoryType::PARALLEL_MERGE_CONTROL); + + auto cmp = std::less{}; + auto result = ParallelMerge::identify_merge_unit( + spans, &cmp, memory, target_items); + RC_ASSERT(target_items == result.num_items()); + + for (size_t s = 0; s < streams.size(); s++) { + if (streams[s].empty()) { + RC_ASSERT(result.starts[s] == 0); + RC_ASSERT(result.ends[s] == 0); + } else if (result.starts[s] == result.ends[s]) { + // this stream is not used at all, its first item must exceed + // what lies at the boundary for all other streams + for (size_t s2 = 0; s2 < streams.size(); s2++) { + if (result.starts[s2] != result.ends[s2]) { + RC_ASSERT(streams[s][0] >= streams[s2][result.ends[s2] - 1]); + } + } + } else { + // items from this stream in range `[starts[s], ends[s])` are included + // in the merge unit, so everything outside of the merge unit must + // exceed it + auto& bound_item = streams[s][result.ends[s] - 1]; + for (size_t s2 = 0; s2 < streams.size(); s2++) { + if (result.ends[s2] == streams[s2].size()) { + // the entirety of `s2` is included in the merge unit, + // we can infer no relation to `bound_item` + continue; + } else { + RC_ASSERT(bound_item <= streams[s2][result.ends[s2]]); + } + } + } + } + } +}; + +template +struct VerifyTournamentMerge { + std::shared_ptr memory_tracker; + // appears unused but is needed for `MergeUnit` lifetime + + Streams streams; + MergeUnit unit; + + void verify() { + auto cmp = std::less{}; + + std::vector output(unit.num_items()); + + // SAFETY: the merge unit will begin writing at index + // `unit.output_start()` + T* output_buffer = output.data(); + T* output_ptr = output_buffer - unit.output_start(); + + std::vector> spans; + for (size_t s = 0; s < streams.size(); s++) { + auto& stream = streams[s]; + spans.push_back(std::span(stream)); + } + + auto result = ParallelMerge::tournament_merge( + spans, &cmp, unit, output_ptr); + RC_ASSERT(result.ok()); + + // compare against a naive and slow merge + std::vector inputcmp; + inputcmp.reserve(output.size()); + for (size_t s = 0; s < streams.size(); s++) { + inputcmp.insert( + inputcmp.end(), + streams[s].begin() + unit.starts[s], + streams[s].begin() + unit.ends[s]); + } + std::sort(inputcmp.begin(), inputcmp.end()); + + RC_ASSERT(inputcmp == output); + } +}; + +template +struct VerifyParallelMerge { + Streams streams; + ParallelMergeOptions options; + size_t pool_concurrency; + + void verify() { + auto cmp = std::less{}; + + uint64_t total_items = 0; + for (const auto& stream : streams) { + total_items += stream.size(); + } + + std::vector> spans; + for (auto& stream : streams) { + spans.push_back(std::span(stream)); + } + + // compare against a naive and slow merge + std::vector inputcmp; + { + inputcmp.reserve(total_items); + for (size_t s = 0; s < streams.size(); s++) { + inputcmp.insert(inputcmp.end(), streams[s].begin(), streams[s].end()); + } + std::sort(inputcmp.begin(), inputcmp.end()); + } + + std::vector output(total_items); + + auto memory_tracker = get_test_memory_tracker(); + ParallelMergeMemoryResources resources(*memory_tracker.get()); + + ThreadPool pool(pool_concurrency); + auto future = + parallel_merge(pool, resources, options, spans, cmp, &output.data()[0]); + + std::optional prev_bound; + std::optional bound; + while ((bound = future->await()).has_value()) { + if (prev_bound.has_value()) { + RC_ASSERT(*prev_bound < *bound); + RC_ASSERT(std::equal( + inputcmp.begin() + *prev_bound, + inputcmp.begin() + *bound, + output.begin() + *prev_bound, + output.begin() + *bound)); + } else { + RC_ASSERT(std::equal( + inputcmp.begin(), + inputcmp.begin() + *bound, + output.begin(), + output.begin() + *bound)); + } + prev_bound = bound; + } + + RC_ASSERT(inputcmp == output); + } +}; + +} // namespace tiledb::algorithm + +namespace rc { + +/** + * @return a generator which generates a list of streams each sorted in + * ascending order + */ +template +Gen> streams() { + auto stream = + gen::map(gen::arbitrary>(), [](std::vector elts) { + std::sort(elts.begin(), elts.end()); + return elts; + }); + return gen::nonEmpty(gen::container>>(stream)); +} + +template +Gen> streams_non_empty() { + return gen::suchThat(streams(), [](const Streams& streams) { + return std::any_of( + streams.begin(), streams.end(), [](const std::vector& stream) { + return !stream.empty(); + }); + }); +} + +template +Gen merge_unit( + std::shared_ptr memory_tracker, const Streams& streams) { + std::vector>> all_stream_bounds; + for (const auto& stream : streams) { + Gen> bounds = gen::apply( + [](uint64_t a, uint64_t b) { + return std::make_pair(std::min(a, b), std::max(a, b)); + }, + gen::inRange(0, stream.size() + 1), + gen::inRange(0, stream.size() + 1)); + all_stream_bounds.push_back(bounds); + } + + Gen>> gen_stream_bounds = + // gen::arbitrary>>(); + gen::exec([all_stream_bounds] { + std::vector> bounds; + for (const auto& stream_bound : all_stream_bounds) { + bounds.push_back(*stream_bound); + } + return bounds; + }); + + auto memory = + memory_tracker->get_resource(MemoryType::PARALLEL_MERGE_CONTROL); + + return gen::map( + gen_stream_bounds, + [memory](std::vector> bounds) { + MergeUnit unit(*memory); + unit.starts.reserve(bounds.size()); + unit.ends.reserve(bounds.size()); + for (const auto& bound : bounds) { + unit.starts.push_back(bound.first); + unit.ends.push_back(bound.second); + } + return unit; + }); +} + +template <> +struct Arbitrary { + static Gen arbitrary() { + auto parallel_factor = gen::inRange(1, 16); + auto min_merge_items = gen::inRange(1, 16 * 1024); + return gen::apply( + [](uint64_t parallel_factor, uint64_t min_merge_items) { + return ParallelMergeOptions{ + .parallel_factor = parallel_factor, + .min_merge_items = min_merge_items}; + }, + parallel_factor, + min_merge_items); + } +}; + +/** + * Arbitrary `VerifySplitPointStream` input. + * + * Values generated by this satisfy the following properties: + * 1) At least one of the generated `streams` is non-empty. + * 2) `which` is an index of a non-empty stream. + * 3) `search_bounds` has a non-empty range for stream `which`. + */ +template +struct Arbitrary> { + static Gen> arbitrary() { + return gen::mapcat(streams_non_empty(), [](Streams streams) { + std::vector which_candidates; + for (size_t s = 0; s < streams.size(); s++) { + if (!streams[s].empty()) { + which_candidates.push_back(s); + } + } + + auto memory_tracker = get_test_memory_tracker(); + + auto which = gen::elementOf(which_candidates); + auto search_bounds = merge_unit(memory_tracker, streams); + + return gen::apply( + [](std::shared_ptr memory_tracker, + Streams streams, + uint64_t which, + MergeUnit search_bounds) { + for (size_t s = 0; s < streams.size(); s++) { + if (s == which && + search_bounds.starts[s] == search_bounds.ends[s]) { + // tweak to ensure that the split point is valid + if (search_bounds.starts[s] == 0) { + search_bounds.ends[s] = 1; + } else { + search_bounds.starts[s] -= 1; + } + } + } + return VerifySplitPointStream{ + .memory = memory_tracker, + .streams = streams, + .which = which, + .search_bounds = search_bounds}; + }, + gen::just(memory_tracker), + gen::just(streams), + which, + search_bounds); + }); + } +}; + +template +struct Arbitrary> { + static Gen> arbitrary() { + auto fields = + gen::mapcat(streams_non_empty(), [](const Streams streams) { + uint64_t total_items = 0; + for (const auto& stream : streams) { + total_items += stream.size(); + } + return gen::pair( + gen::just(streams), gen::inRange(1, total_items + 1)); + }); + return gen::map(fields, [](std::pair, uint64_t> fields) { + return VerifyIdentifyMergeUnit{ + .streams = fields.first, .target_items = fields.second}; + }); + } +}; + +template +struct Arbitrary> { + static Gen> arbitrary() { + return gen::mapcat(streams(), [](Streams streams) { + auto memory_tracker = get_test_memory_tracker(); + auto unit = merge_unit(memory_tracker, streams); + return gen::apply( + [memory_tracker](Streams streams, MergeUnit unit) { + return VerifyTournamentMerge{ + .memory_tracker = memory_tracker, + .streams = streams, + .unit = unit}; + }, + gen::just(streams), + unit); + }); + } +}; + +template +struct Arbitrary> { + static Gen> arbitrary() { + return gen::apply( + [](Streams streams, + ParallelMergeOptions options, + size_t pool_concurrency) { + return VerifyParallelMerge{ + .streams = streams, + .options = options, + .pool_concurrency = pool_concurrency}; + }, + streams(), + gen::arbitrary(), + gen::inRange(1, 32)); + } +}; + +template <> +void show>( + const VerifyIdentifyMergeUnit& instance, std::ostream& os) { + os << "{" << std::endl; + os << "\t\"streams\": "; + show(instance.streams, os); + os << "," << std::endl; + os << "\t\"target_items\": " << instance.target_items << std::endl; + os << "}"; +} + +template <> +void show>( + const VerifyTournamentMerge& instance, std::ostream& os) { + os << "{" << std::endl; + os << "\t\"streams\": "; + show(instance.streams, os); + os << "," << std::endl; + os << "\t\"starts\": ["; + show(instance.unit.starts, os); + os << "]" << std::endl; + os << "," << std::endl; + os << "\t\"ends\": ["; + show(instance.unit.ends, os); + os << "]" << std::endl; + os << "}"; +} + +template <> +void show>( + const VerifyParallelMerge& instance, std::ostream& os) { + os << "{" << std::endl; + os << "\t\"streams\": "; + show(instance.streams, os); + os << "," << std::endl; + os << "\t\"options\": {" << std::endl; + os << "\t\t\"parallel_factor\": " << instance.options.parallel_factor + << std::endl; + os << "\t\t\"min_merge_items\": " << instance.options.min_merge_items + << std::endl; + os << "\t}," << std::endl; + os << "\t\"pool_concurrency\": " << instance.pool_concurrency << std::endl; + os << "}"; +} + +} // namespace rc + +TEST_CASE( + "parallel merge rapidcheck uint64_t VerifySplitPointStream", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_split_point_stream_bounds", + [](VerifySplitPointStream input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck uint64_t VerifyIdentifyMergeUnit", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_identify_merge_unit", + [](VerifyIdentifyMergeUnit input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck uint64_t VerifyTournamentMerge", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_tournament_merge", + [](VerifyTournamentMerge input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck uint64_t VerifyParallelMerge", + "[algorithm][parallel_merge]") { + SECTION("Shrink", "Instances found previously") { + VerifyParallelMerge instance; + + instance.pool_concurrency = 16; + instance.options.parallel_factor = 9; + instance.options.min_merge_items = 838; + + instance.streams.push_back({0, 1964708352853888873}); + instance.streams.push_back( + {340504051726841736, + 344852335005584131, + 691432333882738270, + 905545882978089750, + 963620181783757219, + 2280318111133887983, + 2360815377599083728, + 2690651619726011594, + 2702620732101879003, + 3914614302172357712, + 3920420695891434736, + 3977654368208177782, + 4146162831399918660, + 4505912157534417120, + 8178660334482019917}); + instance.streams.push_back( + {250243497698869483, 342520146670513946, 460639582018147099, + 846477702552957623, 878044668058705049, 1357775358844315809, + 1370649669225041685, 1622322930626715479, 2422936557820787022, + 2664728687050573609, 4086304083561519276, 4604089473340659230, + 4695253583520271148, 4815681767159335799, 5076904023605818640, + 5161056711708826442, 5186830854753185896, 5320898922395769611, + 5702839098081247517, 6822853618435881949, 6906251812414549285, + 7306870941006900029, 7598257804853437932, 8000178328658993696, + 8447950993548860398, 8603384033055905231, 9090603704217711025}); + instance.streams.push_back( + {365034819649908200, 537278200643201754, 628028489233162476, + 1056053721546457072, 1631089707251576705, 2157012731714267023, + 2173302468290621506, 2190114075257687709, 3536586244893021144, + 4098871024727029043, 4219940064230999996, 5598434201692323521, + 5733035996731085551, 6151143939934632694, 6492184551178156461, + 6722564512870941217, 6828171325375387848, 6850196870345667255, + 6906839647346621570, 7360142404381869265, 7806912150418303184, + 8205639776231111202, 8313443253139136789, 8374628726628789598, + 8523234285186696347, 8807175409409426574, 8980081069707960942}); + instance.streams.push_back( + {4756539517425999047, + 4798031049377651616, + 5977213149434464454, + 6005322683776308758, + 6224906161411016770}); + instance.streams.push_back( + {111560816646238841, 163505226104379596, 242548861111792893, + 324636646688740289, 331129684786145021, 450004049859509952, + 550145571669736402, 654817179640157417, 671213505367837278, + 706726065752612744, 999609168490346640, 1100961644439277701, + 1414872839773979159, 1480413849222835949, 1738904315782367161, + 2027102963545979713, 2050103317583867222, 2107545739634791825, + 2141915563107849007, 2397505339096866433, 2576574260658871131, + 2845069992345569991, 3638249576331329231, 4441729953183251969, + 4452501912875377316, 4723777203540135155, 4766912539124929330, + 4819717683949706938, 4829678320063540316, 5088008666876198638, + 5107137635514295670, 5226988857720318312, 5382090861412964157, + 5433266385705824660, 5436982627608092533, 5517492712986358013, + 5747809558130145132, 6176513921995629268, 6211511678766187527, + 6275198405564436990, 6506598163638930866, 6507880583921708819, + 6623175656306519005, 6630776298659461624, 6700610119070243161, + 6873876316847742320, 6980573326640936743, 7252188579248074502, + 7326588526750597920, 7732810562794454604, 8063314292280663775, + 8099787036675479852, 8210799376119817540, 8349334719321385082, + 8494590863039018141, 8688281034194548590}); + instance.streams.push_back( + {320254704479545700, 420655069887453760, 429060508926581977, + 460196499028024337, 953561779682446307, 1039074518241554955, + 1188212173726072620, 1249146672866809218, 1704032370646125413, + 1737139332407886824, 1842382122035045074, 1844020417020201978, + 2207529352739723407, 2228893670053514494, 2923067297152209597, + 3144322480149264161, 3500619286656977840, 3533301382068690249, + 3964300661998913793, 4283662014244198584, 4446399025796556476, + 4536325536394672331, 4626130514711806540, 4682353918542575176, + 5676795806028379698, 5866113662510159298, 5879769286594349065, + 6276319267672760976, 6609827901507963179, 6646639152544114166, + 6859661685633003582, 6925850022106562901, 6985856750423128642, + 7158392145585477116, 7210529973088861882, 7412129063702359716, + 8016834345512203897, 8088581722283968874, 8296428221780161990, + 8731882166226556027, 9015400517265605074, 9138799535618320177, + 9155087561764082181}); + instance.streams.push_back( + {51454735997831121, 117143172861056921, 157721973952731687, + 881932815709816266, 927958564919087108, 976777298133626077, + 1032588504959403168, 1091640153918895492, 2054775938455049603, + 2113295226998029101, 2334647935409448680, 2484803221265381168, + 2523622069586548513, 2568249104269020600, 2593969581548133852, + 2739976198173245229, 3251441493648931270, 3305261233013288899, + 3544069342123672695, 3862199197989559994, 4217932160089300610, + 4415142305312094124, 4603038276368455611, 5042299410124212582, + 5076219980086298771, 6539019728857392915, 6945670032672847129, + 7327131529652747193, 7518215687926471612, 7593723469225582738, + 7602455822485877224, 7687988919468341787, 7744840933722738994, + 7902192185997485984, 8447693537973016685, 8912337266114505543, + 8986535159330870946, 9080616779175741434, 9148761422118920839, + 9176500996858521366}); + instance.streams.push_back( + {137326360377084220, + 149180667847225337, + 316502800165470534, + 1869340588901188621, + 2164437463958863510, + 2231858092884084971, + 2291182276971213098, + 2416425506771763220, + 3384721358062562721, + 3867990211623809440, + 3902778783808334295, + 4816387939906796007, + 5589009429684157617, + 5834067346218124068, + 6763754842773870515, + 8115638931351625710, + 8687742730714094669, + 8795917704285277995, + 8803839723448146545}); + + instance.streams.push_back( + {23678161213040705, 510241764617141901, 536672264796840765, + 817271100240475214, 824992929621593785, 936259153877883971, + 960608553447658296, 1016644615123228297, 1079513929429818370, + 1247622598380225718, 1265875085621850877, 1286630358723915721, + 1585784388405292846, 1882514364265324397, 1962983099044038040, + 2047240307888766369, 2089641636142471494, 2104081503879861800, + 2157499291986582082, 2242462293059025198, 2397818829671002449, + 2474643594359212111, 2626879015906626636, 2800942538936715183, + 2950124780977040035, 2962729703188401848, 3195363707418545400, + 3254400008948970680, 3341934193963561380, 3568105357608078905, + 3607416972776535502, 3742895572352962311, 3750922277236933324, + 3862781470922951700, 4133639907119172438, 4167991245860412544, + 4215009550182849694, 4348634397772330783, 4540720827918093668, + 4605892725108032743, 5112147611803069534, 5240743235082602958, + 5293780188735699423, 5344803738723997122, 5359644938166001876, + 5383045845773588147, 5460539756774575500, 5485135325974371263, + 5635140245844161553, 5710092722342554088, 5937538870686885810, + 6035986245880797533, 6332037239660591599, 6377740404404122118, + 6513905600152676553, 6745922411070403519, 6842657271710417778, + 7012233895223404545, 7082439372294707788, 7169430476557444103, + 7252294858470375727, 7296001643492304039, 7362899468051412298, + 7429857992136000225, 7554448534418702302, 7584251785448506899, + 7962776980238621413, 8113407862216908129, 8190116647855294077, + 8243475005557144977, 8246578745154143591, 8300197884230884027, + 8511569804890566846, 8521339560457020348, 8524049315697237765, + 8609444196105514408, 8705353794789460453, 8804664057451386903, + 8995133238524296428, 9002674372116479015}); + instance.streams.push_back( + {852799182518779523, 976211879747489697, 1037751937156943550, + 1183309793253415613, 1278268616507222314, 1451165745236086062, + 1621512904012972626, 1651735659866361412, 1777214878523913043, + 2214931377439983634, 2283177876896964407, 2738593353743079856, + 2820127664090891864, 2844373930338721165, 3189296936039027077, + 3254954514620592062, 3494386171639606090, 3551531641784773313, + 3684669015657700411, 3778424844868346851, 3807518953727490121, + 4014024793634145913, 4376528050311066505, 4709788895635016301, + 4802875159745211525, 4912077114664418965, 5154896847848578782, + 5399889012382577346, 5428207563089827676, 5490404187634371736, + 5658622587346343974, 5749250937460737403, 5854686605696362016, + 5982049066807674509, 6242946560536538596, 6541217505859643210, + 6573036243604757175, 6705697932294170025, 6713109902807273426, + 6921284692183694826, 6957992858094231231, 6967608779543192373, + 7154467612072689977, 7165366467803280993, 7528984442376590632, + 7562549652554641450, 7717229551377374413, 7754567712155703384, + 7868914565741615787, 7937187999987813578, 8701842928973852450, + 8892464805414749943, 9115325089189555539}); + instance.streams.push_back( + {20943666391368365, 50301707991823441, 78148549653558169, + 223537727943675077, 457374694131826701, 677769231307288719, + 738690972072549792, 808779816463584996, 1177985475557649325, + 1237824973985190974, 1409756236817959093, 1435911996961227602, + 1503641274947220311, 1511764275313859679, 1528131221941858934, + 1534381681586604241, 1542839573900239322, 1544003133799503333, + 1588804139273928590, 1801974190474587685, 1823514731001628521, + 2310083928250447665, 2326244911525083082, 2472708620182869557, + 2503087945401652204, 2600086538901526228, 2628865213158208270, + 2667076894330604699, 2707422557831791237, 2777846399740576177, + 2926005513144882263, 3140746181082342021, 3234383478068003737, + 3284748605682722794, 3654468547581799774, 3950170168303459161, + 3951670650353826130, 4047945913306191959, 4113807635664092203, + 4266045397888357498, 4299374661736063607, 4438427720679448353, + 4723519166837053894, 4771139015133323752, 4869515068761357173, + 4970536998559858982, 5073557385565822888, 5198166880328273127, + 5271572181663246052, 5314082606635789328, 5477307833345145180, + 5524207557087096653, 5886015987106668609, 6026454889985748648, + 6084803043157522481, 6158372282711088755, 6168222106544477004, + 6204006830525076915, 6408019547372630191, 6513075043672880140, + 6674516535180876795, 6822681571021993635, 6862399161419494149, + 7067115452221214899, 7138516978426804873, 7689152016280202686, + 7909580144088976322, 8324672428675972990, 8445663724062838862, + 8623264473608867537, 8670488819051193171, 8790796972651896445, + 8974117459504560162, 9096662290612606577}); + instance.streams.push_back( + {63166126530205759, 242392958169270718, 299572426752384365, + 371210267059975449, 409803653906946475, 468886860378125805, + 470084126843809657, 481593649516078721, 903834583636702311, + 958411081937070554, 1643594388685317464, 1867077484313968133, + 1878992910711600903, 2096269166292483012, 2221603162432482372, + 2367859021422440563, 2640404573406261967, 2677835376674463112, + 2762566780438590041, 2970834855784194277, 3245590543761084988, + 3331926875433018809, 3354894536277567765, 3359505071381462687, + 3402470977684680351, 3551747279395294877, 3643287767386033028, + 3785191949950430905, 4110028576828673249, 4170786080207290662, + 4219806117295035085, 4455232860654935884, 4536653577501826109, + 4545527876600156094, 4625757865323486607, 5113820071837780591, + 5431359545392859663, 5644169066530322315, 5812510450528493936, + 5887112635987962293, 5930820878774918392, 6034785964355711944, + 6046455894880780916, 6359379134180690054, 6427488655203662660, + 6488922940107192870, 6630859187399752714, 6647199501483481209, + 6702497299919482097, 6913283435188544583, 7000765135102730735, + 7323710144182268219, 7609074946912556631, 7630879955827434637, + 8012837334883764116, 8177369519382860768, 8373691128186397719, + 8586863860394192504, 8658966137662110078, 8745623093981340006, + 8759582612872391189, 9216498227193543282}); + instance.streams.push_back( + {83371540939874687, 99090153966040961, 265066980165143733, + 270185734230162060, 270764349377328729, 288791090302716150, + 515685752694836926, 527086694921111292, 537609362202016533, + 599732720196005842, 612031408334258699, 896904216546241482, + 999769207989122942, 1133043511208190641, 1693690049598026418, + 1772719663166431987, 1951519083505548569, 2091446513470438219, + 2893471902139543033, 2901452159654803955, 2911580424380098401, + 3011003540331906665, 3117245771853887492, 3340914718518675556, + 3454665172494129776, 3540466628941670785, 3631479891179203900, + 3820561738171968624, 3936713133309989730, 3974381306592113118, + 4072672321187480082, 4134085955791964608, 4252036902039087422, + 4648478980133725331, 4846311851024512063, 4922935334112814400, + 5143553175086741003, 5160210906682153305, 5190619362343208008, + 5286806527989322631, 5347835397983529763, 5480058516342719799, + 5509729406506462759, 6127143000822511824, 6247595783004300435, + 6538518445609528857, 6896728072888269877, 7042490559673111571, + 7797198894572665496, 7929344577293117573, 7953224965042879744, + 7972399940972724227, 7986080139156520841, 8024168897009383804, + 8496115956499380773, 8534953934598353778, 8562337432838158120, + 8630779688559053755, 8738379673480272412, 8936361841694130292, + 8961445256133042831, 9050699752745546434, 9092917754555593883, + 9141148046934586086, 9201142433737462276}); + instance.streams.push_back( + {978079929146482953, + 1409525660235222906, + 1509503421641846840, + 1731861974956539197, + 2287095917157287151, + 2385684395183412530, + 2877469094727427554, + 2877927961277983997, + 3309349110943104743, + 4453367091600589556, + 5445630170469231733, + 5578150355180782693, + 5965388825909305963, + 7337522975923020524, + 7397754781949302590, + 7445025070938323285, + 8373329521362769317, + 8912516122168670635}); + instance.streams.push_back( + {115099052366245073, 348810044115197638, 454297756558378945, + 525963111296184808, 593512962558545249, 798491669620362826, + 869327439478613518, 981097126473349404, 1002101216421266929, + 1034813295165320003, 1264393434589177395, 1345021521632827357, + 1350700929957070172, 1580333759835292921, 2012016518513982623, + 2391834124653836594, 2498010035820125293, 2899557215378165650, + 3159601364147854417, 3340910529178310262, 3383200101416954525, + 3595715391364568339, 3626747055096018491, 3745851203974667752, + 3815940356287831934, 3852280379023155391, 4075227241719476687, + 4277592629799541448, 4689266042484411530, 4808153135201443937, + 4906570846257116542, 5179952845682833086, 5182478464701929134, + 5292145640151081163, 5437316131059031693, 5541408742880580812, + 5682887433002060720, 5714191344223292326, 5731579309052433765, + 5865851265832527092, 6188296807941703853, 6308053424554918208, + 6453124965166334493, 6521001068166938079, 6648365436846851505, + 6686617273303982256, 6710657999109138778, 6742339195490840062, + 6821214221249683902, 6842753050238529660, 6944981180425698867, + 7040942354944121390, 7406993941394416541, 7495074592260989740, + 7527885580667786327, 8021058472116249049, 8303714322765813099, + 8368006548853101370, 8520467236911439331, 8655923628540909373, + 8730767083443288498, 8777309848157974118, 8803674745857201165, + 9120894070780185000, 9125782479443805331}); + instance.streams.push_back( + {82214946185582147, 638230386769953760, 930974016942623358, + 1225826573214176199, 1381192865341561544, 1909694923570278723, + 2780702356789299978, 3765543648183879203, 4018997926153994916, + 4089453881191544673, 4414382114507979262, 4662817260219833523, + 5168913145368538531, 5507337850840720077, 5747627772310775789, + 5991145201999384747, 6480288725892269305, 6807643059838787541, + 7597697085082207038, 7962815082597967978, 7969930733722087093, + 8001999050896731948, 8657206603771597168, 8750184224838691451, + 8761404347128873411, 9069817638306973173}); + instance.streams.push_back( + {487921498259078381, 679071658617431230, 764374888490611317, + 925238360244807201, 1157497519003408600, 1188010947874881034, + 1294245316709222942, 1308759129017077937, 1456021713368322450, + 1562145014347624567, 1665773474303121608, 1830853550932232765, + 1984890043982131546, 2027238322486055348, 2028467493513280808, + 2048654823136184085, 2050731743184357900, 2051006077165771338, + 2122605183153034700, 2164916519025182090, 2306688405259976416, + 2569911000316990930, 2707215687757190194, 2798872767746140499, + 3039005933784495926, 3155860803036790280, 3287334551636549192, + 3460037407701804509, 3480514362615843109, 3529761578628417370, + 3605421548746285977, 3668137967771604963, 3854196067176047399, + 4174953448500066916, 4183641199393801913, 4257822442447196266, + 4517889586519195136, 4545792098053641234, 4713067602976952130, + 4974919041423989357, 5200537300214769139, 5346144590813326687, + 5348593616115520032, 5450244594874073013, 5534173425947140314, + 5691231366916640932, 5854564983197580789, 6033804493935938541, + 6050661574631158637, 6338616026487638908, 6468369487438837308, + 6718920907523993661, 7494540413174816428, 7700271121561213222, + 7747996660032220767, 8153855686220060373, 8301835542127523736, + 8310814362457213780, 8357421137243286428, 8421758511454956292, + 8488672609098859079, 8601102709344884123, 8717181881737517673, + 8863705693387701392, 8949916871602876731}); + instance.streams.push_back( + {629323900936271853, + 1137842357866871600, + 2544291276218199502, + 2548577492105538193, + 4478450303133459836, + 5759012633820715055, + 5841580894289216512, + 6595012440364481517, + 7748533340176401956, + 7823400066738152787, + 7836255795681407155, + 8164837487421063835, + 8331526455846075045, + 8720314151969493218, + 8922460715960042588, + 8984160442812207787}); + instance.streams.push_back( + {285705971133895684, 1137948778300781933, 1456321730948168173, + 1701005447601122263, 1779219982736449125, 1901064185213667642, + 2323311467631549246, 3062745247368715424, 3255723597016881970, + 3620694899140096178, 3669875189360761436, 3820970890922102074, + 4421532518240930469, 4668525690843200017, 5059454289865985611, + 5138688983424923464, 5251311098633968937, 5272795211633015455, + 5429117732141768878, 5594384885252184809, 5736762777112863449, + 6375704680289738215, 6464379414503375352, 6595935814828333306, + 6883681170390130206, 6939426714908848607, 7195435632323483879, + 7393834961036793238, 7486339410612782998, 7822901423881124560, + 8205963132292198176, 8461200943179660674, 8784667488105527035, + 8795332699575539093, 9062848554475650570}); + instance.streams.push_back( + {649342514129014019, 1013609001391672817, 1552770829407841903, + 1706074387936052883, 2548683352257880495, 2780139330698936149, + 2951212375452138462, 3870386150608349786, 3960748984116259407, + 4515559401023148294, 4826228728631893094, 4828509659393215759, + 4836725078690018234, 5656385505541132678, 6107120255981371338, + 6586396852017701806, 6916523393735689456, 6917559196143595205, + 7077742276716875042, 7177046026795199725, 7339158529155971675, + 7856953593588045735}); + instance.streams.push_back( + {103312494178528482, 234405838252514844, 330032410803652666, + 360939175401072749, 519867640403597499, 593307860016959663, + 623778226224542360, 640643113234747207, 861311118685843861, + 1214613703731224172, 1529705270541373318, 2793264392155230386, + 3103915778589791456, 3146440560008244613, 3257465928619382554, + 3282715791883507152, 3323036804885755637, 4222176784995031174, + 4289430572947015273, 4321162610100759218, 4589140594212039846, + 4645145067993788922, 4816731277388022047, 4837005048398405969, + 5090487625726538849, 5298654518040793720, 5629080880414982651, + 6271444793144846681, 6524487041982597712, 7152771782154854398, + 7479111365116323952, 7669307042553249175, 7835952280336708833, + 8012716915770825646, 8016767728769763250, 8647290126259516347, + 8908070696861763158}); + instance.streams.push_back( + {468137721914325517, 505117020292705358, 617453048083099567, + 722432666278755340, 743944767710608524, 817337702921561702, + 964298007710047270, 1085788254735572558, 1229163532786185187, + 1267904705291553727, 1335338329848030023, 1456849486329528319, + 1458456523464408631, 1522873537225983581, 1690285984647312583, + 2279670235761762162, 2338619718483254870, 2393649659784264625, + 2494691992724655834, 2562853983115884052, 2631662446792181160, + 2671788079680194427, 2750913865345085626, 2777943866185224959, + 2819109626164132074, 2878115195039336529, 2927533075635477881, + 3074465162329395288, 3207616553987777400, 3436178864499235290, + 3687937503069163511, 3790031988860279331, 3808430880928299164, + 3972622864508455896, 3987422619560181033, 4304895466987371555, + 4591838038828380747, 4603042037887037413, 4770973830065599712, + 4830870656411977203, 5064320335183742324, 5068835356504398856, + 5247762342994044306, 5388649844889336005, 5430693440992736553, + 5857226827051826792, 5947841653468176431, 5971499925210093244, + 6474956286328840336, 6479747409942328448, 6594678433421674820, + 6648975724640599917, 6743815068136504683, 7164603377895064448, + 7176624776583216008, 7193972700885868440, 7267424656938049115, + 7755322816414385148, 7761365782584823534, 8208479161312198501, + 8231630458641300420, 8331053690100244654, 8534276101518867142, + 8542420471227393797, 8579650596911953516}); + instance.streams.push_back( + {92056098981300951, 183191604288414135, 425609311770953489, + 733399501951125206, 815017252624361671, 1174214440473219376, + 1527447727224758418, 1604558772613236346, 1815102969572261701, + 2020023237241195659, 2133221346465028689, 2309573638621438843, + 3059647754764119277, 3089018963508080823, 3520610057292262122, + 3706959419050779691, 4088044684824043707, 4266682710981069621, + 4367141727169343278, 4405220041098668848, 4572714270240917773, + 4582869992764212527, 4730057491341003468, 4863235522401558598, + 4960168984954372159, 5213732910162448626, 5265577214356543708, + 5579206437592754639, 5886198738755368709, 6057211466533567402, + 6272910612650927157, 6817377680671947413, 6819793996159518888, + 6835624039613756154, 7130584319880415797, 7563998286582581021, + 8048710272435454281, 8106671345277378212, 8200067328726125681, + 8397152345872926563, 8416609622231974393, 8432816603155051087, + 8546110210248055888, 8559128773431956735, 9040583859392571998, + 9079747481631520099, 9154620002255343025}); + instance.streams.push_back( + {186460269856899145, 312877223343858913, 392010051167932117, + 592734175915682324, 638573601945881452, 674255518590737590, + 688880322638418820, 1013857353576768172, 1112411780679572365, + 1139883811869746750, 1190575836840241598, 1207982001775715382, + 1211552828881387578, 1233350427106076327, 1385471799097662144, + 1531490738187958505, 1935384258720069435, 2071863720326007106, + 2289216337643919462, 2408743966916292164, 2454481366897894733, + 2814363792059893654, 2826202475368046979, 2833923842432602204, + 2975442383331736876, 3208835367202654229, 3292645341676952881, + 3305481577550342988, 3584241433084694833, 3743273293915887787, + 3879826256137027085, 4495397115498651467, 4589766514545949658, + 4645502849012582421, 4698623771475978677, 4921845594641293690, + 4973700052797881949, 5054012183269618598, 5121747673560119933, + 5155381082608426532, 5169820373996821774, 5193501081967012768, + 5300401153239731147, 5312160546165835590, 5366563483338550359, + 5407723435082400908, 5479116058910514884, 5852869581120320600, + 6056660948569423595, 6059285926721535698, 6066133154606264656, + 6150528155680608358, 6205405588570672356, 6455658007269102337, + 6517690050619506041, 6604837358100922800, 6740391577115078550, + 6754870494384726974, 6782744164019537637, 6858670266450659431, + 7031477863821497751, 7090103973012968883, 7125261418080735528, + 7333138589807036720, 7350755014925499557, 7353877654437638031, + 7400869418000115287, 7543529844050914728, 7603067751752487016, + 7799970764974260643, 7827063332152321532, 7975289673723089483, + 8090629125471632636, 8251074770046802788, 8328759754833931749, + 8685070722362163095, 8866312412480244316, 8866825913959048686, + 8917266639179383883, 8951106491369592720, 9006103287160623971, + 9081097821549706483, 9139726052133853214, 9156509614091645994, + 9211010153690967898}); + instance.streams.push_back( + {104230526944818390, 194872867561694437, 212441359160354232, + 321012325665976628, 431120611506335132, 533888981277404640, + 993196096130371670, 1030830090146476659, 1092324484284608889, + 1385143609848204220, 1447957761663850140, 1883972771473232369, + 1886132719335527511, 2041392723108802553, 2593917574312472347, + 2937372744931372239, 2996487507555474375, 3670702336583709390, + 3878940726871043993, 4157152156857244222, 4960850397334813825, + 5113920412938398589, 5169812923568587478, 5285613458152029336, + 5559283782542258439, 6157831502356165880, 6566627599020312553, + 7006145820121144150, 7225573382617770716, 7287615231139773891, + 7303873576606859546, 7317271246953513712, 7349671905717534681, + 7421533849456513697, 7526668599944036583, 7599086076101535523, + 7623492095781433338, 7711283192366365947, 7949095350580553873, + 8228783759112069242, 8295567568192555838, 8534365345714775019, + 8758541163334201549, 8821924156918821279, 8883120207037353129}); + instance.streams.push_back( + {58386620028953901, 603149038174852283, 784733004041771519, + 849256375728271061, 876434749911498209, 1153277607489026889, + 1284332405460994971, 1544280345202798527, 1569222782641349918, + 1605199731050122714, 1749308567067587821, 1863672956468109621, + 1926342313223625877, 2017927708422559327, 2027659154571342628, + 2117877383956303269, 2147965921916703578, 2233730766407072802, + 2286207197542499020, 2475529216448935277, 2537071554667498648, + 2561933176400165365, 2582706607166598310, 2629988231088809001, + 2668258253087373077, 2740522624473634895, 2750386422056636885, + 2772828149547986246, 2867225468300520004, 2895603989194672142, + 2916624250131739096, 3001758350138792451, 3023960594088693992, + 3103897686160767364, 3241265739486382608, 3272279529480798961, + 3519715009004256471, 3579844164298648535, 3659500891570190670, + 3829842546426485320, 3960671906396409644, 4140246018246094004, + 4420865993105853873, 4508843839749714898, 4523034759866345601, + 4957603909945162337, 5227269089039875466, 5336771787899565333, + 5345440780239373841, 5350973736703669852, 5510225308627802955, + 5695942359188224540, 5809619522614336735, 6728065708154526109, + 6829383044318697405, 6946524976100950664, 7153006052997047142, + 7167920757371519098, 7332662781768146263, 7373793185529622940, + 7447826569645645832, 7490493972617251142, 7831303308378876856, + 7843091442338072549, 7906935755119598122, 7914194807915183145, + 8039495272051275925, 8314689053957053388, 8387542429457488819, + 8466597753541104284, 8483741723290222858, 8599301570471318808, + 8716226300070556585, 8756763689271138139, 8955295406776997764, + 9012673292027226953, 9036242292240002258}); + instance.streams.push_back( + {221152499512725057, 468653913289994511, 573406392190002398, + 608549633585505280, 721590223379320754, 893804687502387698, + 994645852819618858, 1010504207871866436, 1226458553438834225, + 1322102128875218366, 1352617939551229058, 1367657169503026665, + 1431054573484265696, 1716664507247859085, 1819808470744051546, + 1832883987492520423, 1913575203897912572, 2078343662767049992, + 2144184772162852472, 2175338692854700871, 2515520083286899021, + 2658219409155723888, 2662216173057158227, 2791102212542615133, + 2810251461404023989, 3010160067457807675, 3198247003463721391, + 3207210396276830453, 3655687150569838438, 3742949808956029213, + 3939408096155747775, 3960751161684758408, 4062454131612436236, + 4126733056338249731, 4144406660403408900, 4160281903605117507, + 4160886814405501932, 4301845892945418166, 4335382680745413153, + 4505056660537898379, 4539434107911381923, 4559707416640177661, + 4581627552336714482, 4634932691215475440, 4652256940502250041, + 4709034174943716141, 4755067195445668372, 5031870437171497866, + 5086241845842489323, 5194885361498644327, 5335820239968792638, + 5381866204428555333, 5496620869292551625, 5607696027294180101, + 5641888110506579814, 5669980928511650063, 5692882051604805422, + 5869033276887872018, 5921828484003683005, 6018591452797071010, + 6043330380531903780, 6073539888671339222, 6349804831539426958, + 6466951481745709342, 6541925016453000741, 6606577194341690198, + 6655821998787366041, 6869403876421968685, 6952236217247677418, + 7064632969560758954, 7262799951330799140, 7299731795147493418, + 7452516877284055758, 7516278093926782953, 7724657529939371422, + 8277537304437407399, 8287042823318847978, 8375033050716909821, + 8383421555375425502, 8502394728509959035, 8502811416815954103, + 8601942285631187625, 8668259048921332853, 8679917789291195612, + 8902185247016931860, 8975374683294029673, 9018123807247419003, + 9081591481240593402, 9203064079702243357}); + instance.streams.push_back( + {9528858636270754, 74767308924267190, 92523084385092125, + 282510914521185986, 565939653640943279, 766893259352706773, + 1025334812935058435, 1086642043696211947, 1090103539577005545, + 1117559866996102610, 1150892608993861532, 1431520706401660610, + 1461925516829895752, 1491154270719434347, 1564570908804651001, + 1686708764851230308, 1693018175221748741, 1827964802829643811, + 1871856400612904987, 2269031422942301096, 2321184329693335268, + 2576411078823723038, 2901956572209495751, 3299554731045776001, + 3578582730292621865, 3605481165030718200, 3622286981235981834, + 4644544455473625042, 4704715844976049230, 4707408952461877611, + 5079828316722713851, 5332860656232543172, 5357945905588425444, + 5462910895481110403, 5486651617427880829, 5920963317972710651, + 5945547189608426266, 6097995450464204286, 6155072814328938262, + 6427618809519937314, 6498736386875881011, 6557345715154802672, + 6591361732789478133, 6795736613838753671, 7063310025635337958, + 7288379786078052405, 7477826447866021589, 7544407848018322037, + 7864854448541658478, 7924809231654480847, 8155610344096072120, + 8168318381948177277, 8489138127444983219, 8533218767481812480, + 8899267526102130921, 9051763646967677194}); + instance.streams.push_back( + {1160239440999495868, 1160627100977908024, 1445977391255418495, + 2100709699684953613, 2311002752371175131, 2326987246415777165, + 2409509260037050400, 2770408159376628543, 2885970397539782206, + 2897849329158521471, 3073323606059489201, 3097044942437084142, + 3169575954896219039, 3319951716499728030, 3494135475371065426, + 3496002297262349575, 3841375314084544552, 4007888085756092075, + 4717250350765676101, 4830685926850246039, 5066619425573186987, + 5142827563397788282, 5840091769545424048, 6185402624342137099, + 6520242562806050579, 7340354838898445609, 7446287944723287024, + 7819425906479635150, 8179247966270686645, 8793860582000764117, + 8876989220535143815, 8954309083312314973, 9189126043219875235}); + instance.streams.push_back( + {285145543975647006, 400464406369380005, 425052447908362043, + 558717929621862200, 780184890604101452, 802552417514008088, + 821353789735020113, 1461771191595340049, 1707838069528986431, + 1968758736472278720, 2179970727013701927, 2301096876229371288, + 2586755882369725036, 2642888823133285734, 2724932478710618595, + 2799219108908357183, 3210508660878406295, 3390360820647531457, + 3500608069640623603, 3794000156360803026, 3870372465245409129, + 3962864942943613290, 4052594741754433954, 4066439209995535105, + 4625364126734950672, 4755100478414716611, 4837056974522792783, + 5138435736439382420, 5252516194543331360, 5276885357297734928, + 5339285312869309795, 5383414072988834799, 5419750407305078642, + 5504475032307800929, 5531444670009604260, 5610198686052584931, + 5879342333105160359, 6227163917223061198, 6394549805539869304, + 6534105811771329013, 6538298296181075612, 6683665059254528394, + 6915570322469724208, 7215054837440533904, 7281318269792757267, + 7374371223175351465, 7562113046836517303, 7838864936942600980, + 7844181398943447729, 7942351656727780460, 7950201023600342051, + 8016629451324720863, 8044191201507598865, 8229239515369106446, + 8993425554747953330}); + instance.streams.push_back( + {125583000667520524, 214839438467369427, 249367830951934430, + 308261031714688831, 1134768329775635958, 1183613722982518046, + 1357058262944026234, 1357710861438500066, 1748413022866802117, + 1857598835930134266, 3055019199902384006, 3062281030982283629, + 3528678498241652912, 4063396096182487012, 4467122486929931357, + 4846856950656598934, 5785035888379523440, 5855646996685670595, + 6095919523788855289, 6205455101336312663, 6666077315644367652, + 6757656301020430328, 7275449995231256065, 7306530842416062343, + 7409626532873823691, 7534989846036976651, 7970400668449759273, + 8069921964831893849, 8196425277077193920, 8389499930778050745, + 8701072344837825080}); + instance.streams.push_back( + {33564926325826831, + 260291524539990308, + 374076536813175702, + 445706191910983060, + 2615929502664660778, + 2870926401217071659, + 3088279460307954084, + 4633051241451350592, + 4636848118939709473, + 4982408764483706312, + 6145844808415620138, + 6503848779295974662, + 6803286364456511128, + 7301808229090216723, + 7527961052034746578, + 8755659592883848787}); + instance.streams.push_back( + {50391867020209568, 109973211041671007, 426365914246397281, + 780216749836386995, 927137594156658301, 1280297890312031835, + 1356872469290895499, 1646520938759084189, 1770128407464172181, + 1876106980342957607, 1992958432625854788, 2093272732169720418, + 2241800693961349596, 2315318181472203645, 2726778221200855105, + 2731148968788143056, 2784451205304767311, 2889302029852103008, + 2914456568933696331, 3033308498900870284, 3069608453528247024, + 3147683791728929502, 3162396888809983919, 3173374637506992417, + 3306363691517815071, 3567685134602033707, 3732317047444193546, + 3864954392412328092, 4304660060986936816, 4666673387047506586, + 4765432491107097576, 5108976781632236584, 5110400105803816066, + 5200335758573175396, 5640989876800271598, 5665274647100247796, + 5702149274760003174, 5728664901759643253, 5788098188481174646, + 5795560782524298146, 5852686300889056461, 5973152740219653583, + 5977429576954595040, 6103223664281897127, 6111609016377058074, + 6235168972471541048, 6237552891612236200, 6335487740557681471, + 6353432702630052474, 6365163378435576774, 6779431742606687646, + 7055429296271398220, 7142092728874400757, 7160088139694496237, + 7161080095306591569, 7195482977425683059, 7265328107229114597, + 7323761413577848136, 7330533757178675459, 7346545798057982568, + 7543397985293350879, 7605471257315203444, 7677075232577432180, + 7770682335946669870, 7801171200783882642, 7879542219695847235, + 7889434293679595443, 8027893157968605900, 8131696298458998640, + 8221580404217896279, 8287527045961328812, 8340326440093299701, + 8355257415634238153, 8638160453384811767, 8654116231443860953, + 8713028169329039008, 8741251814621908606}); + instance.streams.push_back( + {38552260850125970, 306379511766359735, 711300096798339321, + 892018019103254274, 2364206832643103382, 2420535690560486819, + 2980386295304646523, 3101600218221266377, 3141790607746941505, + 3999134727658658069, 4462290689715673199, 5204914504379723995, + 5467769513963022713, 5963515685465164797, 6307703548487249061, + 6521727204313461172, 6746273471781072806, 7133296936471547422, + 7413405836526190068, 7460132468170665268, 9118746925568133696}); + instance.streams.push_back( + {311746355635346537, 357280282242159933, 453794801155624312, + 470124658103437043, 480500591405735777, 642968216130651766, + 677382766653156986, 742336241959437289, 995597266969363512, + 1438905128019740072, 1774939855749446509, 2534734553076187949, + 2675412948582352200, 2945299999579094336, 3085388497824334414, + 3244765152133405099, 3327833825330566369, 3703194822023351032, + 3765658082600741596, 3825281533385903512, 3883149142639520189, + 3938352829216712487, 4120683798246541427, 4407478830753418084, + 4714722467734011191, 4778090814542958798, 4865684247595699604, + 5564250447415400601, 5675380593702259207, 5953049262426054107, + 6322306904987766773, 6576158849963679983, 6585910776451113262, + 6695013865519715556, 6822580595787479730, 7005579721591160602, + 7053187268953133165, 7137337825055068764, 7154614606682799364, + 7162695738484944585, 7204207140983511889, 7657343589727357634, + 7726203520276412700, 7885282374693638275, 7898080252964135291, + 8063562120530344324, 8111621434828014272, 8136473663541304080, + 8955369150293893931, 8974612926096998142}); + instance.streams.push_back( + {197420451713811676, 281445277459917203, 740082088833530644, + 1290412288467608392, 1294985646752861418, 1459338324889185197, + 1545467342772798788, 1725296824044696622, 1790928042402142285, + 1819195369020482914, 1845318846420546596, 1916804860036750684, + 1934795655439316734, 2123406663108189928, 2161364767387674755, + 2199349719437568153, 2201446205661462318, 2304422276794393017, + 2367362048086896201, 2520840158493853225, 2637488475771115834, + 2695468587025769956, 2868497673804512236, 2986730852667904426, + 3049027770127401337, 3088814443181055397, 3142185799974885998, + 3145610541985422508, 3376917417266132278, 3474311476906199144, + 3480295733245426278, 3756461875941152626, 3768960023228980524, + 3786412095536719213, 3818558100696073513, 3832731138330958677, + 4013125545855280982, 4058451558097473690, 4061654997981645019, + 4079781890597804187, 4145540956726304260, 4213067535490157135, + 4387646299216426469, 4532586244009288120, 4599368551837569974, + 4719137179319807721, 4774240273842121853, 4823692999474104868, + 4878380451678305609, 5080805857519288366, 5272561874891052360, + 5393458881428272808, 5758340275080427297, 5886639535186242096, + 5912173887580571910, 6054663179080587357, 6138520848359196864, + 6388603114548809966, 6409577084921583203, 6536152899730873147, + 6594754796754003142, 6835272684932377793, 6860056536556710109, + 7219012018184660258, 7316465780676346496, 7661428841823577249, + 7906168207960936451, 8184686816308280852, 8916752945036324140, + 8985070033502476478, 9145582347483890099}); + instance.streams.push_back( + {153324836101059419, 158780138759790326, 174215935958616755, + 179731076471247779, 269276934678469319, 393639793694666172, + 432631673890631004, 670995085036662336, 721887896591779145, + 748122147291880857, 771326157253533848, 783650624759832650, + 1014975687794962171, 1379488317060232881, 1463947847163713623, + 1559978541312388344, 1677050512347768440, 1679788471700081953, + 1697344722681459414, 1796670363905560694, 1875437320199800047, + 1958046921272092141, 2012390301428218497, 2032697606825981501, + 2063860587455918843, 2076755491339447914, 2163553294232027512, + 2185286087988362175, 2223845822471630205, 2508102531103681824, + 2576117223285183167, 2714809954419134452, 2792838922831161963, + 2900435853024427713, 2937083093165162409, 2939480550795499937, + 3072243433185798666, 3228964562507837847, 3257430266232071473, + 3401500904443689432, 3410334239187579089, 3620509784201584781, + 3707729414802869989, 3750258169852724918, 3756680820327526039, + 3900685920024254630, 4024598042066710501, 4188274758129042402, + 4323638211698224578, 4580570891550236607, 5009980347824435138, + 5122166740162881305, 5329562660783230510, 5372442803515899785, + 5418173101818824583, 5485499651120889655, 5521202335275584192, + 5729929930779710255, 5831082418654309238, 5982740560563910312, + 5995159670704615124, 6088662697234208829, 6120996226448363060, + 6214200379979203888, 6468448109110284451, 6525029247710148615, + 6581357662703674942, 6780008824250326220, 6875650962481044987, + 6938461292439554984, 7027602581413514955, 7100073000167249186, + 7208005517459406600, 7228453795929202340, 7603315537325918661, + 7611050507858817021, 7644795789683514694, 7868219793368483911, + 7966758117976278992, 8011601341464333596, 8077164278247408893, + 8181466141626535480, 8771155570066914574, 8879865897468670264, + 8885481783436167324, 9025760847000160788, 9164430375972757624, + 9173963918183109716}); + instance.streams.push_back( + {1059433874806495660, 2105370760666799235, 2550457278644318367, + 2588733214791715635, 3081056091507702768, 3536661695747334932, + 3642559614667528631, 3844174653456592779, 3874828053345872276, + 4259604692437009810, 4395405895302928283, 4805831959048227379, + 5035667734207638846, 5089440685667631502, 5170000450239185361, + 5544572753180138321, 5748328650770747767, 6212120819500360922, + 6812019836498165705, 6962001610179228000, 7132285316145578530, + 7511954185336675079, 7912817484509858870, 8708158977779035254, + 8907676047014618471, 8913855313747888928}); + + instance.verify(); + } + + SECTION("Rapidcheck") { + rc::prop("verify_parallel_merge", [](VerifyParallelMerge input) { + input.verify(); + }); + } +} + +TEST_CASE( + "parallel merge rapidcheck int8_t VerifySplitPointStream", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_split_point_stream_bounds", + [](VerifySplitPointStream input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck int8_t VerifyIdentifyMergeUnit", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_identify_merge_unit", + [](VerifyIdentifyMergeUnit input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck int8_t VerifyTournamentMerge", + "[algorithm][parallel_merge]") { + rc::prop("verify_tournament_merge", [](VerifyTournamentMerge input) { + input.verify(); + }); +} + +TEST_CASE( + "parallel merge rapidcheck int8_t VerifyParallelMerge", + "[algorithm][parallel_merge]") { + rc::prop("verify_parallel_merge", [](VerifyParallelMerge input) { + input.verify(); + }); +} + +struct OneDigit { + uint8_t value_; + + operator uint8_t() const { + return value_; + } +}; + +namespace rc { +template <> +struct Arbitrary { + static Gen arbitrary() { + return gen::map(gen::inRange(0, 10), [](uint8_t value) { + RC_PRE(0 <= value && value < 10); + return OneDigit{.value_ = value}; + }); + } +}; +} // namespace rc + +TEST_CASE( + "parallel merge rapidcheck OneDigit VerifySplitPointStream", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_split_point_stream_bounds", + [](VerifySplitPointStream input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck OneDigit VerifyIdentifyMergeUnit", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_identify_merge_unit", + [](VerifyIdentifyMergeUnit input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck OneDigit VerifyTournamentMerge", + "[algorithm][parallel_merge]") { + rc::prop( + "verify_tournament_merge", + [](VerifyTournamentMerge input) { input.verify(); }); +} + +TEST_CASE( + "parallel merge rapidcheck OneDigit VerifyParallelMerge", + "[algorithm][parallel_merge]") { + rc::prop("verify_parallel_merge", [](VerifyParallelMerge input) { + input.verify(); + }); +} + +TEST_CASE("parallel merge example", "[algorithm][parallel_merge]") { + std::vector output(20); + + ParallelMergeOptions options = {.parallel_factor = 4, .min_merge_items = 4}; + + auto memory_tracker = get_test_memory_tracker(); + ParallelMergeMemoryResources resources(*memory_tracker.get()); + + ThreadPool pool(4); + + SECTION("less") { + auto cmp = std::less{}; + + const std::vector expect = {24, 100, 123, 135, 200, 246, 300, + 357, 400, 456, 468, 500, 579, 680, + 789, 791, 802, 890, 901, 913}; + + auto future = parallel_merge( + pool, resources, options, EXAMPLE_STREAMS, cmp, &output[0]); + + future->block(); + + CHECK(output.size() == expect.size()); + CHECK(output == expect); + } + + SECTION("greater") { + auto cmp = std::greater{}; + + auto descending = EXAMPLE_STREAMS; + for (auto& stream : descending) { + std::sort(stream.begin(), stream.end(), std::greater<>()); + } + + const std::vector expect = {913, 901, 890, 802, 791, 789, 680, + 579, 500, 468, 456, 400, 357, 300, + 246, 200, 135, 123, 100, 24}; + + auto future = + parallel_merge(pool, resources, options, descending, cmp, &output[0]); + + future->block(); + + CHECK(output.size() == expect.size()); + CHECK(output == expect); + } +} diff --git a/tiledb/common/memory_tracker.cc b/tiledb/common/memory_tracker.cc index 47601320d91..787bd64f773 100644 --- a/tiledb/common/memory_tracker.cc +++ b/tiledb/common/memory_tracker.cc @@ -76,6 +76,8 @@ std::string memory_type_to_str(MemoryType type) { return "GenericTileIO"; case MemoryType::METADATA: return "Metadata"; + case MemoryType::PARALLEL_MERGE_CONTROL: + return "ParallelMergeControl"; case MemoryType::QUERY_CONDITION: return "QueryCondition"; case MemoryType::RESULT_TILE: diff --git a/tiledb/common/memory_tracker.h b/tiledb/common/memory_tracker.h index 67afebf5a81..e56ec902b11 100644 --- a/tiledb/common/memory_tracker.h +++ b/tiledb/common/memory_tracker.h @@ -122,6 +122,7 @@ enum class MemoryType { GENERIC_TILE_IO, METADATA, QUERY_CONDITION, + PARALLEL_MERGE_CONTROL, RESULT_TILE, RESULT_TILE_BITMAP, RTREE, diff --git a/tiledb/common/pmr.h b/tiledb/common/pmr.h index dccbd15a650..3024ffc9d97 100644 --- a/tiledb/common/pmr.h +++ b/tiledb/common/pmr.h @@ -75,24 +75,31 @@ class unique_ptr_deleter { public: unique_ptr_deleter() = delete; - unique_ptr_deleter(memory_resource* resource, size_t size, size_t alignment) + unique_ptr_deleter(memory_resource* resource, size_t nmemb, size_t alignment) : resource_(resource) - , size_(size) + , nmemb_(nmemb) , alignment_(alignment) { } void operator()(Tp* ptr) { - if (ptr != nullptr) { - resource_->deallocate(ptr, size_, alignment_); + if (ptr == nullptr) { + return; + } + if (!std::is_trivially_destructible::value) { + // destruct in reverse order since the elements are constructed in + // forwards order + for (size_t i = nmemb_; i > 0; i--) { + ptr[i - 1].~Tp(); + } } - } - void set_size(size_t size) { - size_ = size; + const size_t dealloc_size = nmemb_ * sizeof(Tp); + resource_->deallocate(ptr, dealloc_size, alignment_); } memory_resource* resource_; - size_t size_; + /** number of members of the array */ + size_t nmemb_; size_t alignment_; }; @@ -101,24 +108,37 @@ using unique_ptr = std::unique_ptr>; template unique_ptr make_unique( - memory_resource* resource, size_t size, size_t alignment) { + memory_resource* resource, size_t nmemb, size_t alignment) { static_assert(std::is_arithmetic_v || std::is_same_v); - auto alloc_size = size * sizeof(Tp); + auto alloc_size = nmemb * sizeof(Tp); Tp* data = static_cast(resource->allocate(alloc_size, alignment)); if (data == nullptr) { throw std::bad_alloc(); } - auto deleter = unique_ptr_deleter(resource, alloc_size, alignment); + auto deleter = unique_ptr_deleter(resource, nmemb, alignment); return std::unique_ptr>(data, deleter); } template -unique_ptr make_unique(memory_resource* resource, size_t size) { - return make_unique(resource, size, alignof(Tp)); +unique_ptr make_unique(memory_resource* resource, size_t nmemb) { + return make_unique(resource, nmemb, alignof(Tp)); +} + +/** + * Constructs an object in place and returns it in a `unique_ptr`. + */ +template +unique_ptr emplace_unique(memory_resource* resource, Args&&... args) { + Tp* obj = static_cast(resource->allocate(sizeof(Tp), alignof(Tp))); + new (obj) Tp(std::forward(args)...); + + auto deleter = unique_ptr_deleter(resource, 1, alignof(Tp)); + + return std::unique_ptr>(obj, deleter); } /* ********************************* */ diff --git a/tiledb/common/thread_pool/producer_consumer_queue.h b/tiledb/common/thread_pool/producer_consumer_queue.h index b4202ee555c..6afcaaee7be 100644 --- a/tiledb/common/thread_pool/producer_consumer_queue.h +++ b/tiledb/common/thread_pool/producer_consumer_queue.h @@ -148,10 +148,10 @@ class ProducerConsumerQueue { if (closed_ && queue_.empty()) { return {}; } - Item item = queue_.front(); + Item item = std::move(queue_.front()); if constexpr (std::is_same>::value) { - queue_.pop(item); + queue_.pop(); } else if constexpr (std::is_same>::value) { queue_.pop_front(); } else { diff --git a/tiledb/common/types/untyped_datum.h b/tiledb/common/types/untyped_datum.h index 94e1a684e50..1f61e681ea7 100644 --- a/tiledb/common/types/untyped_datum.h +++ b/tiledb/common/types/untyped_datum.h @@ -45,6 +45,11 @@ class UntypedDatumView { : datum_content_(content) , datum_size_(size) { } + UntypedDatumView(std::string_view ss) + : datum_content_(ss.data()) + , datum_size_(ss.size()) { + } + [[nodiscard]] inline const void* content() const { return datum_content_; } diff --git a/tiledb/sm/array_schema/domain.h b/tiledb/sm/array_schema/domain.h index 365bcb1021f..97092f1582c 100644 --- a/tiledb/sm/array_schema/domain.h +++ b/tiledb/sm/array_schema/domain.h @@ -255,6 +255,13 @@ class Domain { return dimensions_[i]; } + /** + * @return a view over the dimensions + */ + inline std::span> dimensions() const { + return std::span(dimensions_.begin(), dimensions_.end()); + } + /** Returns the dimension given a name (nullptr upon error). */ const Dimension* dimension_ptr(const std::string& name) const; diff --git a/tiledb/sm/config/config.cc b/tiledb/sm/config/config.cc index 4e15c56702c..1d434704df8 100644 --- a/tiledb/sm/config/config.cc +++ b/tiledb/sm/config/config.cc @@ -118,6 +118,8 @@ const std::string Config::SM_MEMORY_BUDGET_VAR = "10737418240"; // 10GB const std::string Config::SM_QUERY_DENSE_QC_COORDS_MODE = "false"; const std::string Config::SM_QUERY_DENSE_READER = "refactored"; const std::string Config::SM_QUERY_SPARSE_GLOBAL_ORDER_READER = "refactored"; +const std::string Config::SM_QUERY_SPARSE_GLOBAL_ORDER_PREPROCESS_TILE_MERGE = + "32768"; const std::string Config::SM_QUERY_SPARSE_UNORDERED_WITH_DUPS_READER = "refactored"; const std::string Config::SM_MEM_MALLOC_TRIM = "true"; @@ -315,6 +317,9 @@ const std::map default_config_values = { std::make_pair( "sm.query.sparse_global_order.reader", Config::SM_QUERY_SPARSE_GLOBAL_ORDER_READER), + std::make_pair( + "sm.query.sparse_global_order.preprocess_tile_merge", + Config::SM_QUERY_SPARSE_GLOBAL_ORDER_PREPROCESS_TILE_MERGE), std::make_pair( "sm.query.sparse_unordered_with_dups.reader", Config::SM_QUERY_SPARSE_UNORDERED_WITH_DUPS_READER), @@ -522,7 +527,7 @@ const std::map default_config_values = { "vfs.hdfs.kerb_ticket_cache_path", Config::VFS_HDFS_KERB_TICKET_CACHE_PATH), std::make_pair("filestore.buffer_size", Config::FILESTORE_BUFFER_SIZE), -}; +}; // namespace tiledb::sm /* ****************************** */ /* PRIVATE CONSTANTS */ diff --git a/tiledb/sm/config/config.h b/tiledb/sm/config/config.h index ef8dda564ca..204facf4249 100644 --- a/tiledb/sm/config/config.h +++ b/tiledb/sm/config/config.h @@ -233,6 +233,16 @@ class Config { /** Which reader to use for sparse global order queries. */ static const std::string SM_QUERY_SPARSE_GLOBAL_ORDER_READER; + /** + * If nonzero, prior to loading the first tiles, the reader will run + * a preprocessing step to arrange tiles from all fragments in a single + * globally ordered list. This is expected to improve performance when + * there are many fragments or when the distribution in space of the + * tiles amongst the fragments is skewed. The value of the parameter + * specifies the amount of work per parallel task. + */ + static const std::string SM_QUERY_SPARSE_GLOBAL_ORDER_PREPROCESS_TILE_MERGE; + /** Which reader to use for sparse unordered with dups queries. */ static const std::string SM_QUERY_SPARSE_UNORDERED_WITH_DUPS_READER; diff --git a/tiledb/sm/cpp_api/config.h b/tiledb/sm/cpp_api/config.h index 4860429d3d4..e9e7ab64a48 100644 --- a/tiledb/sm/cpp_api/config.h +++ b/tiledb/sm/cpp_api/config.h @@ -427,6 +427,16 @@ class Config { * Which reader to use for sparse global order queries. "refactored" * or "legacy".
* **Default**: refactored + * - `sm.query.sparse_global_order.preprocess_tile_merge`
+ * **Experimental for testing purposes, do not use.**
+ * Performance configuration for sparse global order read queries. + * If nonzero, prior to loading the first tiles, the reader will run + * a preprocessing step to arrange tiles from all fragments in a single + * globally ordered list. This is expected to improve performance when + * there are many fragments or when the distribution in space of the + * tiles amongst the fragments is skewed. The value of the parameter + * specifies the amount of work per parallel task. + * **Default**: "32768" * - `sm.query.sparse_unordered_with_dups.reader`
* Which reader to use for sparse unordered with dups queries. * "refactored" or "legacy".
diff --git a/tiledb/sm/misc/comparators.h b/tiledb/sm/misc/comparators.h index 3bcac5efc1b..37bb260cabe 100644 --- a/tiledb/sm/misc/comparators.h +++ b/tiledb/sm/misc/comparators.h @@ -40,14 +40,69 @@ #include "tiledb/sm/array_schema/domain.h" #include "tiledb/sm/enums/layout.h" +#include "tiledb/sm/misc/type_traits.h" #include "tiledb/sm/query/readers/result_coords.h" #include "tiledb/sm/query/readers/sparse_global_order_reader.h" #include "tiledb/sm/query/writers/domain_buffer.h" -using namespace tiledb::common; +namespace stdx { + +/** + * Generic comparator adapter which reverses the comparison arguments. + * For a comparison `c(a, b)`, this returns `c(b, a)`. + */ +template +struct reverse_comparator { + Comparator inner_; + + template + reverse_comparator(Args&&... args) + : inner_(std::forward(args)...) { + } + + template + bool operator()(const L& a, const R& b) const { + return inner_(b, a); + } +}; + +/** + * Generic comparator adapter which transforms a `int compare(a, b)` function + * into a `bool operator` which returns true if `a <= b` + * (whereas a typical comparator's `bool operator` returns true if `a < b`). + */ +template +struct or_equal { + Comparator inner_; + + template + or_equal(Args&&... args) + : inner_(std::forward(args)...) { + } + + template + bool operator()(const L& a, const R& b) const { + return inner_.compare(a, b) <= 0; + } +}; + +} // namespace stdx namespace tiledb::sm { +using namespace tiledb::common; + +namespace cell_compare { +template +int compare( + const Domain& domain, unsigned int d, const RCTypeL& a, const RCTypeR& b) { + const auto& dim{*(domain.dimension_ptr(d))}; + auto v1{a.dimension_datum(dim, d)}; + auto v2{b.dimension_datum(dim, d)}; + return domain.cell_order_cmp(d, v1, v2); +} +} // namespace cell_compare + class CellCmpBase { protected: /** The domain. */ @@ -56,45 +111,16 @@ class CellCmpBase { /** The number of dimensions. */ const unsigned dim_num_; - /** Use timestamps or not during comparison. */ - const bool use_timestamps_; - - /** Enforce strict ordering for the comparator if used in a queue. */ - const bool strict_ordering_; - - /** Pointer to access fragment metadata. */ - const std::vector>* frag_md_; - public: - explicit CellCmpBase( - const Domain& domain, - const bool use_timestamps = false, - const bool strict_ordering = false, - const std::vector>* frag_md = nullptr) + explicit CellCmpBase(const Domain& domain) : domain_(domain) - , dim_num_(domain.dim_num()) - , use_timestamps_(use_timestamps) - , strict_ordering_(strict_ordering) - , frag_md_(frag_md) { + , dim_num_(domain.dim_num()) { } - template + template [[nodiscard]] int cell_order_cmp_RC( - unsigned int d, const RCType& a, const RCType& b) const { - const auto& dim{*(domain_.dimension_ptr(d))}; - auto v1{a.dimension_datum(dim, d)}; - auto v2{b.dimension_datum(dim, d)}; - return domain_.cell_order_cmp(d, v1, v2); - } - - template - uint64_t get_timestamp(const RCType& rc) const { - const auto f = rc.tile_->frag_idx(); - if ((*frag_md_)[f]->has_timestamps()) { - return rc.tile_->timestamp(rc.pos_); - } else { - return (*frag_md_)[f]->timestamp_range().first; - } + unsigned int d, const RCTypeL& a, const RCTypeR& b) const { + return cell_compare::compare(domain_, d, a, b); } }; @@ -161,8 +187,42 @@ class ColCmp : CellCmpBase { } }; +class ResultTileCmpBase : public CellCmpBase { + protected: + /** Use timestamps or not during comparison. */ + const bool use_timestamps_; + + /** Enforce strict ordering for the comparator if used in a queue. */ + const bool strict_ordering_; + + /** Pointer to access fragment metadata. */ + const std::vector>* frag_md_; + + public: + explicit ResultTileCmpBase( + const Domain& domain, + const bool use_timestamps = false, + const bool strict_ordering = false, + const std::vector>* frag_md = nullptr) + : CellCmpBase(domain) + , use_timestamps_(use_timestamps) + , strict_ordering_(strict_ordering) + , frag_md_(frag_md) { + } + + template + uint64_t get_timestamp(const RCType& rc) const { + const auto f = rc.tile_->frag_idx(); + if ((*frag_md_)[f]->has_timestamps()) { + return rc.tile_->timestamp(rc.pos_); + } else { + return (*frag_md_)[f]->timestamp_range().first; + } + } +}; + /** Wrapper of comparison function for sorting coords on Hilbert values. */ -class HilbertCmp : public CellCmpBase { +class HilbertCmp : public ResultTileCmpBase { public: /** Constructor. */ HilbertCmp( @@ -170,7 +230,7 @@ class HilbertCmp : public CellCmpBase { const bool use_timestamps = false, const bool strict_ordering = false, const std::vector>* frag_md = nullptr) - : CellCmpBase(domain, use_timestamps, strict_ordering, frag_md) { + : ResultTileCmpBase(domain, use_timestamps, strict_ordering, frag_md) { } /** @@ -316,11 +376,118 @@ class HilbertCmpRCI : protected CellCmpBase { } }; +template +struct global_order_compare { + template + static int compare( + const Domain& domain, const GlobalCmpL& a, const GlobalCmpR& b) { + const auto num_dims = domain.dim_num(); + + for (unsigned di = 0; di < num_dims; ++di) { + const unsigned d = + (TILE_ORDER == Layout::ROW_MAJOR ? di : (num_dims - di - 1)); + + // Not applicable to var-sized dimensions + if (domain.dimension_ptr(d)->var_size()) + continue; + + auto res = domain.tile_order_cmp(d, a.coord(d), b.coord(d)); + if (res != 0) { + return res; + } + // else same tile on dimension d --> continue + } + + // then cell order + for (unsigned di = 0; di < num_dims; ++di) { + const unsigned d = + (CELL_ORDER == Layout::ROW_MAJOR ? di : (num_dims - di - 1)); + auto res = cell_compare::compare(domain, d, a, b); + + if (res != 0) { + return res; + } + // else same tile on dimension d --> continue + } + + // NB: some other comparators care about timestamps here, we will not bother + // (for now?) + return 0; + } +}; + +template +class GlobalCellCmpStaticDispatch : public CellCmpBase { + public: + explicit GlobalCellCmpStaticDispatch(const Domain& domain) + : CellCmpBase(domain) { + static_assert( + TILE_ORDER == Layout::ROW_MAJOR || TILE_ORDER == Layout::COL_MAJOR); + static_assert( + CELL_ORDER == Layout::ROW_MAJOR || CELL_ORDER == Layout::COL_MAJOR); + } + + template + bool operator()(const GlobalCmpL& a, const GlobalCmpR& b) const { + return global_order_compare::compare( + domain_, a, b) < 0; + } +}; + +class GlobalCellCmp : public CellCmpBase { + public: + GlobalCellCmp(const Domain& domain) + : CellCmpBase(domain) + , tile_order_(domain.tile_order()) + , cell_order_(domain.cell_order()) { + } + + template + int compare(const GlobalCmpL& a, const GlobalCmpR& b) const { + if (tile_order_ == Layout::ROW_MAJOR) { + if (cell_order_ == Layout::ROW_MAJOR) { + return global_order_compare:: + compare(domain_, a, b); + } else { + return global_order_compare:: + compare(domain_, a, b); + } + } else { + if (cell_order_ == Layout::ROW_MAJOR) { + return global_order_compare:: + compare(domain_, a, b); + } else { + return global_order_compare:: + compare(domain_, a, b); + } + } + } + + template + bool operator()(const GlobalCmpL& a, const GlobalCmpR& b) const { + return compare(a, b) < 0; + } + + private: + /** The tile order. */ + Layout tile_order_; + /** The cell order. */ + Layout cell_order_; +}; + +template +concept GlobalTileComparable = + GlobalCellComparable and requires(const T& a, uint64_t pos) { + { a.fragment_idx() } -> std::convertible_to; + { a.tile_idx() } -> std::convertible_to; + { a.tile_->timestamp(pos) } -> std::same_as; + }; + /** * Wrapper of comparison function for sorting coords on the global order * of some domain. */ -class GlobalCmp : public CellCmpBase { +class GlobalCmp : public ResultTileCmpBase { public: /** * Constructor. @@ -336,98 +503,46 @@ class GlobalCmp : public CellCmpBase { const bool use_timestamps = false, const bool strict_ordering = false, const std::vector>* frag_md = nullptr) - : CellCmpBase(domain, use_timestamps, strict_ordering, frag_md) { - tile_order_ = domain.tile_order(); - cell_order_ = domain.cell_order(); + : ResultTileCmpBase(domain, use_timestamps, strict_ordering, frag_md) + , cellcmp_(domain) { } /** - * Comparison operator for a vector of `ResultCoords`. + * Comparison operator for a vector of `GlobalTileComparable`. * * @param a The first coordinate. * @param b The second coordinate. * @return `true` if `a` precedes `b` and `false` otherwise. */ - template - bool operator()(const RCType& a, const RCType& b) const { - if (tile_order_ == Layout::ROW_MAJOR) { - for (unsigned d = 0; d < dim_num_; ++d) { - // Not applicable to var-sized dimensions - if (domain_.dimension_ptr(d)->var_size()) - continue; - - auto res = domain_.tile_order_cmp(d, a.coord(d), b.coord(d)); - - if (res == -1) - return true; - if (res == 1) - return false; - // else same tile on dimension d --> continue - } - } else { // COL_MAJOR - assert(tile_order_ == Layout::COL_MAJOR); - for (int32_t d = static_cast(dim_num_) - 1; d >= 0; d--) { - // Not applicable to var-sized dimensions - if (domain_.dimension_ptr(d)->var_size()) - continue; - - auto res = domain_.tile_order_cmp(d, a.coord(d), b.coord(d)); - - if (res == -1) - return true; - if (res == 1) - return false; - // else same tile on dimension d --> continue - } - } - - // Compare cell order - if (cell_order_ == Layout::ROW_MAJOR) { - for (unsigned d = 0; d < dim_num_; ++d) { - auto res = cell_order_cmp_RC(d, a, b); - - if (res == -1) - return true; - if (res == 1) - return false; - // else same tile on dimension d --> continue - } - } else { // COL_MAJOR - assert(cell_order_ == Layout::COL_MAJOR); - for (int32_t d = static_cast(dim_num_) - 1; d >= 0; d--) { - auto res = cell_order_cmp_RC(d, a, b); - - if (res == -1) - return true; - if (res == 1) - return false; - // else same tile on dimension d --> continue - } + template + bool operator()(const GlobalCmpL& a, const GlobalCmpR& b) const { + const int cellcmp = cellcmp_.compare(a, b); + if (cellcmp < 0) { + return true; + } else if (cellcmp > 0) { + return false; } // Compare timestamps if (use_timestamps_) { return get_timestamp(a) > get_timestamp(b); } else if (strict_ordering_) { - if (a.tile_->frag_idx() == b.tile_->frag_idx()) { - if (a.tile_->tile_idx() == b.tile_->tile_idx()) { + if (a.fragment_idx() == b.fragment_idx()) { + if (a.tile_idx() == b.tile_idx()) { return a.pos_ > b.pos_; } - return a.tile_->tile_idx() > b.tile_->tile_idx(); + return a.tile_idx() > b.tile_idx(); } - return a.tile_->frag_idx() > b.tile_->frag_idx(); + return a.fragment_idx() > b.fragment_idx(); } return false; } private: - /** The tile order. */ - Layout tile_order_; - /** The cell order. */ - Layout cell_order_; + GlobalCellCmp cellcmp_; }; /** @@ -460,8 +575,8 @@ class GlobalCmpReverse { * @param b The second coordinate. * @return `true` if `a` precedes `b` and `false` otherwise. */ - template - bool operator()(const RCType& a, const RCType& b) const { + template + bool operator()(const GlobalCmpL& a, const GlobalCmpR& b) const { return !cmp_.operator()(a, b); } @@ -595,6 +710,36 @@ class HilbertCmpQB : protected DomainValueCmpBaseQB { } }; +/** + * View into NDRange for using the range start / lower bound in comparisons. + */ +struct RangeLowerBound { + const NDRange& mbr; + + const void* coord(unsigned dim) const { + return mbr[dim].data(); + } + + UntypedDatumView dimension_datum(const Dimension&, unsigned dim) const { + return mbr[dim].start_datum(); + } +}; + +/** + * View into NDRange for using the range end / upper bound in comparisons. + */ +struct RangeUpperBound { + const NDRange& mbr; + + const void* coord(unsigned dim) const { + return mbr[dim].end_datum().content(); + } + + UntypedDatumView dimension_datum(const Dimension&, unsigned dim) const { + return mbr[dim].end_datum(); + } +}; + } // namespace tiledb::sm #endif // TILEDB_COMPARATORS_H diff --git a/tiledb/sm/misc/type_traits.h b/tiledb/sm/misc/type_traits.h new file mode 100644 index 00000000000..230c1358571 --- /dev/null +++ b/tiledb/sm/misc/type_traits.h @@ -0,0 +1,65 @@ +/** + * @file type_traits.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * Provides type traits (concepts) which contain the interface requirements + * for various templates. + */ + +#ifndef TILEDB_COMPARE_TRAITS_H +#define TILEDB_COMPARE_TRAITS_H + +#include "tiledb/common/types/untyped_datum.h" +#include "tiledb/sm/array_schema/dimension.h" + +namespace tiledb::sm { + +/** + * Concept describing types which can be ordered using `CellCmpBase` + * (see `tiledb/sm/misc/comparators.h`) + */ +template +concept CellComparable = + requires(const T& a, const Dimension& dim, unsigned dim_idx) { + { a.dimension_datum(dim, dim_idx) } -> std::same_as; + }; + +/** + * Concept describing types which can be ordered using `GlobalCellCmp`. + * (see `tiledb/sm/misc/comparators.h`) + */ +template +concept GlobalCellComparable = + CellComparable && + requires(const T& a, const Dimension& dim, unsigned d) { + { a.coord(d) } -> std::convertible_to; + }; + +} // namespace tiledb::sm + +#endif diff --git a/tiledb/sm/query/readers/result_coords.h b/tiledb/sm/query/readers/result_coords.h index f835002a946..79eacf6c133 100644 --- a/tiledb/sm/query/readers/result_coords.h +++ b/tiledb/sm/query/readers/result_coords.h @@ -38,6 +38,7 @@ #include "tiledb/common/types/dynamic_typed_datum.h" #include "tiledb/sm/array_schema/dimension.h" +#include "tiledb/sm/misc/type_traits.h" #include "tiledb/sm/query/readers/result_tile.h" using namespace tiledb::common; @@ -77,6 +78,14 @@ struct ResultCoordsBase { , pos_(pos) { } + unsigned fragment_idx() const { + return tile_->frag_idx(); + } + + uint64_t tile_idx() const { + return tile_->tile_idx(); + } + /** * Returns a string coordinate. Applicable only to string * dimensions. @@ -266,9 +275,9 @@ struct GlobalOrderResultCoords * * @return Max slab length that can be merged for this tile. */ - template + template uint64_t max_slab_length( - const GlobalOrderResultCoords& next, const CompType& cmp) { + const GlobalOrderLowerBound& next, const CompType& cmp) { uint64_t cell_num = base::tile_->cell_num(); // Store the original position. diff --git a/tiledb/sm/query/readers/result_tile.h b/tiledb/sm/query/readers/result_tile.h index 72cef169e50..afc1a26f91b 100644 --- a/tiledb/sm/query/readers/result_tile.h +++ b/tiledb/sm/query/readers/result_tile.h @@ -70,6 +70,18 @@ class Subarray; */ bool result_tile_cmp(const ResultTile* a, const ResultTile* b); +struct ResultTileId { + unsigned fragment_idx_; + uint64_t tile_idx_; + + ResultTileId() = default; + + ResultTileId(unsigned fragment_idx, uint64_t tile_idx) + : fragment_idx_(fragment_idx) + , tile_idx_(tile_idx) { + } +}; + /** * Stores information about a logical dense or sparse result tile. Note that it * may store the physical tiles across more than one attributes for the same diff --git a/tiledb/sm/query/readers/sparse_global_order_reader.cc b/tiledb/sm/query/readers/sparse_global_order_reader.cc index e49c69133a6..5ab35f807d8 100644 --- a/tiledb/sm/query/readers/sparse_global_order_reader.cc +++ b/tiledb/sm/query/readers/sparse_global_order_reader.cc @@ -31,8 +31,10 @@ */ #include "tiledb/sm/query/readers/sparse_global_order_reader.h" +#include "tiledb/common/algorithm/parallel_merge.h" #include "tiledb/common/logger.h" #include "tiledb/common/memory_tracker.h" +#include "tiledb/common/unreachable.h" #include "tiledb/sm/array/array.h" #include "tiledb/sm/array_schema/array_schema.h" #include "tiledb/sm/array_schema/dimension.h" @@ -60,6 +62,124 @@ class SparseGlobalOrderReaderException : public StatusException { } }; +class SparseGlobalOrderReaderInternalError + : public SparseGlobalOrderReaderException { + public: + explicit SparseGlobalOrderReaderInternalError(const std::string& message) + : SparseGlobalOrderReaderException("Internal error: " + message) { + } +}; + +/** + * Encapsulates the input to the preprocess tile merge, and the + * merge output future for polling. + */ +struct PreprocessTileMergeFuture { + using MemoryCounter = std::atomic; + + PreprocessTileMergeFuture(Stats& stats, MemoryCounter& memory_used) + : stats_(stats) + , memory_used_(memory_used) { + } + + /** Query timers and metrics */ + Stats& stats_; + + /** memory used for result tile IDs */ + MemoryCounter& memory_used_; + + /** merge configuration, looks unused but must out-live the merge future */ + algorithm::ParallelMergeOptions merge_options_; + + /** merge input, looks unused but must out-live the merge future */ + std::vector> fragment_result_tiles_; + + /** comparator, looks unused but must out-live the merge future */ + std::shared_ptr cmp_; + + /** the merge future */ + std::optional> merge_; + + /** the known bound in the output up to which the merge has completed */ + std::optional safe_bound_; + + private: + void free_input() { + for (auto& fragment : fragment_result_tiles_) { + const auto num_tiles = fragment.size(); + fragment.clear(); + memory_used_ -= sizeof(ResultTileId) * num_tiles; + } + fragment_result_tiles_.clear(); + } + + public: + std::optional await() { + if (!merge_.has_value()) { + return std::nullopt; + } + std::optional ret; + { + auto timer_await = + stats_.start_timer("preprocess_result_tile_order_await"); + ret = merge_.value()->await(); + } + if (merge_.value()->finished()) { + free_input(); + } + return ret; + } + + /** + * Waits for the merge to have completed up to the requested `cursor`. + * + * @return true if the requested cursor is available, false otherwise + */ + bool wait_for(uint64_t request_cursor) { + if (!merge_.has_value()) { + return true; + } + + if (safe_bound_.has_value() && request_cursor < safe_bound_.value()) { + return true; + } + + const auto prev_safe_bound_ = safe_bound_; + + safe_bound_ = merge_.value()->valid_output_bound(); + if (safe_bound_.has_value() && safe_bound_.value() > request_cursor) { + if (prev_safe_bound_.has_value() && + prev_safe_bound_.value() > safe_bound_.value()) { + throw SparseGlobalOrderReaderInternalError( + "Unexpected preprocess tile merge state: out of order merge " + "bound"); + } + return true; + } + + while ((safe_bound_ = await()).has_value()) { + if (prev_safe_bound_.has_value() && + prev_safe_bound_.value() > safe_bound_.value()) { + throw SparseGlobalOrderReaderInternalError( + "Unexpected preprocess tile merge state: out of order merge " + "bound"); + } + if (safe_bound_.value() > request_cursor) { + return true; + } + } + + return false; + } + + void block() { + if (merge_.has_value()) { + merge_.value()->block(); + free_input(); + } + } +}; + /* ****************************** */ /* CONSTRUCTORS */ /* ****************************** */ @@ -77,12 +197,27 @@ SparseGlobalOrderReader::SparseGlobalOrderReader( params, true) , result_tiles_leftover_(array_->fragment_metadata().size()) - , memory_used_for_coords_(array_->fragment_metadata().size()) , consolidation_with_timestamps_(consolidation_with_timestamps) , last_cells_(array_->fragment_metadata().size()) , tile_offsets_loaded_(false) { // Initialize memory budget variables. refresh_config(); + + // Clear preprocess tile order + preprocess_tile_order_.enabled_ = false; + preprocess_tile_order_.cursor_ = 0; + + const auto preprocess_tile_merge_min_items = config_.get( + "sm.query.sparse_global_order.preprocess_tile_merge"); + preprocess_tile_order_.enabled_ = + array_schema_.cell_order() != Layout::HILBERT && + 0 < preprocess_tile_merge_min_items.value_or(0); + preprocess_tile_order_.cursor_ = 0; + + if (!preprocess_tile_order_.enabled_) { + per_fragment_memory_state_.memory_used_for_coords_.resize( + array_->fragment_metadata().size()); + } } /* ****************************** */ @@ -91,8 +226,18 @@ SparseGlobalOrderReader::SparseGlobalOrderReader( template bool SparseGlobalOrderReader::incomplete() const { - return !read_state_.done_adding_result_tiles() || - memory_used_for_coords_total_ != 0; + if (!read_state_.done_adding_result_tiles()) { + return true; + } else if (!preprocess_tile_order_.enabled_) { + return memory_used_for_coords_total_ != 0; + } else if (preprocess_tile_order_.has_more_tiles()) { + return true; + } else { + [[maybe_unused]] const size_t mem_for_tile_order = + sizeof(ResultTileId) * preprocess_tile_order_.tiles_.size(); + assert(memory_used_for_coords_total_ >= mem_for_tile_order); + return memory_used_for_coords_total_ > mem_for_tile_order; + } } template @@ -136,6 +281,33 @@ Status SparseGlobalOrderReader::dowork() { // Load initial data, if not loaded already. throw_if_not_ok(load_initial_data()); + + // Determine result tile order + // (this happens after load_initial_data which identifies which tiles pass + // subarray) + std::optional preprocess_future; + if (preprocess_tile_order_.enabled_ && + preprocess_tile_order_.tiles_.empty()) { + // For native, this should only happen in the first `tiledb_query_submit`, + // then the merged list remains in memory until the end is reached. + // For REST, this happens in the first `submit` call; then the tiles + // are serialized back and forth; and then this happens again once the + // end of the list is reached (at which point it is cleared), if more + // iterations are needed. + preprocess_future.emplace(*stats_, memory_used_for_coords_total_); + preprocess_compute_result_tile_order(preprocess_future.value()); + preprocess_tile_order_.cursor_ = + PreprocessTileOrder::compute_cursor_from_read_state( + subarray_.relevant_fragments(), + read_state_.frag_idx(), + preprocess_tile_order_.tiles_, + preprocess_future.value()); + } else if (preprocess_tile_order_.enabled_) { + // NB: we could have avoided loading these in the first place + // since we already have determined which tiles qualify, and their order + tmp_read_state_.clear_tile_ranges(); + } + purge_deletes_consolidation_ = !deletes_consolidation_no_purge_ && consolidation_with_timestamps_ && !delete_and_update_conditions_.empty(); @@ -143,6 +315,8 @@ Status SparseGlobalOrderReader::dowork() { !array_schema_.allows_dups() && purge_deletes_consolidation_; // Load tile offsets, if required. + // TODO: this can be improved to load only the offsets from + // `preprocess_tile_orders_.tiles_`. load_all_tile_offsets(); // Field names to process. @@ -154,7 +328,7 @@ Status SparseGlobalOrderReader::dowork() { stats_->add_counter("internal_loop_num", 1); // Create the result tiles we are going to process. - auto created_tiles = create_result_tiles(result_tiles); + auto created_tiles = create_result_tiles(result_tiles, preprocess_future); if (created_tiles.size() > 0) { // Read and unfilter coords. @@ -189,16 +363,22 @@ Status SparseGlobalOrderReader::dowork() { if (array_schema_.cell_order() == Layout::HILBERT) { auto&& [user_buffs_full, rcs] = merge_result_cell_slabs( - max_num_cells_to_copy(), result_tiles); + max_num_cells_to_copy(), result_tiles, preprocess_future); user_buffers_full = user_buffs_full; result_cell_slabs = std::move(rcs); } else { auto&& [user_buffs_full, rcs] = merge_result_cell_slabs( - max_num_cells_to_copy(), result_tiles); + max_num_cells_to_copy(), result_tiles, preprocess_future); user_buffers_full = user_buffs_full; result_cell_slabs = std::move(rcs); } + if (created_tiles.empty() && result_cell_slabs.empty() && incomplete()) { + throw SparseGlobalOrderReaderException( + "Cannot load enough tiles to emit results from all fragments in " + "global order"); + } + // No more tiles to process, done. if (!result_cell_slabs.empty()) { // Copy cell slabs. @@ -209,6 +389,13 @@ Status SparseGlobalOrderReader::dowork() { } } + if (preprocess_future.has_value() && !incomplete()) { + // clean up gracefully and let the merge finish, freeing input etc + // (this also helps simplify assertions about memory usage) + preprocess_future->block(); + preprocess_future.reset(); + } + // End the iteration. end_iteration(result_tiles); } while (!user_buffers_full && incomplete()); @@ -226,6 +413,13 @@ Status SparseGlobalOrderReader::dowork() { stats_->add_counter("ignored_tiles", tmp_read_state_.num_ignored_tiles()); + if (preprocess_future.has_value()) { + // wait for tile merge to complete so we don't have to keep this + // state around for the next iteration, and if this errors for + // some reason we should return that + preprocess_future->block(); + } + return Status::Ok(); } @@ -298,7 +492,6 @@ uint64_t SparseGlobalOrderReader::get_coord_tiles_size( template bool SparseGlobalOrderReader::add_result_tile( const unsigned dim_num, - const uint64_t memory_budget_coords_tiles, const unsigned f, const uint64_t t, const FragmentMetadata& frag_md, @@ -311,15 +504,25 @@ bool SparseGlobalOrderReader::add_result_tile( auto tiles_size = get_coord_tiles_size(dim_num, f, t); // Don't load more tiles than the memory budget. - if (memory_used_for_coords_[f] + tiles_size > memory_budget_coords_tiles) { - return true; + if (preprocess_tile_order_.enabled_) { + if (memory_used_for_coords_total_ + tiles_size > + memory_budget_.coordinates_budget()) { + return true; + } + } else { + if (per_fragment_memory_state_.memory_used_for_coords_[f] + tiles_size > + per_fragment_memory_state_.per_fragment_memory_) { + return true; + } } // Adjust total memory used. memory_used_for_coords_total_ += tiles_size; - // Adjust per fragment memory used. - memory_used_for_coords_[f] += tiles_size; + if (!preprocess_tile_order_.enabled_) { + // Adjust per fragment memory used. + per_fragment_memory_state_.memory_used_for_coords_[f] += tiles_size; + } // Add the tile. result_tiles[f].emplace_back( @@ -333,12 +536,372 @@ bool SparseGlobalOrderReader::add_result_tile( return false; } +/** + * Computes the order in which result tiles should be loaded. + * + * The sparse global order reader combines the cells from one or more + * fragments, and returns the combined cells to the user in the + * global order. De-duplication of the coordinates may occur if + * the same coordinates are written to multiple fragments. + * + * Each fragment has a list of tiles. The tiles in these lists are + * arranged in global order. If we have a single fragment, then + * we can just stream the tiles in order from that list to the user. + * + * But we probably have multiple fragments which may or may not overlap + * in any part of the coordinate space. + * + * A naive approach to understanding + * that overlap is load tiles from each fragment and compare the values. + * One problem with this approach is that it requires loading a tile from + * each fragment and keeping those tiles loaded, even if the contents + * of that tile might not appear in the global order for quite a while. + * + * A smarter approach uses the fragment metadata, which we want to load + * anyway, in order to determine the order in which all of the tiles must + * be loaded. This function implements that approach. + * + * The tiles in each fragment are already arranged in the global order, + * so we can combine the fragment tile lists into a single list which + * holds the tiles from all fragments in the order which they must + * be loaded. We will run a "parallel merge" algorithm to do this - + * see `tiledb/common/algorithm/parallel_merge.h`. But what is the + * sort key for the tiles? + * + * The fragment metadata contains the "minimum bounding rectangle" or "MBR" + * of each tile. The MBRs give us a hint for arranging all of the tiles + * in the order in which their contents will fit in the global order. + * + * For one-dimension arrays, this works great. The MBR is equivalent + * to the minimum and maximum of each tile. However... + * + * For two-dimension arrays, the MBR is *not* always the min/max of the tiles. + * It is the min of the coordinates, in each dimension. This means + * that two tiles from the same fragment whose coordinates are in the global + * order might have MBRs which are *not* in the global order. + * + * Example: space tile extent is 3, data tile capacity is 4 + * c-T1 c-T2 + * | | | + * ---+---+---+---+---+---+---+--- + * | a b | f | + * r-T1 | | | + * | c d e | g | + * ---+---+---+---+---+---+---+--- + * + * Data tile 1 holds [a, b, c, d] and data tile 2 holds [e, f, g]. + * + * Data tile 1 has an MBR of [(1, 1), (3, 3)]. + * Data tile 2 has an MBR of [(3, 1), (6, 3)]. + * + * The lower range of data tile 2's MBR is less than the lower range of tile + * 1's. + * + * Does this mean that the MBR is unsuitable as a merge key? + * Surprisingly, the answer is no! We can still use it as a merge key. + * + * If used, then merged tile list would be ordered on MBR, + * but the coordinates within would not necessarily be in global order. + * + * However, the coordinates would still be in global order *for each fragment*. + * The parallel merge algorithm does not re-order the items of its lists. + * + * How do we resolve this? + * + * If all of the tiles fit in memory, it is resolved trivially: + * the MBRs are just used to determine what order to load tiles in. + * Once all the tiles are loaded we use the actual coordinate values. + * + * If not all of the tiles fit in memory, then each iteration we identify a + * bisection of the remaining tiles. Let's say tiles [0.. K] fit within the + * memory budget and we load the coordinates, and let's say tiles [K.. N] cannot + * be loaded and thus we can only access their MBRs via fragment metadata. + * + * If we select a tile in 0 <= A < K, and a tile in K <= B < N: + * (1) + * If A and B are in the same fragment, then all of the coordinates + * of A arrive before all of the coordinates of B. + * (2) + * If A and B are from different fragments, but there is any tile + * in 0 <= C < K which is from the same fragment as B, then all of + * the coordinates from A which are bounded by a coordinate in C + * are also bounded by coordinates in B. + * (3) + * If A and B are from different fragments, but there is no tile + * in 0 <= C < K which is from the same fragment as B, then we + * can infer no relationship whatsoever between the coordinates of + * A and the coordinates of B. + * + * (2) and (3) demonstrate that it is not always correct to emit + * all of the coordinates of A prior to loading B. + * + * If all the fragments have at least one tile in 0 <= A < K, then we + * can avoid out-of-order coordinates by fetching more tiles + * once we exhaust all of the coordinates from the first fragment + * which has tiles in K <= B < N. + * + * If there is at least one fragment F which does not have a tile in 0 <= A < K, + * then we must construct a "merge bound" which is an upper bound on the + * coordinates which are guaranteed to be ordered correctly with respect to the + * coordinates in F. This value does not have to be an actual coordinate, just + * a lower bound on what coordinates are possible. The MBR of the first + * not-loaded tile of F is an easy and correct choice as it is guaranteed to be + * the lower bound of all coordinates in F. + * + * @precondition the `TempReadState` is up to date with which tiles pass the + * subarray (e.g. by calling `load_initial_data`) + */ +template +void SparseGlobalOrderReader::preprocess_compute_result_tile_order( + PreprocessTileMergeFuture& future) { + auto timer_start_tile_order = + stats_->start_timer("preprocess_result_tile_order_compute"); + + const auto& relevant_fragments = subarray_.relevant_fragments(); + const uint64_t num_relevant_fragments = relevant_fragments.size(); + + future.fragment_result_tiles_.resize(fragment_metadata_.size()); + + auto& fragment_result_tiles = future.fragment_result_tiles_; + + // TODO: ideally this could be async or on demand for each tile + // so that we could be closer to a proper LIMIT + subarray_.load_relevant_fragment_rtrees(&resources_.compute_tp()); + + auto try_reserve = [&](unsigned f, size_t num_tiles) { + memory_used_for_coords_total_ += sizeof(ResultTileId) * num_tiles; + + if (memory_used_for_coords_total_ > memory_budget_.coordinates_budget()) { + return Status_QueryError( + "Cannot allocate space for preprocess result tile ID list, " + "increase memory budget: total budget = " + + std::to_string(memory_budget_.coordinates_budget()) + + ", memory needed >= " + + std::to_string(memory_used_for_coords_total_)); + } + fragment_result_tiles[f].reserve(num_tiles); + return Status::Ok(); + }; + + // first apply subarray (in parallel) + if (!subarray_.is_set()) { + for (const auto& f : relevant_fragments) { + fragment_result_tiles[f].reserve(fragment_metadata_[f]->tile_num()); + } + auto populate_fragment_relevant_tiles = parallel_for( + &resources_.compute_tp(), 0, num_relevant_fragments, [&](unsigned rf) { + const unsigned f = relevant_fragments[rf]; + const unsigned num_tiles = fragment_metadata_[f]->tile_num(); + RETURN_NOT_OK(try_reserve(f, num_tiles)); + + for (uint64_t t = 0; t < fragment_metadata_[f]->tile_num(); t++) { + fragment_result_tiles[f].push_back(ResultTileId(f, t)); + } + return Status::Ok(); + }); + throw_if_not_ok(populate_fragment_relevant_tiles); + } else { + /** + * Determines which tiles from a given fragment qualify for the subarray. + */ + auto populate_relevant_tiles = [&](unsigned rf) { + const unsigned f = relevant_fragments[rf]; + + const auto& fragment_tile_ranges = tmp_read_state_.tile_ranges(f); + + uint64_t num_qualifying_tiles = 0; + for (const auto& tile_range : fragment_tile_ranges) { + num_qualifying_tiles += 1 + tile_range.second - tile_range.first; + } + + RETURN_NOT_OK(try_reserve(f, num_qualifying_tiles)); + + for (auto tile_range = fragment_tile_ranges.rbegin(); + tile_range != fragment_tile_ranges.rend(); + ++tile_range) { + for (uint64_t t = tile_range->first; t <= tile_range->second; t++) { + fragment_result_tiles[f].push_back(ResultTileId(f, t)); + } + } + + if (fragment_result_tiles[f].empty()) { + tmp_read_state_.set_all_tiles_loaded(f); + } + + return Status::Ok(); + }; + + auto populate_fragment_relevant_tiles = parallel_for( + &resources_.compute_tp(), + 0, + num_relevant_fragments, + populate_relevant_tiles); + throw_if_not_ok(populate_fragment_relevant_tiles); + + tmp_read_state_.clear_tile_ranges(); + } + + size_t num_result_tiles = 0; + for (const auto& fragment : fragment_result_tiles) { + num_result_tiles += fragment.size(); + } + + memory_used_for_coords_total_ += sizeof(ResultTileId) * num_result_tiles; + if (memory_used_for_coords_total_ > memory_budget_.coordinates_budget()) { + throw SparseGlobalOrderReaderException( + "Cannot allocate space for preprocess result tile ID list, " + "increase memory budget: total budget = " + + std::to_string(memory_budget_.coordinates_budget()) + + ", memory needed = " + std::to_string(memory_used_for_coords_total_)); + } + + /* then do parallel merge */ + preprocess_tile_order_.tiles_.resize(num_result_tiles, ResultTileId(0, 0)); + + const auto min_merge_items = + config_ + .get("sm.query.sparse_global_order.preprocess_tile_merge") + .value(); + future.merge_options_ = { + .parallel_factor = resources_.compute_tp().concurrency_level(), + .min_merge_items = min_merge_items}; + + algorithm::ParallelMergeMemoryResources merge_resources( + *query_memory_tracker_.get()); + + auto do_global_order_merge = [&]() { + struct ResultTileCmp + : public GlobalCellCmpStaticDispatch { + using Base = GlobalCellCmpStaticDispatch; + using PerFragmentMetadata = + const std::vector>; + + ResultTileCmp( + const Domain& domain, const PerFragmentMetadata& fragment_metadata) + : Base(domain) + , fragment_metadata_(fragment_metadata) { + } + + bool operator()(const ResultTileId& a, const ResultTileId& b) const { + // FIXME: this can potentially make a better global order if + // we clamp the MBR lower bounds using the subarray + const RangeLowerBound a_mbr = { + .mbr = fragment_metadata_[a.fragment_idx_]->mbr(a.tile_idx_)}; + const RangeLowerBound b_mbr = { + .mbr = fragment_metadata_[b.fragment_idx_]->mbr(b.tile_idx_)}; + return (*static_cast(this))(a_mbr, b_mbr); + } + + const PerFragmentMetadata& fragment_metadata_; + }; + + ResultTileCmp cmp(array_schema_.domain(), fragment_metadata_); + + { + std::shared_ptr cmp = tdb::make_shared( + "ResultTileCmp", array_schema_.domain(), fragment_metadata_); + future.cmp_ = std::static_pointer_cast(cmp); + } + + future.merge_ = algorithm::parallel_merge( + resources_.compute_tp(), + merge_resources, + future.merge_options_, + future.fragment_result_tiles_, + *static_cast(future.cmp_.get()), + &preprocess_tile_order_.tiles_[0]); + }; + + switch (array_schema_.cell_order()) { + case Layout::ROW_MAJOR: + switch (array_schema_.tile_order()) { + case Layout::ROW_MAJOR: { + do_global_order_merge + .template operator()(); + break; + } + case Layout::COL_MAJOR: { + do_global_order_merge + .template operator()(); + break; + } + default: + stdx::unreachable(); + } + break; + case Layout::COL_MAJOR: + switch (array_schema_.tile_order()) { + case Layout::ROW_MAJOR: { + do_global_order_merge + .template operator()(); + break; + } + case Layout::COL_MAJOR: { + do_global_order_merge + .template operator()(); + break; + } + default: + stdx::unreachable(); + } + break; + case Layout::HILBERT: + default: + stdx::unreachable(); + } +} + template std::vector SparseGlobalOrderReader::create_result_tiles( - std::vector& result_tiles) { + std::vector& result_tiles, + std::optional& preprocess_future) { auto timer_se = stats_->start_timer("create_result_tiles"); + // Distinguish between leftover result tiles from the previous `submit` + // and result tiles which are being added now + std::vector rt_list_num_tiles(result_tiles.size()); + for (uint64_t i = 0; i < result_tiles.size(); i++) { + rt_list_num_tiles[i] = result_tiles[i].size(); + } + + if (preprocess_tile_order_.enabled_) { + create_result_tiles_using_preprocess(result_tiles, preprocess_future); + } else { + create_result_tiles_all_fragments(result_tiles); + } + + const auto num_fragments = fragment_metadata_.size(); + bool done_adding_result_tiles = tmp_read_state_.done_adding_result_tiles(); + uint64_t num_rt = 0; + for (unsigned int f = 0; f < num_fragments; f++) { + num_rt += result_tiles[f].size(); + } + + logger_->debug("Done adding result tiles, num result tiles {0}", num_rt); + + if (done_adding_result_tiles) { + logger_->debug("All result tiles loaded"); + } + + read_state_.set_done_adding_result_tiles(done_adding_result_tiles); + + // Return the list of tiles added. + std::vector created_tiles; + for (uint64_t i = 0; i < result_tiles.size(); i++) { + TileListIt it = result_tiles[i].begin(); + std::advance(it, rt_list_num_tiles[i]); + for (; it != result_tiles[i].end(); ++it) { + created_tiles.emplace_back(&*it); + } + } + + return created_tiles; +} + +template +void SparseGlobalOrderReader::create_result_tiles_all_fragments( + std::vector& result_tiles) { // For easy reference. auto fragment_num = fragment_metadata_.size(); auto dim_num = array_schema_.dim_num(); @@ -346,6 +909,9 @@ SparseGlobalOrderReader::create_result_tiles( // Get the number of fragments to process and compute per fragment memory. uint64_t num_fragments_to_process = tmp_read_state_.num_fragments_to_process(); + per_fragment_memory_state_.per_fragment_memory_ = + memory_budget_.total_budget() * memory_budget_.ratio_coords() / + num_fragments_to_process; // Save which result tile list is empty. std::vector rt_list_num_tiles(result_tiles.size()); @@ -353,83 +919,18 @@ SparseGlobalOrderReader::create_result_tiles( rt_list_num_tiles[i] = result_tiles[i].size(); } - if (num_fragments_to_process > 0) { - per_fragment_memory_ = - memory_budget_.coordinates_budget() / num_fragments_to_process; - - // Create result tiles. - if (subarray_.is_set()) { - // Load as many tiles as the memory budget allows. - throw_if_not_ok(parallel_for( - &resources_.compute_tp(), 0, fragment_num, [&](uint64_t f) { - uint64_t t = 0; - auto& tile_ranges = tmp_read_state_.tile_ranges(f); - while (!tile_ranges.empty()) { - auto& range = tile_ranges.back(); - for (t = range.first; t <= range.second; t++) { - auto budget_exceeded = add_result_tile( - dim_num, - per_fragment_memory_, - f, - t, - *fragment_metadata_[f], - result_tiles); - - if (budget_exceeded) { - logger_->debug( - "Budget exceeded adding result tiles, fragment {0}, tile " - "{1}", - f, - t); - - if (result_tiles[f].empty()) { - auto tiles_size = get_coord_tiles_size(dim_num, f, t); - throw SparseGlobalOrderReaderException( - "Cannot load a single tile for fragment, increase " - "memory " - "budget, tile size : " + - std::to_string(tiles_size) + ", per fragment memory " + - std::to_string(per_fragment_memory_) + - ", total budget " + - std::to_string(memory_budget_.total_budget()) + - ", processing fragment " + std::to_string(f) + - " out of " + std::to_string(num_fragments_to_process) + - " total fragments"); - } - return Status::Ok(); - } - - range.first++; - } - - tmp_read_state_.remove_tile_range(f); - } - - tmp_read_state_.set_all_tiles_loaded(f); - - return Status::Ok(); - })); - } else { - // Load as many tiles as the memory budget allows. - throw_if_not_ok(parallel_for( - &resources_.compute_tp(), 0, fragment_num, [&](uint64_t f) { - uint64_t t = 0; - auto tile_num = fragment_metadata_[f]->tile_num(); - - // Figure out the start index. - auto start = read_state_.frag_idx()[f].tile_idx_; - if (!result_tiles[f].empty()) { - start = std::max(start, result_tiles[f].back().tile_idx() + 1); - } - - for (t = start; t < tile_num; t++) { + // Create result tiles. + if (subarray_.is_set()) { + // Load as many tiles as the memory budget allows. + throw_if_not_ok(parallel_for( + &resources_.compute_tp(), 0, fragment_num, [&](uint64_t f) { + uint64_t t = 0; + auto& tile_ranges = tmp_read_state_.tile_ranges(f); + while (!tile_ranges.empty()) { + auto& range = tile_ranges.back(); + for (t = range.first; t <= range.second; t++) { auto budget_exceeded = add_result_tile( - dim_num, - per_fragment_memory_, - f, - t, - *fragment_metadata_[f], - result_tiles); + dim_num, f, t, *fragment_metadata_[f], result_tiles); if (budget_exceeded) { logger_->debug( @@ -440,51 +941,258 @@ SparseGlobalOrderReader::create_result_tiles( if (result_tiles[f].empty()) { auto tiles_size = get_coord_tiles_size(dim_num, f, t); - return logger_->status(Status_SparseGlobalOrderReaderError( - "Cannot load a single tile for fragment, increase memory " + throw SparseGlobalOrderReaderException( + "Cannot load a single tile for fragment, increase " + "memory " "budget, tile size : " + std::to_string(tiles_size) + ", per fragment memory " + - std::to_string(per_fragment_memory_) + ", total budget " + + std::to_string( + per_fragment_memory_state_.per_fragment_memory_) + + ", total budget " + std::to_string(memory_budget_.total_budget()) + ", num fragments to process " + - std::to_string(num_fragments_to_process))); + std::to_string(num_fragments_to_process)); } return Status::Ok(); } + + range.first++; } - tmp_read_state_.set_all_tiles_loaded(f); + tmp_read_state_.remove_tile_range(f); + } - return Status::Ok(); - })); - } - } + tmp_read_state_.set_all_tiles_loaded(f); - bool done_adding_result_tiles = tmp_read_state_.done_adding_result_tiles(); - uint64_t num_rt = 0; - for (unsigned int f = 0; f < fragment_num; f++) { - num_rt += result_tiles[f].size(); + return Status::Ok(); + })); + } else { + // Load as many tiles as the memory budget allows. + throw_if_not_ok(parallel_for( + &resources_.compute_tp(), 0, fragment_num, [&](uint64_t f) { + uint64_t t = 0; + auto tile_num = fragment_metadata_[f]->tile_num(); + + // Figure out the start index. + auto start = read_state_.frag_idx()[f].tile_idx_; + if (!result_tiles[f].empty()) { + start = std::max(start, result_tiles[f].back().tile_idx() + 1); + } + + for (t = start; t < tile_num; t++) { + auto budget_exceeded = add_result_tile( + dim_num, f, t, *fragment_metadata_[f], result_tiles); + + if (budget_exceeded) { + logger_->debug( + "Budget exceeded adding result tiles, fragment {0}, tile " + "{1}", + f, + t); + + if (result_tiles[f].empty()) { + auto tiles_size = get_coord_tiles_size(dim_num, f, t); + return logger_->status(Status_SparseGlobalOrderReaderError( + "Cannot load a single tile for fragment, increase memory " + "budget, tile size : " + + std::to_string(tiles_size) + ", per fragment memory " + + std::to_string( + per_fragment_memory_state_.per_fragment_memory_) + + ", total budget " + + std::to_string(memory_budget_.total_budget()) + + ", num fragments to process " + + std::to_string(num_fragments_to_process))); + } + return Status::Ok(); + } + } + + tmp_read_state_.set_all_tiles_loaded(f); + + return Status::Ok(); + })); } +} - logger_->debug("Done adding result tiles, num result tiles {0}", num_rt); +template +void SparseGlobalOrderReader::create_result_tiles_using_preprocess( + std::vector& result_tiles, + std::optional& merge_future) { + // For easy reference. + const auto num_dims = array_schema_.dim_num(); + + if (preprocess_tile_order_.cursor_ > 0 && + std::all_of(result_tiles.begin(), result_tiles.end(), [](const auto& r) { + return r.empty(); + })) { + // When a result tile is created for a ResultTileId, the preprocess merge + // cursor advances. Then the result tile tracks how much data has been + // emitted from that tile. + // + // When running natively, if an iteration does not exhaust that result tile + // remains in the result tile list and its cursor remains in memory for + // use in the next iteration. + // + // When running against the REST server, if an iteration does not exhaust a + // result tile, then its cursor must be serialized/deserialized. That state + // lives in `read_state_.frag_idx()`. + // + // Hence if we have no result tiles, and a preprocess cursor, then we must + // check if we have any un-exhausted result tiles. + const auto& rtcursors = read_state_.frag_idx(); + + if (merge_future.has_value()) { + const bool wait_ok = + merge_future->wait_for(preprocess_tile_order_.cursor_ - 1); + if (!wait_ok) { + throw SparseGlobalOrderReaderInternalError( + "Waiting for preprocess tile merge results failed"); + } + } - if (done_adding_result_tiles) { - logger_->debug("All result tiles loaded"); + for (const auto& f : subarray_.relevant_fragments()) { + // identify the tiles in reverse global order + std::vector rts; + for (size_t rti = preprocess_tile_order_.cursor_; rti > 0; --rti) { + const auto& rt = preprocess_tile_order_.tiles_[rti - 1]; + if (rt.fragment_idx_ != f) { + continue; + } else if (rtcursors[f].tile_idx_ > rt.tile_idx_) { + // all of the tiles for this fragment up to this are done + break; + } + + rts.push_back(rti - 1); + } + + // and then re-create them + for (auto rti = rts.rbegin(); rti != rts.rend(); ++rti) { + const auto& rt = preprocess_tile_order_.tiles_[*rti]; + + // this is a tile which qualified for the subarray and was + // a created result tile, we must continue processing it + bool budget_exceeded; + while ((budget_exceeded = add_result_tile( + num_dims, + f, + rt.tile_idx_, + *fragment_metadata_[f], + result_tiles)) && + merge_future.has_value()) { + // try to free some memory by waiting for merge if it is ongoing + if (merge_future.has_value()) { + merge_future->block(); + merge_future.reset(); + } + } + + // all these tiles were created in a previous iteration, so we *had* + // the memory budget - and must not any more. Can a user change + // configuration between rounds of `tiledb_query_submit`? If + // not then this represents internal error; if so then this + // represents user error. Since we have lost the ordering of tiles, + // we have to fall back to "per fragment" mode which is achievable + // but too complicated for this author to want to bother. + if (budget_exceeded) { + throw SparseGlobalOrderReaderException( + "Cannot load result tiles from previous submit: this likely " + "indicates a reduction in memory budget. Increase memory " + "budget."); + } + } + } } - read_state_.set_done_adding_result_tiles(done_adding_result_tiles); + if (preprocess_tile_order_.has_more_tiles()) { + uint64_t rt; + for (rt = preprocess_tile_order_.cursor_; + rt < preprocess_tile_order_.tiles_.size(); + rt++) { + if (merge_future.has_value()) { + if (!merge_future->wait_for(rt)) { + throw SparseGlobalOrderReaderInternalError( + "Unexpected preprocess tile merge state: expected new merge " + "bound but found none"); + } + } - // Return the list of tiles added. - std::vector created_tiles; - for (uint64_t i = 0; i < result_tiles.size(); i++) { - TileListIt it = result_tiles[i].begin(); - std::advance(it, rt_list_num_tiles[i]); - for (; it != result_tiles[i].end(); ++it) { - created_tiles.emplace_back(&*it); + const auto f = preprocess_tile_order_.tiles_[rt].fragment_idx_; + const auto t = preprocess_tile_order_.tiles_[rt].tile_idx_; + + auto budget_exceeded = + add_result_tile(num_dims, f, t, *fragment_metadata_[f], result_tiles); + + if (budget_exceeded) { + logger_->debug( + "Budget exceeded adding result tiles, fragment {0}, tile " + "{1}", + f, + t); + + bool canProgress = false; + for (const auto& rts : result_tiles) { + if (!rts.empty()) { + canProgress = true; + break; + } + } + + if (!canProgress) { + // first try to free some memory by waiting for merge if it is ongoing + if (merge_future.has_value()) { + merge_future->block(); + merge_future.reset(); + continue; + } + + // this means we cannot safely produce any results + const auto tiles_size = get_coord_tiles_size(num_dims, f, t); + throw SparseGlobalOrderReaderException( + "Cannot load a single tile, increase memory budget: " + "current coords tile size = " + + std::to_string(memory_used_for_coords_total_) + + ", next coords tile size = " + std::to_string(tiles_size) + + ", coords tile budget = " + + std::to_string(memory_budget_.coordinates_budget()) + + ", total_budget = " + + std::to_string(memory_budget_.total_budget())); + } else { + // this tile has the lowest MBR lower bound of the remaining tiles, + // we cannot safely emit cells exceeding its lower bound later + break; + } + } } + + // update position for next iteration + preprocess_tile_order_.cursor_ = rt; } - return created_tiles; + // update which fragments are done + for (const auto& f : subarray_.relevant_fragments()) { + if (!tmp_read_state_.all_tiles_loaded(f)) { + bool all_tiles_loaded = true; + for (uint64_t ri = preprocess_tile_order_.cursor_; + all_tiles_loaded && ri < preprocess_tile_order_.tiles_.size(); + ri++) { + if (merge_future.has_value()) { + if (!merge_future->wait_for(ri)) { + throw SparseGlobalOrderReaderInternalError( + "Waiting for preprocess tile merge results failed"); + } + } + + const auto& tile = preprocess_tile_order_.tiles_[ri]; + if (tile.fragment_idx_ == f) { + all_tiles_loaded = false; + break; + } + } + if (all_tiles_loaded) { + tmp_read_state_.set_all_tiles_loaded(f); + } + } + } } template @@ -750,12 +1458,13 @@ bool SparseGlobalOrderReader::add_all_dups_to_queue( template template -bool SparseGlobalOrderReader::add_next_cell_to_queue( +AddNextCellResult SparseGlobalOrderReader::add_next_cell_to_queue( GlobalOrderResultCoords& rc, std::vector& result_tiles_it, const std::vector& result_tiles, TileMinHeap& tile_queue, - std::vector& to_delete) { + std::vector& to_delete, + const std::optional& merge_bound) { auto frag_idx = rc.tile_->frag_idx(); auto dups = array_schema_.allows_dups(); @@ -763,7 +1472,7 @@ bool SparseGlobalOrderReader::add_next_cell_to_queue( // This would be because a cell after this one in the fragment was added to // the queue as it had the same coordinates as this one. if (!rc.has_next_) { - return false; + return AddNextCellResult::Done; } // Try the next cell in the same tile. @@ -775,6 +1484,8 @@ bool SparseGlobalOrderReader::add_next_cell_to_queue( // Remove the tile from result tiles if it wasn't used at all. if (!rc.tile_->used()) { tmp_read_state_.add_ignored_tile(*to_delete_it); + + std::unique_lock ul(tile_queue_mutex_); to_delete.push_back(to_delete_it); } @@ -798,11 +1509,11 @@ bool SparseGlobalOrderReader::add_next_cell_to_queue( // This fragment has more tiles potentially. if (!tmp_read_state_.all_tiles_loaded(frag_idx)) { // Return we need more tiles. - return true; + return AddNextCellResult::NeedMoreTiles; } // All tiles processed, done. - return false; + return AddNextCellResult::Done; } } @@ -812,8 +1523,29 @@ bool SparseGlobalOrderReader::add_next_cell_to_queue( // with timestamps if not all tiles are loaded. if (!dups && last_in_memory_cell_of_consolidated_fragment( frag_idx, rc, result_tiles)) { - return true; + return AddNextCellResult::NeedMoreTiles; } + + // If the cell value exceeds the lower bound of the un-populated result + // tiles then it is not correct to emit it; hopefully we clear out + // a tile somewhere and trying again will make progress. + // + // If the next cell is equal to the merge bound, then it is only + // correct to add it if duplicates are allowed. + if (merge_bound.has_value()) { + GlobalCellCmp cmp(array_schema_.domain()); + if (array_schema_.allows_dups()) { + if (cmp(*merge_bound, rc)) { + return AddNextCellResult::MergeBound; + } + } else { + // `!(rc < *merge_bound)`, i.e. `*merge_bound <= rc` + if (!cmp(rc, *merge_bound)) { + return AddNextCellResult::MergeBound; + } + } + } + std::unique_lock ul(tile_queue_mutex_); // Add all the cells in this tile with the same coordinates as this cell @@ -822,14 +1554,14 @@ bool SparseGlobalOrderReader::add_next_cell_to_queue( fragment_metadata_[frag_idx]->has_timestamps()) { if (add_all_dups_to_queue( rc, result_tiles_it, result_tiles, tile_queue, to_delete)) { - return true; + return AddNextCellResult::NeedMoreTiles; } } tile_queue.emplace(std::move(rc)); } // We don't need more tiles as a tile was found. - return false; + return AddNextCellResult::FoundCell; } template @@ -890,7 +1622,9 @@ template template tuple> SparseGlobalOrderReader::merge_result_cell_slabs( - uint64_t num_cells, std::vector& result_tiles) { + uint64_t num_cells, + std::vector& result_tiles, + std::optional& merge_future) { auto timer_se = stats_->start_timer("merge_result_cell_slabs"); // User gave us some empty buffers, exit. @@ -919,11 +1653,54 @@ SparseGlobalOrderReader::merge_result_cell_slabs( TileMinHeap tile_queue(cmp, std::move(container)); // If any fragments needs to load more tiles. - bool need_more_tiles = false; + AddNextCellResult add_next_cell_result = AddNextCellResult::FoundCell; // Tile iterators, per fragments. std::vector rt_it(result_tiles.size()); + // For preprocess tile order, compute the merge bound + // (see comment in preprocess_compute_result_tile_order) + std::optional merge_bound; + if (preprocess_tile_order_.has_more_tiles()) { + auto has_pending_tiles = [&](uint64_t f) -> bool { + return result_tiles[f].empty() && !tmp_read_state_.all_tiles_loaded(f); + }; + bool any_pending_fragments = false; + for (const auto& f : subarray_.relevant_fragments()) { + if (has_pending_tiles(f)) { + any_pending_fragments = true; + break; + } + } + + if (any_pending_fragments) { + for (uint64_t rt = preprocess_tile_order_.cursor_; + rt < preprocess_tile_order_.tiles_.size(); + rt++) { + if (merge_future.has_value()) { + if (!merge_future->wait_for(rt)) { + throw SparseGlobalOrderReaderInternalError( + "Unexpected preprocess tile merge state: expected new merge " + "bound but found none"); + } + } + const auto frt = preprocess_tile_order_.tiles_[rt].fragment_idx_; + if (has_pending_tiles(frt)) { + merge_bound.emplace(RangeLowerBound{ + .mbr = fragment_metadata_[frt]->mbr( + preprocess_tile_order_.tiles_[rt].tile_idx_)}); + break; + } + } + } + } + + auto push_result = [&add_next_cell_result](auto res) { + if (add_next_cell_result != AddNextCellResult::NeedMoreTiles) { + add_next_cell_result = res; + } + }; + // For all fragments, get the first tile in the sorting queue. std::vector to_delete; throw_if_not_ok(parallel_for( @@ -938,11 +1715,11 @@ SparseGlobalOrderReader::merge_result_cell_slabs( read_state_.frag_idx()[f].cell_idx_ : 0; GlobalOrderResultCoords rc(&*(rt_it[f]), cell_idx); - bool res = add_next_cell_to_queue( - rc, rt_it, result_tiles, tile_queue, to_delete); + auto res = add_next_cell_to_queue( + rc, rt_it, result_tiles, tile_queue, to_delete, merge_bound); { std::unique_lock ul(tile_queue_mutex_); - need_more_tiles |= res; + push_result(res); } } @@ -953,7 +1730,9 @@ SparseGlobalOrderReader::merge_result_cell_slabs( // Process all elements. bool user_buffers_full = false; - while (!tile_queue.empty() && !need_more_tiles && num_cells > 0) { + while (!tile_queue.empty() && + add_next_cell_result != AddNextCellResult::NeedMoreTiles && + num_cells > 0) { auto to_process = tile_queue.top(); auto tile = to_process.tile_; tile_queue.pop(); @@ -1013,14 +1792,24 @@ SparseGlobalOrderReader::merge_result_cell_slabs( tile_queue.pop(); // Put the next cell from the processed tile in the queue. - need_more_tiles = add_next_cell_to_queue( - to_remove, rt_it, result_tiles, tile_queue, to_delete); + push_result(add_next_cell_to_queue( + to_remove, + rt_it, + result_tiles, + tile_queue, + to_delete, + merge_bound)); } else { update_frag_idx(tile, to_process.pos_ + 1); // Put the next cell from the processed tile in the queue. - need_more_tiles = add_next_cell_to_queue( - to_process, rt_it, result_tiles, tile_queue, to_delete); + push_result(add_next_cell_to_queue( + to_process, + rt_it, + result_tiles, + tile_queue, + to_delete, + merge_bound)); to_process = tile_queue.top(); tile_queue.pop(); @@ -1050,11 +1839,32 @@ SparseGlobalOrderReader::merge_result_cell_slabs( // Compute the length of the cell slab. uint64_t length = 1; if (to_process.has_next_ || single_cell_only) { - if (tile_queue.empty()) { - length = to_process.max_slab_length(); + if (merge_bound.has_value()) { + // the cell slab may overlap the lower bound of tiles which aren't in + // the queue yet, clamp length using the merge bound (or tile queue) + // FIXME: shouldn't everything in the tile queue already be clamped + // by the merge bound? do we actually need this? + stdx::reverse_comparator> cmp( + stdx::or_equal(array_schema_.domain())); + + if (tile_queue.empty()) { + length = to_process.max_slab_length(merge_bound.value(), cmp); + } else if (cmp(tile_queue.top(), merge_bound.value())) { + length = to_process.max_slab_length(merge_bound.value(), cmp); + } else { + length = to_process.max_slab_length(tile_queue.top(), cmp); + } } else { - length = - to_process.max_slab_length(tile_queue.top(), cmp_max_slab_length); + if (add_next_cell_result == AddNextCellResult::NeedMoreTiles) { + // e.g. because we were hitting duplicate coords and reached + // the end of a tile while de-duplicating + length = 1; + } else if (tile_queue.empty()) { + length = to_process.max_slab_length(); + } else { + length = to_process.max_slab_length( + tile_queue.top(), cmp_max_slab_length); + } } } @@ -1105,8 +1915,8 @@ SparseGlobalOrderReader::merge_result_cell_slabs( } // Put the next cell in the queue. - need_more_tiles = add_next_cell_to_queue( - to_process, rt_it, result_tiles, tile_queue, to_delete); + push_result(add_next_cell_to_queue( + to_process, rt_it, result_tiles, tile_queue, to_delete, merge_bound)); } user_buffers_full = num_cells == 0; @@ -2181,8 +2991,9 @@ void SparseGlobalOrderReader::remove_result_tile( auto tiles_size = get_coord_tiles_size(array_schema_.dim_num(), frag_idx, tile_idx); - // Adjust per fragment memory usage. - memory_used_for_coords_[frag_idx] -= tiles_size; + if (!preprocess_tile_order_.enabled_) { + per_fragment_memory_state_.memory_used_for_coords_[frag_idx] -= tiles_size; + } // Adjust total memory usage. memory_used_for_coords_total_ -= tiles_size; @@ -2210,7 +3021,13 @@ void SparseGlobalOrderReader::end_iteration( })); if (!incomplete()) { - assert(memory_used_for_coords_total_ == 0); + if (preprocess_tile_order_.enabled_) { + [[maybe_unused]] const size_t mem_for_tile_order = + sizeof(ResultTileId) * preprocess_tile_order_.tiles_.size(); + assert(memory_used_for_coords_total_ == mem_for_tile_order); + } else { + assert(memory_used_for_coords_total_ == 0); + } assert(tmp_read_state_.memory_used_tile_ranges() == 0); } diff --git a/tiledb/sm/query/readers/sparse_global_order_reader.h b/tiledb/sm/query/readers/sparse_global_order_reader.h index d5da6f20ce2..e3d38defdfe 100644 --- a/tiledb/sm/query/readers/sparse_global_order_reader.h +++ b/tiledb/sm/query/readers/sparse_global_order_reader.h @@ -51,6 +51,113 @@ using namespace tiledb::common; namespace tiledb::sm { class Array; +struct PreprocessTileMergeFuture; +struct RangeLowerBound; + +enum class AddNextCellResult { + // finished the current tile + Done, + // successfully added a cell to the queue + FoundCell, + // more tiles from the same fragment are needed to continue + NeedMoreTiles, + // this tile cannot continue because it would be out of order with + // un-created result tiles + MergeBound +}; + +/** + * Identifies an order in which to load result tiles. + * See `preprocess_tile_order_`. + */ +struct PreprocessTileOrder { + bool enabled_; + size_t cursor_; + std::vector tiles_; + + bool has_more_tiles() const { + return enabled_ && cursor_ < tiles_.size(); + } + + /** + * Identifies the current position in the preprocess tile stream using + * the read state, and updates the cursor to that position. + * This is called after starting the result tile order. + * + * When running libtiledb natively, this is only called in the + * first instance of `tiledb_query_submit` and sets the cursor + * to that position. + * + * When running libtiledb against the REST server, this is called + * on the REST server for each `tiledb_query_submit`. + * We assume that recomputing the tile order for each message + * is cheaper than serializing the tile order after computing it once. + * However, as the read state progresses over the subarray, + * the tiles which qualify as input to the tile merge change. + * This causes the tile list to vary from submit to submit. + * Hence instead of serializing the position in the list we must + * recompute it. + */ + template + static uint64_t compute_cursor_from_read_state( + const RelevantFragments& relevant_fragments, + const std::span& read_state, + const std::span& tiles, + MergeFuture& merge_future) { + // The current position is that of the first tile in the list + // which comes after the last tile in the list from which + // data was emitted. + // + // Data was emitted from a tile if its `cell_idx_` is nonzero. + // + // In a synchronous world we can identify that tile trivially by + // walking backwards from the end of the list and finding + // the first nonzero `cell_idx_`. + // + // In an async world we want to walk forwards, so that we + // don't have to wait for the whole merge to finish. + + size_t bound = 0; + for (const auto f : relevant_fragments) { + std::optional f_bound; + for (uint64_t t = 0; t < tiles.size(); t++) { + merge_future.wait_for(t); + + if (tiles[t].fragment_idx_ != f) { + continue; + } + if (tiles[t].tile_idx_ < read_state[f].tile_idx_) { + f_bound.emplace(t + 1); + } else if (tiles[t].tile_idx_ == read_state[f].tile_idx_) { + if (read_state[f].cell_idx_ > 0) { + // this is the current tile, we have emitted some cells already + f_bound.emplace(t + 1); + } else if (f_bound.has_value()) { + // we exhausted the previous tile but did not emit anything from + // this one + } else { + // this is the first tile from the fragment, with no data emitted + } + break; + } else { + // this means we never saw the `==` tile, it did not qualify + // (is this even reachable except for the end of the fragment?) + // but the read state had advanced beyond the previous, so that's the + // bound + if (f_bound.has_value()) { + f_bound.emplace(f_bound.value() + 1); + } + break; + } + } + if (f_bound.has_value() && f_bound.value() > bound) { + bound = f_bound.value(); + } + } + + return bound; + } +}; /** Processes sparse global order read queries. */ @@ -86,7 +193,7 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, * * @return Status. */ - Status finalize() { + Status finalize() override { return Status::Ok(); } @@ -95,32 +202,32 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, * * @return The query status. */ - bool incomplete() const; + bool incomplete() const override; /** * Returns `true` if the query was incomplete. * * @return The query status. */ - QueryStatusDetailsReason status_incomplete_reason() const; + QueryStatusDetailsReason status_incomplete_reason() const override; /** * Initialize the memory budget variables. */ - void refresh_config(); + void refresh_config() override; /** * Performs a read query using its set members. * * @return Status. */ - Status dowork(); + Status dowork() override; /** Resets the reader object. */ - void reset(); + void reset() override; /** Returns the name of the strategy */ - std::string name(); + std::string name() override; private: /* ********************************* */ @@ -130,17 +237,38 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, /** UID of the logger instance */ inline static std::atomic logger_id_ = 0; + /** + * State for the optional mode to preprocess the tiles across + * all fragments and merge them into a single list which identifies + * the order they should be read in. + * + * This is used to merge the tiles + * into a single globally-ordered list prior to loading. + * Tile identifiers in this list are sorted using their starting ranges + * and have already had the subarray (if any) applied. + */ + PreprocessTileOrder preprocess_tile_order_; + /** * Result tiles currently for which we loaded coordinates but couldn't * process in the previous iteration. */ std::vector result_tiles_leftover_; - /** Memory used for coordinates tiles per fragment. */ - std::vector memory_used_for_coords_; - - /** Memory budget per fragment. */ - double per_fragment_memory_; + /** + * State for the mode to evenly distribute memory + * budget amongst the fragments and create a result + * tile per fragment regardless of how their tiles + * fit in the unified global order. + * + * Used only when preprocess tile order is not enabled. + */ + struct { + /** Memory used for coordinates tiles per fragment */ + std::vector memory_used_for_coords_; + /** Memory budget per fragment */ + double per_fragment_memory_; + } per_fragment_memory_state_; /** Enables consolidation with timestamps or not. */ bool consolidation_with_timestamps_; @@ -206,7 +334,6 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, * Add a result tile to process, making sure maximum budget is respected. * * @param dim_num Number of dimensions. - * @param memory_budget_coords_tiles Memory budget for coordinate tiles. * @param f Fragment index. * @param t Tile index. * @param frag_md Fragment metadata. @@ -216,21 +343,76 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, */ bool add_result_tile( const unsigned dim_num, - const uint64_t memory_budget_coords_tiles, const unsigned f, const uint64_t t, const FragmentMetadata& frag_md, std::vector& result_tiles); + /** + * Kicks off computation of the single list of tiles across all fragments + * arranged in the order they must be processed for this query. + * See `preprocess_tile_order_`. + * + * The `merge_future` in-out parameter provides memory locations for + * the parallel merge algorithm inputs - this includes the global order + * comparator as well as the fragment `ResultTileId` lists. This method sets + * the parallel merge algorithm future on `merge_future` as its postcondition. + * + * The merge output is written to `this->preprocess_tile_order_.tiles_` + * and the `merge_future` can be used to poll the state of this asynchronous + * operation. + * + * @param [in-out] merge_future where to put data for the merge and the merge + * + * @precondition `merge_future` is freshly constructed. + * @postcondition `merge_future.fragment_result_tiles_` is initialized with + * the lists of tiles from each fragment which qualify for the subarray. + * @postcondition `merge_future.cmp_` holds the memory for the comparator used + * in the asynchronous parallel merge. + * @postcondition `merge_future.merge_` is initialized and can be used to poll + * the results of the asynchronous parallel merge. + */ + void preprocess_compute_result_tile_order( + PreprocessTileMergeFuture& merge_future); + /** * Create the result tiles. * * @param result_tiles Result tiles per fragment. + * @param preprocess_future future for polling the global order tile + * stream, if running in preprocess mode + * * @return Newly created tiles. */ std::vector create_result_tiles( + std::vector& result_tiles, + std::optional& preprocess_future); + + /** + * Create the result tiles naively, without coordinating + * the ranges of tiles of each fragment. This uses a + * per-fragment memory budget, and (assuming enough memory) + * creates at least one result tile for each fragment. + * + * See `all_fragment_tile_order_`. + * + * @param result_tiles [in-out] Result tiles per fragment. + */ + void create_result_tiles_all_fragments( std::vector& result_tiles); + /** + * Create the result tiles using the pre-processed + * tile order. See `preprocess_tile_order_`. + * + * @param result_tiles [in-out] Result tiles per fragment. + * @param merge_future handle to polling the preprocess merge stream + * (if it has not completed yet) + */ + void create_result_tiles_using_preprocess( + std::vector& result_tiles, + std::optional& merge_future); + /** * Clean tiles that have 0 results from the tile lists. * @@ -311,16 +493,19 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, * @param result_tiles Result tiles per fragment. * @param tile_queue Queue of one result coords, per fragment, sorted. * @param to_delete List of tiles to delete. + * @param merge_bound reference to the bound where results can be correctly + * put in the queue * - * @return If more tiles are needed. + * @return result of trying to add a cell */ template - bool add_next_cell_to_queue( + AddNextCellResult add_next_cell_to_queue( GlobalOrderResultCoords& rc, std::vector& result_tiles_it, const std::vector& result_tiles, TileMinHeap& tile_queue, - std::vector& to_delete); + std::vector& to_delete, + const std::optional& merge_bound); /** * Computes a tile's Hilbert values for a tile. @@ -343,12 +528,16 @@ class SparseGlobalOrderReader : public SparseIndexReaderBase, * * @param num_cells Number of cells that can be copied in the user buffer. * @param result_tiles Result tiles per fragment. + * @param merge_future handle to polling the preprocess merge stream + * (if it has not completed yet). * * @return user_buffers_full, result_cell_slabs. */ template tuple> merge_result_cell_slabs( - uint64_t num_cells, std::vector& result_tiles); + uint64_t num_cells, + std::vector& result_tiles, + std::optional& merge_future); /** * Compute parallelization parameters for a tile copy operation. diff --git a/tiledb/sm/query/readers/sparse_index_reader_base.h b/tiledb/sm/query/readers/sparse_index_reader_base.h index 2a7553a70ed..b3ee49071c3 100644 --- a/tiledb/sm/query/readers/sparse_index_reader_base.h +++ b/tiledb/sm/query/readers/sparse_index_reader_base.h @@ -53,9 +53,6 @@ class Subarray; */ class FragIdx { public: - /* ********************************* */ - /* CONSTRUCTORS & DESTRUCTORS */ - /* ********************************* */ FragIdx() = default; FragIdx(uint64_t tile_idx, uint64_t cell_idx) @@ -63,36 +60,6 @@ class FragIdx { , cell_idx_(cell_idx) { } - /** Move constructor. */ - FragIdx(FragIdx&& other) noexcept { - // Swap with the argument - swap(other); - } - - /** Move-assign operator. */ - FragIdx& operator=(FragIdx&& other) { - // Swap with the argument - swap(other); - - return *this; - } - - DISABLE_COPY_AND_COPY_ASSIGN(FragIdx); - - /* ********************************* */ - /* PUBLIC METHODS */ - /* ********************************* */ - - /** Swaps the contents (all field values) of this tile with the given tile. */ - void swap(FragIdx& frag_tile_idx) { - std::swap(tile_idx_, frag_tile_idx.tile_idx_); - std::swap(cell_idx_, frag_tile_idx.cell_idx_); - } - - /* ********************************* */ - /* PUBLIC ATTRIBUTES */ - /* ********************************* */ - /** Tile index. */ uint64_t tile_idx_; diff --git a/tiledb/sm/query/readers/test/CMakeLists.txt b/tiledb/sm/query/readers/test/CMakeLists.txt index 8de0a4fa8c5..8d1b6479db5 100644 --- a/tiledb/sm/query/readers/test/CMakeLists.txt +++ b/tiledb/sm/query/readers/test/CMakeLists.txt @@ -27,6 +27,7 @@ include(unit_test) commence(unit_test readers) - this_target_sources(main.cc unit_reader_base.cc) + this_target_sources(main.cc unit_reader_base.cc unit_sparse_global_order_reader_preprocess_tile_order.cc) this_target_object_libraries(baseline) + this_target_link_libraries(rapidcheck) conclude(unit_test) diff --git a/tiledb/sm/query/readers/test/unit_sparse_global_order_reader_preprocess_tile_order.cc b/tiledb/sm/query/readers/test/unit_sparse_global_order_reader_preprocess_tile_order.cc new file mode 100644 index 00000000000..a876c00b28b --- /dev/null +++ b/tiledb/sm/query/readers/test/unit_sparse_global_order_reader_preprocess_tile_order.cc @@ -0,0 +1,416 @@ +/** + * @file unit_sparse_global_order_reader_preprocess_tile_order.cc + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2025 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * Tests the `SparseGlobalOrderReader` preprocess tile merge functionality. + */ + +#include "tiledb/common/common.h" +#include "tiledb/sm/query/readers/sparse_global_order_reader.h" +#include "tiledb/sm/query/readers/sparse_index_reader_base.h" + +struct VerifySetCursorFromReadState; + +// forward declarations of `showValue` overloads +// (these must be declared prior to including `rapidcheck/Show.hpp` for some +// reason) +namespace rc { +namespace detail { + +void showValue(const tiledb::sm::ResultTileId& rt, std::ostream& os); +void showValue(const tiledb::sm::FragIdx& f, std::ostream& os); +void showValue(const VerifySetCursorFromReadState& instance, std::ostream& os); + +} // namespace detail +} // namespace rc + +#include +#include +#include + +using namespace tiledb::sm; +using tiledb::test::AsserterCatch; +using tiledb::test::AsserterRapidcheck; + +struct NotAsync { + void wait_for(uint64_t) { + // no-op + } + + static NotAsync INSTANCE; +}; + +NotAsync NotAsync::INSTANCE = NotAsync(); + +struct VerifySetCursorFromReadState { + std::map read_state_; + std::vector qualified_tiles_; + + VerifySetCursorFromReadState( + std::map read_state, + std::vector qualified_tiles) + : read_state_(read_state) + , qualified_tiles_(qualified_tiles) { + } + + RelevantFragments relevant_fragments() const { + std::set distinct_fragments; + for (const auto& rt : qualified_tiles_) { + distinct_fragments.insert(rt.fragment_idx_); + } + + std::vector relevant_fragments( + distinct_fragments.begin(), distinct_fragments.end()); + return RelevantFragments(relevant_fragments); + } + + /** validate input */ + template + void validate() { + if (qualified_tiles_.empty()) { + ASSERTER(read_state_.empty()); + } + + // each fragment must have its tiles sorted + std::map last_tile; + for (const auto& rt : qualified_tiles_) { + if (last_tile.find(rt.fragment_idx_) != last_tile.end()) { + RC_PRE(last_tile[rt.fragment_idx_] < rt.tile_idx_); + } + last_tile[rt.fragment_idx_] = rt.tile_idx_; + } + + for (const auto& fragment : read_state_) { + const unsigned f = fragment.first; + const auto& frag_idx = fragment.second; + if (last_tile.find(f) != last_tile.end()) { + // read state does not have to be a tile in the list, + // but if it isn't then its cell had better be zero + bool found = frag_idx.cell_idx_ == 0; + for (size_t rti = 0; !found && rti < qualified_tiles_.size(); rti++) { + const auto& rt = qualified_tiles_[rti]; + if (rt.fragment_idx_ == f && rt.tile_idx_ == frag_idx.tile_idx_) { + found = true; + } + } + RC_PRE(found); + } else { + // no read state + RC_PRE(frag_idx.tile_idx_ == 0); + RC_PRE(frag_idx.cell_idx_ == 0); + } + } + } + + template + uint64_t verify() { + validate(); + + std::vector read_state; + if (!qualified_tiles_.empty()) { + read_state.resize( + 1 + std::max_element( + qualified_tiles_.begin(), + qualified_tiles_.end(), + [](const auto& a, const auto& b) { + return a.fragment_idx_ < b.fragment_idx_; + }) + ->fragment_idx_); + } + for (const auto& fragment : read_state_) { + read_state[fragment.first] = + FragIdx(fragment.second.tile_idx_, fragment.second.cell_idx_); + } + + const uint64_t cursor = PreprocessTileOrder::compute_cursor_from_read_state( + relevant_fragments(), read_state, qualified_tiles_, NotAsync::INSTANCE); + ASSERTER(cursor <= qualified_tiles_.size()); + + for (uint64_t rti = qualified_tiles_.size(); rti > cursor; rti--) { + const auto& rt = qualified_tiles_[rti - 1]; + const auto& rstate = read_state[rt.fragment_idx_]; + ASSERTER(rstate.tile_idx_ <= rt.tile_idx_); + if (rstate.tile_idx_ == rt.tile_idx_) { + ASSERTER(rstate.cell_idx_ == 0); + } + } + return cursor; + } +}; + +// rapidcheck generators +namespace rc { + +namespace detail { + +void showValue(const ResultTileId& rt, std::ostream& os) { + os << "ResultTileId { .fragment_idx_ = " << rt.fragment_idx_ + << ", .tile_idx_ = " << rt.tile_idx_ << "}" << std::endl; +} + +void showValue(const FragIdx& f, std::ostream& os) { + os << "FragIdx { .tile_idx_ = " << f.tile_idx_ + << ", .cell_idx_ = " << f.cell_idx_ << "}" << std::endl; +} + +void showValue(const VerifySetCursorFromReadState& instance, std::ostream& os) { + os << ".read_state_ = "; + showValue(instance.read_state_, os); + os << ".qualified_tiles_ = "; + showValue(instance.qualified_tiles_, os); +} + +} // namespace detail + +template <> +struct Arbitrary { + static Gen arbitrary() { + return gen::apply( + [](unsigned f, uint64_t t) { return ResultTileId(f, t); }, + gen::inRange(0, 1024), + gen::inRange(0, 1024 * 1024)); + } +}; + +Gen> make_qualified_tiles() { + auto gen_rts = + gen::container>(gen::arbitrary()); + + return gen::map(gen_rts, [](std::vector rts) { + std::vector rts_out(rts.size()); + std::map> fragments; + for (size_t rt = 0; rt < rts.size(); rt++) { + fragments[rts[rt].fragment_idx_].push_back(rt); + } + for (auto& f : fragments) { + const auto idx_unsorted = f.second; + auto idx_sorted = idx_unsorted; + std::sort(idx_sorted.begin(), idx_sorted.end(), [&](size_t a, size_t b) { + return rts[a].tile_idx_ < rts[b].tile_idx_; + }); + + for (size_t fi = 0; fi < idx_sorted.size(); fi++) { + rts_out[idx_unsorted[fi]] = rts[idx_sorted[fi]]; + } + } + return rts_out; + }); +} + +template <> +struct Arbitrary { + static Gen arbitrary() { + auto gen_tiles = make_qualified_tiles(); + + auto gen_read_states = + gen::mapcat(gen_tiles, [](std::vector tiles) { + if (tiles.empty()) { + return gen::pair( + gen::just(tiles), + gen::construct>>()); + } + return gen::pair( + gen::just(tiles), + gen::container>>( + gen::apply( + [](ResultTileId qualified_tile, Maybe cell_idx) + -> std::pair { + if (cell_idx) { + return std::make_pair( + qualified_tile.fragment_idx_, + FragIdx(qualified_tile.tile_idx_, *cell_idx)); + } else { + return std::make_pair( + qualified_tile.fragment_idx_, + FragIdx(qualified_tile.tile_idx_ + 1, 0)); + } + }, + gen::elementOf(tiles), + gen::maybe( + gen::inRange(0, 1024 * 1024 * 128))))); + }); + + return gen::apply( + [](std::pair< + std::vector, + std::vector>> arg) { + std::vector tiles = std::move(arg.first); + std::map read_state; + + for (auto& state : arg.second) { + read_state[state.first] = state.second; + } + + return VerifySetCursorFromReadState(read_state, tiles); + }, + gen_read_states); + } +}; + +} // namespace rc + +TEST_CASE( + "SparseGlobalOrderReader: PreprocessTileMerge: correct cursor " + "computation", + "[sparse-global-order][preprocess-tile-merge]") { + using RT = ResultTileId; + + SECTION("Example") { + std::map read_state; + + // partially done fragment + read_state[4] = FragIdx(7, 32); + // done, no more tiles in this fragment + read_state[6] = FragIdx(15, 0); + // other fragments not started + + std::vector tiles = { + RT(6, 8), + RT(6, 9), + RT(6, 10), + RT(6, 11), + RT(6, 12), + RT(6, 13), + RT(6, 14), + RT(4, 4), + RT(4, 5), + RT(4, 6), + RT(4, 7), + RT(8, 32), + RT(4, 8), + RT(8, 33), + RT(8, 34), + RT(8, 35), + RT(8, 36)}; + + const auto cursor = + VerifySetCursorFromReadState(read_state, tiles).verify(); + CHECK(cursor == 11); + } + + SECTION("Shrink", "Some examples found by rapidcheck") { + SECTION("Example 1") { + std::map read_state; + read_state[0] = FragIdx(0, 1); + + auto tiles = std::vector{ResultTileId(0, 0)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 1); + } + SECTION("Example 2") { + std::map read_state; + read_state[0] = FragIdx(0, 1); + read_state[1] = FragIdx(0, 0); + + auto tiles = std::vector{ + ResultTileId(0, 0), ResultTileId(1, 0), ResultTileId(0, 1)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 1); + } + SECTION("Example 3") { + std::map read_state; + read_state[0] = FragIdx(0, 0); + read_state[1] = FragIdx(0, 1); + + auto tiles = std::vector{ + ResultTileId(0, 0), + ResultTileId(1, 0), + ResultTileId(0, 1), + ResultTileId(0, 2), + ResultTileId(0, 3)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 2); + } + SECTION("Example 4") { + std::map read_state; + read_state[0] = FragIdx(0, 0); + read_state[1] = FragIdx(0, 0); + read_state[2] = FragIdx(0, 0); + + std::vector tiles = { + RT(2, 0), + RT(0, 0), + RT(0, 1), + RT(0, 2), + RT(0, 3), + RT(0, 4), + RT(0, 5), + RT(2, 1)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 0); + } + SECTION( + "Example 5", "Read state cell_idx=0 can be used as the bound tile") { + std::map read_state; + read_state[0] = FragIdx(3, 0); + read_state[1] = FragIdx(0, 0); + read_state[2] = FragIdx(0, 0); + + std::vector tiles = { + RT(2, 0), + RT(0, 0), + RT(0, 1), + RT(0, 2), + RT(0, 3), + RT(0, 4), + RT(0, 5), + RT(2, 1)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 4); + } + SECTION("Example 6") { + std::map read_state; + read_state[0] = FragIdx(0, 0); + read_state[1] = FragIdx(0, 1); + + std::vector tiles = {RT(0, 0), RT(0, 1), RT(1, 0), RT(0, 2)}; + + const auto cursor = VerifySetCursorFromReadState(read_state, tiles) + .verify(); + CHECK(cursor == 3); + } + } + + SECTION("Rapidcheck") { + rc::prop( + "verify_set_cursor_from_read_state", + [](VerifySetCursorFromReadState input) { + input.verify(); + }); + } +} diff --git a/tiledb/sm/serialization/query.cc b/tiledb/sm/serialization/query.cc index b6590c7e387..48e0b2df4dd 100644 --- a/tiledb/sm/serialization/query.cc +++ b/tiledb/sm/serialization/query.cc @@ -1574,6 +1574,16 @@ Status query_from_capnp( "Cannot deserialize; array pointer is null.")); } + // Deserialize Config + if (query_reader.hasConfig()) { + tdb_unique_ptr decoded_config = nullptr; + auto config_reader = query_reader.getConfig(); + RETURN_NOT_OK(config_from_capnp(config_reader, &decoded_config)); + if (decoded_config != nullptr) { + query->unsafe_set_config(*decoded_config); + } + } + // Deserialize query type (sanity check). auto type = query->type(); QueryType query_type = QueryType::READ; @@ -1618,16 +1628,6 @@ Status query_from_capnp( // fully-initialized Query until the final QueryStatus is deserialized. query->set_status(QueryStatus::UNINITIALIZED); - // Deserialize Config - if (query_reader.hasConfig()) { - tdb_unique_ptr decoded_config = nullptr; - auto config_reader = query_reader.getConfig(); - RETURN_NOT_OK(config_from_capnp(config_reader, &decoded_config)); - if (decoded_config != nullptr) { - query->unsafe_set_config(*decoded_config); - } - } - // It's important that deserialization of query channels/aggregates happens // before deserializing buffers. set_data_buffer won't know whether a buffer // is aggregate or not if the list of aggregates per channel is not populated. diff --git a/tiledb/sm/stats/duration_instrument.h b/tiledb/sm/stats/duration_instrument.h index 82ca74d6ac4..5158a7073b0 100644 --- a/tiledb/sm/stats/duration_instrument.h +++ b/tiledb/sm/stats/duration_instrument.h @@ -46,7 +46,7 @@ class Stats; /** * This class contains a simple duration instrument. */ -template +template class DurationInstrument { public: /* ****************************** */ @@ -54,7 +54,7 @@ class DurationInstrument { /* ****************************** */ /** Constructs a duration instrument object. */ - DurationInstrument(ParentType& parent_stats, const std::string stat_name) + DurationInstrument(ParentType& parent_stats, const StatNameType stat_name) : parent_stats_(parent_stats) , stat_name_(stat_name) , start_time_(std::chrono::high_resolution_clock::now()) { @@ -75,7 +75,7 @@ class DurationInstrument { ParentType& parent_stats_; /** Stat to report duration for. */ - const std::string stat_name_; + const StatNameType stat_name_; /** Start time of the duration instrument. */ std::chrono::high_resolution_clock::time_point start_time_; diff --git a/tiledb/sm/stats/stats.cc b/tiledb/sm/stats/stats.cc index 4f85b0f6924..a2330b367c1 100644 --- a/tiledb/sm/stats/stats.cc +++ b/tiledb/sm/stats/stats.cc @@ -189,6 +189,56 @@ void Stats::add_counter(const std::string& stat, uint64_t count) { } } +std::optional Stats::get_counter(const std::string& stat) const { + const std::string new_stat = prefix_ + stat; + std::unique_lock lck(mtx_); + auto maybe = counters_.find(new_stat); + if (maybe == counters_.end()) { + return std::nullopt; + } else { + return maybe->second; + } +} + +std::optional Stats::find_counter(const std::string& stat) const { + const auto mine = get_counter(stat); + if (mine.has_value()) { + return mine; + } + for (const auto& child : children_) { + const auto theirs = child.find_counter(stat); + if (theirs.has_value()) { + return theirs; + } + } + return std::nullopt; +} + +std::optional Stats::get_timer(const std::string& stat) const { + const std::string new_stat = prefix_ + stat; + std::unique_lock lck(mtx_); + auto maybe = timers_.find(new_stat); + if (maybe == timers_.end()) { + return std::nullopt; + } else { + return maybe->second; + } +} + +std::optional Stats::find_timer(const std::string& stat) const { + const auto mine = get_timer(stat); + if (mine.has_value()) { + return mine; + } + for (const auto& child : children_) { + const auto theirs = child.find_timer(stat); + if (theirs.has_value()) { + return theirs; + } + } + return std::nullopt; +} + DurationInstrument Stats::start_timer(const std::string& stat) { return DurationInstrument(*this, stat); } diff --git a/tiledb/sm/stats/stats.h b/tiledb/sm/stats/stats.h index 5cea25f2d37..5f375bfb261 100644 --- a/tiledb/sm/stats/stats.h +++ b/tiledb/sm/stats/stats.h @@ -163,6 +163,20 @@ class Stats { /** Adds `count` to the input counter stat. */ void add_counter(const std::string& stat, uint64_t count); + /** Returns the value of the counter for `stat`, if any */ + std::optional get_counter(const std::string& stat) const; + + /** Searches through the child stats to find a counter with the given name, + * and returns its value */ + std::optional find_counter(const std::string& stat) const; + + /** Returns the value of the timer for `stat`, if any */ + std::optional get_timer(const std::string& stat) const; + + /** Searches through the child stats to find a timer with the given name, + * and returns its value */ + std::optional find_timer(const std::string& stat) const; + /** Returns true if statistics are currently enabled. */ bool enabled() const; diff --git a/tiledb/type/apply_with_type.h b/tiledb/type/apply_with_type.h index 3ac89f9abe5..310ff3dc3d3 100644 --- a/tiledb/type/apply_with_type.h +++ b/tiledb/type/apply_with_type.h @@ -34,6 +34,7 @@ #define TILEDB_APPLY_WITH_TYPE_H #include "tiledb/sm/enums/datatype.h" +#include "tiledb/type/datatype_traits.h" using tiledb::sm::Datatype; @@ -56,64 +57,44 @@ concept TileDBNumeric = TileDBIntegral || std::floating_point; */ template inline auto apply_with_type(Fn&& f, Datatype type, Args&&... args) { +#define CASE(type) \ + case (type): \ + return f(datatype_traits<(type)>::value_type{}, std::forward(args)...) + switch (type) { - case Datatype::INT32: { - return f(int32_t{}, std::forward(args)...); - } - case Datatype::INT64: { - return f(int64_t{}, std::forward(args)...); - } - case Datatype::INT8: { - return f(int8_t{}, std::forward(args)...); - } - case Datatype::UINT8: { - return f(uint8_t{}, std::forward(args)...); - } - case Datatype::INT16: { - return f(int16_t{}, std::forward(args)...); - } - case Datatype::UINT16: { - return f(uint16_t{}, std::forward(args)...); - } - case Datatype::UINT32: { - return f(uint32_t{}, std::forward(args)...); - } - case Datatype::UINT64: { - return f(uint64_t{}, std::forward(args)...); - } - case Datatype::FLOAT32: { - return f(float{}, std::forward(args)...); - } - case Datatype::FLOAT64: { - return f(double{}, std::forward(args)...); - } - case Datatype::DATETIME_YEAR: - case Datatype::DATETIME_MONTH: - case Datatype::DATETIME_WEEK: - case Datatype::DATETIME_DAY: - case Datatype::DATETIME_HR: - case Datatype::DATETIME_MIN: - case Datatype::DATETIME_SEC: - case Datatype::DATETIME_MS: - case Datatype::DATETIME_US: - case Datatype::DATETIME_NS: - case Datatype::DATETIME_PS: - case Datatype::DATETIME_FS: - case Datatype::DATETIME_AS: - case Datatype::TIME_HR: - case Datatype::TIME_MIN: - case Datatype::TIME_SEC: - case Datatype::TIME_MS: - case Datatype::TIME_US: - case Datatype::TIME_NS: - case Datatype::TIME_PS: - case Datatype::TIME_FS: - case Datatype::TIME_AS: { - return f(int64_t{}, std::forward(args)...); - } - case Datatype::STRING_ASCII: { - return f(char{}, std::forward(args)...); - } + CASE(Datatype::INT32); + CASE(Datatype::INT64); + CASE(Datatype::INT8); + CASE(Datatype::UINT8); + CASE(Datatype::INT16); + CASE(Datatype::UINT16); + CASE(Datatype::UINT32); + CASE(Datatype::UINT64); + CASE(Datatype::FLOAT32); + CASE(Datatype::FLOAT64); + CASE(Datatype::DATETIME_YEAR); + CASE(Datatype::DATETIME_MONTH); + CASE(Datatype::DATETIME_WEEK); + CASE(Datatype::DATETIME_DAY); + CASE(Datatype::DATETIME_HR); + CASE(Datatype::DATETIME_MIN); + CASE(Datatype::DATETIME_SEC); + CASE(Datatype::DATETIME_MS); + CASE(Datatype::DATETIME_US); + CASE(Datatype::DATETIME_NS); + CASE(Datatype::DATETIME_PS); + CASE(Datatype::DATETIME_FS); + CASE(Datatype::DATETIME_AS); + CASE(Datatype::TIME_HR); + CASE(Datatype::TIME_MIN); + CASE(Datatype::TIME_SEC); + CASE(Datatype::TIME_MS); + CASE(Datatype::TIME_US); + CASE(Datatype::TIME_NS); + CASE(Datatype::TIME_PS); + CASE(Datatype::TIME_FS); + CASE(Datatype::TIME_AS); + CASE(Datatype::STRING_ASCII); default: { throw std::logic_error( "Datatype::" + datatype_str(type) + " is not a supported Datatype"); diff --git a/tiledb/type/datatype_traits.h b/tiledb/type/datatype_traits.h new file mode 100644 index 00000000000..e38c5883a25 --- /dev/null +++ b/tiledb/type/datatype_traits.h @@ -0,0 +1,235 @@ +/** + * @file tiledb/type/datatype_traits.h + * + * @section LICENSE + * + * The MIT License + * + * @copyright Copyright (c) 2023-2024 TileDB, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @section DESCRIPTION + * + * This file declares `datatype_traits` which provides definitions for + * generic programming over Datatypes + */ + +#ifndef TILEDB_DATATYPE_TRAITS_H +#define TILEDB_DATATYPE_TRAITS_H + +#include "tiledb/sm/enums/datatype.h" + +using tiledb::sm::Datatype; + +namespace tiledb::type { + +/** + * `datatype_traits` + * + * This will be specialized for each `Datatype` + */ +template +struct datatype_traits {}; + +template <> +struct datatype_traits { + using value_type = int32_t; +}; + +template <> +struct datatype_traits { + using value_type = int64_t; +}; + +template <> +struct datatype_traits { + using value_type = int8_t; +}; + +template <> +struct datatype_traits { + using value_type = uint8_t; +}; + +template <> +struct datatype_traits { + using value_type = int16_t; +}; + +template <> +struct datatype_traits { + using value_type = uint16_t; +}; + +template <> +struct datatype_traits { + using value_type = uint32_t; +}; + +template <> +struct datatype_traits { + using value_type = uint64_t; +}; + +template <> +struct datatype_traits { + using value_type = float; +}; + +template <> +struct datatype_traits { + using value_type = double; +}; + +struct datatype_datetime_traits { + using value_type = int64_t; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits + : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits : private datatype_datetime_traits { + using datatype_datetime_traits::value_type; +}; + +template <> +struct datatype_traits { + using value_type = char; +}; + +} // namespace tiledb::type + +#endif diff --git a/tiledb/type/range/range.h b/tiledb/type/range/range.h index 5871dc5230a..30d89659d0f 100644 --- a/tiledb/type/range/range.h +++ b/tiledb/type/range/range.h @@ -37,6 +37,7 @@ #include "tiledb/common/logger_public.h" #include "tiledb/common/pmr.h" #include "tiledb/common/tag.h" +#include "tiledb/common/types/untyped_datum.h" #include "tiledb/sm/enums/datatype.h" #include @@ -170,6 +171,17 @@ class Range { set_str_range(s1, s2); } + /** Constructs a range and sets fixed data using values and size of an + * integral type. */ + template + Range(const T& start, const T& end, const allocator_type& alloc = {}) + : Range( + static_cast(&start), + static_cast(&end), + sizeof(T), + alloc) { + } + /** * Construct from two values of a fixed size type. * @@ -352,6 +364,29 @@ class Range { std::memcpy(&range_[fixed_size], end, fixed_size); } + /** + * @return an un-typed non-owning view into the start of the range + */ + UntypedDatumView start_datum() const { + if (var_size_) { + return UntypedDatumView(range_.data(), range_start_size_); + } else { + return UntypedDatumView(start_fixed(), range_.size() / 2); + } + } + + /** + * @return an un-typed non-owning view into the end of the range + */ + UntypedDatumView end_datum() const { + if (var_size_) { + return UntypedDatumView( + range_.data() + range_start_size_, range_.size() - range_start_size_); + } else { + return UntypedDatumView(end_fixed(), range_.size() / 2); + } + } + /** Returns the start range as the requested type. */ template inline T start_as() const { diff --git a/vcpkg.json b/vcpkg.json index 8b6d524d37c..e21bbcce1bc 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -65,7 +65,8 @@ { "name": "libfaketime", "platform": "!windows" - } + }, + "rapidcheck" ] }, "tools": {