From 55107b6f7438f59789752e32f0327c0b6ba739ec Mon Sep 17 00:00:00 2001 From: duanmeng Date: Tue, 20 Aug 2024 23:47:26 -0700 Subject: [PATCH 01/24] Add Query Trace Writers and Readers (#10774) Summary: Add a query tracer to log the input data, and metadata (including query configurations, connector properties, and query plans). This logged data and metadata can be used to replay the operations of a specific operator or pipeline. Part of https://github.com/facebookincubator/velox/issues/9668 Pull Request resolved: https://github.com/facebookincubator/velox/pull/10774 Reviewed By: Yuhta Differential Revision: D61514971 Pulled By: xiaoxmeng fbshipit-source-id: 9a0b901ee1475a6c35169fe77eb19e797e31e210 --- velox/core/QueryConfig.cpp | 5 + velox/core/QueryConfig.h | 2 + velox/core/QueryCtx.h | 5 + velox/exec/CMakeLists.txt | 4 +- velox/exec/trace/CMakeLists.txt | 44 +++++ velox/exec/trace/QueryDataReader.cpp | 66 ++++++++ velox/exec/trace/QueryDataReader.h | 51 ++++++ velox/exec/trace/QueryDataWriter.cpp | 78 +++++++++ velox/exec/trace/QueryDataWriter.h | 60 +++++++ velox/exec/trace/QueryMetadataReader.cpp | 74 ++++++++ velox/exec/trace/QueryMetadataReader.h | 42 +++++ velox/exec/trace/QueryMetadataWriter.cpp | 75 +++++++++ velox/exec/trace/QueryMetadataWriter.h | 40 +++++ velox/exec/trace/QueryTraceConfig.cpp | 27 +++ velox/exec/trace/QueryTraceConfig.h | 35 ++++ velox/exec/trace/QueryTraceTraits.h | 34 ++++ velox/exec/trace/test/CMakeLists.txt | 31 ++++ velox/exec/trace/test/QueryTraceTest.cpp | 205 +++++++++++++++++++++++ 18 files changed, 877 insertions(+), 1 deletion(-) create mode 100644 velox/exec/trace/CMakeLists.txt create mode 100644 velox/exec/trace/QueryDataReader.cpp create mode 100644 velox/exec/trace/QueryDataReader.h create mode 100644 velox/exec/trace/QueryDataWriter.cpp create mode 100644 velox/exec/trace/QueryDataWriter.h create mode 100644 velox/exec/trace/QueryMetadataReader.cpp create mode 100644 velox/exec/trace/QueryMetadataReader.h create mode 100644 velox/exec/trace/QueryMetadataWriter.cpp create mode 100644 velox/exec/trace/QueryMetadataWriter.h create mode 100644 velox/exec/trace/QueryTraceConfig.cpp create mode 100644 velox/exec/trace/QueryTraceConfig.h create mode 100644 velox/exec/trace/QueryTraceTraits.h create mode 100644 velox/exec/trace/test/CMakeLists.txt create mode 100644 velox/exec/trace/test/QueryTraceTest.cpp diff --git a/velox/core/QueryConfig.cpp b/velox/core/QueryConfig.cpp index cc9b57b23d6aa..3d5b25ff94878 100644 --- a/velox/core/QueryConfig.cpp +++ b/velox/core/QueryConfig.cpp @@ -53,4 +53,9 @@ void QueryConfig::testingOverrideConfigUnsafe( config_ = std::make_unique(std::move(values)); } +std::unordered_map QueryConfig::rawConfigsCopy() + const { + return config_->rawConfigsCopy(); +} + } // namespace facebook::velox::core diff --git a/velox/core/QueryConfig.h b/velox/core/QueryConfig.h index 93984749a4e3a..78f51375fa45a 100644 --- a/velox/core/QueryConfig.h +++ b/velox/core/QueryConfig.h @@ -725,6 +725,8 @@ class QueryConfig { void testingOverrideConfigUnsafe( std::unordered_map&& values); + std::unordered_map rawConfigsCopy() const; + private: void validateConfig(); diff --git a/velox/core/QueryCtx.h b/velox/core/QueryCtx.h index bd890a899861d..7df296f1ff553 100644 --- a/velox/core/QueryCtx.h +++ b/velox/core/QueryCtx.h @@ -85,6 +85,11 @@ class QueryCtx : public std::enable_shared_from_this { return it->second.get(); } + const std::unordered_map>& + connectorSessionProperties() const { + return connectorSessionProperties_; + } + /// Overrides the previous configuration. Note that this function is NOT /// thread-safe and should probably only be used in tests. void testingOverrideConfigUnsafe( diff --git a/velox/exec/CMakeLists.txt b/velox/exec/CMakeLists.txt index 4bcc613213814..c68a9af02ea59 100644 --- a/velox/exec/CMakeLists.txt +++ b/velox/exec/CMakeLists.txt @@ -98,7 +98,8 @@ velox_link_libraries( velox_common_base velox_test_util velox_arrow_bridge - velox_common_compression) + velox_common_compression + velox_query_trace_exec) if(${VELOX_BUILD_TESTING}) add_subdirectory(fuzzer) @@ -112,3 +113,4 @@ if(${VELOX_ENABLE_BENCHMARKS}) endif() add_subdirectory(prefixsort) +add_subdirectory(trace) diff --git a/velox/exec/trace/CMakeLists.txt b/velox/exec/trace/CMakeLists.txt new file mode 100644 index 0000000000000..532f3ed27f328 --- /dev/null +++ b/velox/exec/trace/CMakeLists.txt @@ -0,0 +1,44 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_library(velox_query_trace_exec QueryMetadataWriter.cpp QueryTraceConfig.cpp + QueryDataWriter.cpp) + +target_link_libraries( + velox_query_trace_exec + PRIVATE + velox_common_io + velox_file + velox_core + velox_vector + velox_connector + velox_common_base + velox_presto_serializer) + +add_library(velox_query_trace_retrieve QueryDataReader.cpp + QueryMetadataReader.cpp) + +target_link_libraries( + velox_query_trace_retrieve + velox_common_io + velox_file + velox_core + velox_vector + velox_connector + velox_common_base + velox_hive_connector) + +if(${VELOX_BUILD_TESTING}) + add_subdirectory(test) +endif() diff --git a/velox/exec/trace/QueryDataReader.cpp b/velox/exec/trace/QueryDataReader.cpp new file mode 100644 index 0000000000000..b234330dd0e53 --- /dev/null +++ b/velox/exec/trace/QueryDataReader.cpp @@ -0,0 +1,66 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryDataReader.h" + +#include "velox/common/file/File.h" +#include "velox/connectors/hive/HiveDataSink.h" +#include "velox/connectors/hive/TableHandle.h" +#include "velox/exec/TableWriter.h" +#include "velox/exec/trace/QueryTraceTraits.h" + +namespace facebook::velox::exec::trace { + +QueryDataReader::QueryDataReader(std::string path, memory::MemoryPool* pool) + : path_(std::move(path)), + fs_(filesystems::getFileSystem(path_, nullptr)), + pool_(pool), + dataType_(getTraceDataType()), + dataStream_(getDataInputStream()) { + VELOX_CHECK_NOT_NULL(dataType_); + VELOX_CHECK_NOT_NULL(dataStream_); +} + +bool QueryDataReader::read(RowVectorPtr& batch) const { + if (dataStream_->atEnd()) { + batch = nullptr; + return false; + } + + VectorStreamGroup::read( + dataStream_.get(), pool_, dataType_, &batch, &readOptions_); + return true; +} + +RowTypePtr QueryDataReader::getTraceDataType() const { + const auto summaryFile = fs_->openFileForRead( + fmt::format("{}/{}", path_, QueryTraceTraits::kDataSummaryFileName)); + const auto summary = summaryFile->pread(0, summaryFile->size()); + VELOX_USER_CHECK(!summary.empty()); + folly::dynamic obj = folly::parseJson(summary); + return ISerializable::deserialize(obj["rowType"]); +} + +std::unique_ptr QueryDataReader::getDataInputStream() + const { + auto dataFile = fs_->openFileForRead( + fmt::format("{}/{}", path_, QueryTraceTraits::kDataFileName)); + // TODO: Make the buffer size configurable. + return std::make_unique( + std::move(dataFile), 1 << 20, pool_); +} + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryDataReader.h b/velox/exec/trace/QueryDataReader.h new file mode 100644 index 0000000000000..ad61bde5886f2 --- /dev/null +++ b/velox/exec/trace/QueryDataReader.h @@ -0,0 +1,51 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/common/file/FileInputStream.h" +#include "velox/common/file/FileSystems.h" +#include "velox/core/PlanNode.h" +#include "velox/core/QueryCtx.h" +#include "velox/serializers/PrestoSerializer.h" +#include "velox/vector/VectorStream.h" + +namespace facebook::velox::exec::trace { + +class QueryDataReader { + public: + explicit QueryDataReader(std::string path, memory::MemoryPool* pool); + + /// Reads from 'dataStream_' and deserializes to 'batch'. Returns false if + /// reaches to end of the stream and 'batch' is set to nullptr. + bool read(RowVectorPtr& batch) const; + + private: + RowTypePtr getTraceDataType() const; + + std::unique_ptr getDataInputStream() const; + + const std::string path_; + const serializer::presto::PrestoVectorSerde::PrestoOptions readOptions_{ + true, + common::CompressionKind_ZSTD, // TODO: Use trace config. + /*nullsFirst=*/true}; + const std::shared_ptr fs_; + memory::MemoryPool* const pool_; + const RowTypePtr dataType_; + const std::unique_ptr dataStream_; +}; +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryDataWriter.cpp b/velox/exec/trace/QueryDataWriter.cpp new file mode 100644 index 0000000000000..544e7a3d4580f --- /dev/null +++ b/velox/exec/trace/QueryDataWriter.cpp @@ -0,0 +1,78 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryDataWriter.h" +#include "velox/common/base/SpillStats.h" +#include "velox/common/file/File.h" +#include "velox/common/file/FileSystems.h" +#include "velox/exec/TreeOfLosers.h" +#include "velox/exec/UnorderedStreamReader.h" +#include "velox/exec/trace/QueryTraceTraits.h" +#include "velox/serializers/PrestoSerializer.h" + +namespace facebook::velox::exec::trace { + +QueryDataWriter::QueryDataWriter( + const std::string& path, + memory::MemoryPool* pool) + : dirPath_(path), + fs_(filesystems::getFileSystem(dirPath_, nullptr)), + pool_(pool) { + dataFile_ = fs_->openFileForWrite( + fmt::format("{}/{}", dirPath_, QueryTraceTraits::kDataFileName)); + VELOX_CHECK_NOT_NULL(dataFile_); +} + +void QueryDataWriter::write(const RowVectorPtr& rows) { + if (batch_ == nullptr) { + batch_ = std::make_unique(pool_); + batch_->createStreamTree( + std::static_pointer_cast(rows->type()), + 1'000, + &options_); + } + batch_->append(rows); + dataType_ = rows->type(); + + // Serialize and write out each batch. + IOBufOutputStream out( + *pool_, nullptr, std::max(64 * 1024, batch_->size())); + batch_->flush(&out); + batch_->clear(); + auto iobuf = out.getIOBuf(); + dataFile_->append(std::move(iobuf)); +} + +void QueryDataWriter::finish() { + VELOX_CHECK_NOT_NULL( + dataFile_, "The query data writer has already been finished"); + dataFile_->close(); + dataFile_.reset(); + batch_.reset(); + writeSummary(); +} + +void QueryDataWriter::writeSummary() const { + const auto summaryFilePath = + fmt::format("{}/{}", dirPath_, QueryTraceTraits::kDataSummaryFileName); + const auto file = fs_->openFileForWrite(summaryFilePath); + folly::dynamic obj = folly::dynamic::object; + obj[QueryTraceTraits::kDataTypeKey] = dataType_->serialize(); + file->append(folly::toJson(obj)); + file->close(); +} + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryDataWriter.h b/velox/exec/trace/QueryDataWriter.h new file mode 100644 index 0000000000000..435a0bccc00f9 --- /dev/null +++ b/velox/exec/trace/QueryDataWriter.h @@ -0,0 +1,60 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/common/file/File.h" +#include "velox/common/file/FileSystems.h" +#include "velox/exec/trace/QueryTraceTraits.h" +#include "velox/serializers/PrestoSerializer.h" +#include "velox/vector/VectorStream.h" + +namespace facebook::velox::exec::trace { + +/// Used to serialize and write the input vectors from a given operator into a +/// file. +class QueryDataWriter { + public: + explicit QueryDataWriter(const std::string& path, memory::MemoryPool* pool); + + /// Serializes rows and writes out each batch. + void write(const RowVectorPtr& rows); + + /// Closes the data file and writes out the data summary. + /// + /// NOTE: This method should be only called once. + void finish(); + + private: + // Flushes the trace data summaries to the disk. + // + // TODO: add more summaries such as number of rows etc. + void writeSummary() const; + + const std::string dirPath_; + // TODO: make 'useLosslessTimestamp' configuerable. + const serializer::presto::PrestoVectorSerde::PrestoOptions options_ = { + true, + common::CompressionKind::CompressionKind_ZSTD, + /*nullsFirst=*/true}; + const std::shared_ptr fs_; + memory::MemoryPool* const pool_; + std::unique_ptr dataFile_; + TypePtr dataType_; + std::unique_ptr batch_; +}; + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryMetadataReader.cpp b/velox/exec/trace/QueryMetadataReader.cpp new file mode 100644 index 0000000000000..8843aa49a2ab6 --- /dev/null +++ b/velox/exec/trace/QueryMetadataReader.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryMetadataReader.h" + +#include "velox/common/file/File.h" +#include "velox/common/file/FileSystems.h" +#include "velox/connectors/hive/HiveDataSink.h" +#include "velox/connectors/hive/TableHandle.h" +#include "velox/core/PlanNode.h" +#include "velox/exec/PartitionFunction.h" +#include "velox/exec/TableWriter.h" +#include "velox/exec/trace/QueryTraceTraits.h" + +namespace facebook::velox::exec::trace { + +QueryMetadataReader::QueryMetadataReader( + std::string traceDir, + memory::MemoryPool* pool) + : traceDir_(std::move(traceDir)), + fs_(filesystems::getFileSystem(traceDir_, nullptr)), + metaFilePath_(fmt::format( + "{}/{}", + traceDir_, + QueryTraceTraits::kQueryMetaFileName)), + pool_(pool) { + VELOX_CHECK_NOT_NULL(fs_); + VELOX_CHECK(fs_->exists(metaFilePath_)); +} + +void QueryMetadataReader::read( + std::unordered_map& queryConfigs, + std::unordered_map< + std::string, + std::unordered_map>& connectorProperties, + core::PlanNodePtr& queryPlan) const { + const auto file = fs_->openFileForRead(metaFilePath_); + VELOX_CHECK_NOT_NULL(file); + const auto metadata = file->pread(0, file->size()); + VELOX_USER_CHECK(!metadata.empty()); + folly::dynamic obj = folly::parseJson(metadata); + + const auto& queryConfigObj = obj[QueryTraceTraits::kQueryConfigKey]; + for (const auto& [key, value] : queryConfigObj.items()) { + queryConfigs[key.asString()] = value.asString(); + } + + const auto& connectorPropertiesObj = + obj[QueryTraceTraits::kConnectorPropertiesKey]; + for (const auto& [connectorId, configs] : connectorPropertiesObj.items()) { + const auto connectorIdStr = connectorId.asString(); + connectorProperties[connectorIdStr] = {}; + for (const auto& [key, value] : configs.items()) { + connectorProperties[connectorIdStr][key.asString()] = value.asString(); + } + } + + queryPlan = ISerializable::deserialize( + obj[QueryTraceTraits::kPlanNodeKey], pool_); +} +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryMetadataReader.h b/velox/exec/trace/QueryMetadataReader.h new file mode 100644 index 0000000000000..71217653d4dc3 --- /dev/null +++ b/velox/exec/trace/QueryMetadataReader.h @@ -0,0 +1,42 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/common/file/FileSystems.h" +#include "velox/core/PlanNode.h" +#include "velox/core/QueryCtx.h" +#include "velox/vector/VectorStream.h" + +namespace facebook::velox::exec::trace { +class QueryMetadataReader { + public: + explicit QueryMetadataReader(std::string traceDir, memory::MemoryPool* pool); + + void read( + std::unordered_map& queryConfigs, + std::unordered_map< + std::string, + std::unordered_map>& connectorProperties, + core::PlanNodePtr& queryPlan) const; + + private: + const std::string traceDir_; + const std::shared_ptr fs_; + const std::string metaFilePath_; + memory::MemoryPool* const pool_; +}; +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryMetadataWriter.cpp b/velox/exec/trace/QueryMetadataWriter.cpp new file mode 100644 index 0000000000000..beba2e8097469 --- /dev/null +++ b/velox/exec/trace/QueryMetadataWriter.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryMetadataWriter.h" +#include "velox/common/config/Config.h" +#include "velox/common/file/File.h" +#include "velox/connectors/hive/HiveDataSink.h" +#include "velox/connectors/hive/TableHandle.h" +#include "velox/core/PlanNode.h" +#include "velox/core/QueryCtx.h" +#include "velox/exec/TableWriter.h" +#include "velox/exec/trace/QueryTraceTraits.h" + +namespace facebook::velox::exec::trace { + +QueryMetadataWriter::QueryMetadataWriter( + std::string traceDir, + memory::MemoryPool* pool) + : traceDir_(std::move(traceDir)), + fs_(filesystems::getFileSystem(traceDir_, nullptr)), + metaFilePath_(fmt::format( + "{}/{}", + traceDir_, + QueryTraceTraits::kQueryMetaFileName)), + pool_(pool) { + VELOX_CHECK_NOT_NULL(fs_); + VELOX_CHECK(!fs_->exists(metaFilePath_)); +} + +void QueryMetadataWriter::write( + const std::shared_ptr& queryCtx, + const core::PlanNodePtr& planNode) { + VELOX_CHECK(!finished_, "Query metadata can only be written once"); + finished_ = true; + folly::dynamic queryConfigObj = folly::dynamic::object; + const auto configValues = queryCtx->queryConfig().rawConfigsCopy(); + for (const auto& [key, value] : configValues) { + queryConfigObj[key] = value; + } + + folly::dynamic connectorPropertiesObj = folly::dynamic::object; + for (const auto& [connectorId, configs] : + queryCtx->connectorSessionProperties()) { + folly::dynamic obj = folly::dynamic::object; + for (const auto& [key, value] : configs->rawConfigsCopy()) { + obj[key] = value; + } + connectorPropertiesObj[connectorId] = obj; + } + + folly::dynamic metaObj = folly::dynamic::object; + metaObj[QueryTraceTraits::kQueryConfigKey] = queryConfigObj; + metaObj[QueryTraceTraits::kConnectorPropertiesKey] = connectorPropertiesObj; + metaObj[QueryTraceTraits::kPlanNodeKey] = planNode->serialize(); + + const auto metaStr = folly::toJson(metaObj); + const auto file = fs_->openFileForWrite(metaFilePath_); + file->append(metaStr); + file->close(); +} + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryMetadataWriter.h b/velox/exec/trace/QueryMetadataWriter.h new file mode 100644 index 0000000000000..f2cc661238514 --- /dev/null +++ b/velox/exec/trace/QueryMetadataWriter.h @@ -0,0 +1,40 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/common/file/FileSystems.h" +#include "velox/core/PlanNode.h" +#include "velox/core/QueryCtx.h" +#include "velox/vector/VectorStream.h" + +namespace facebook::velox::exec::trace { +class QueryMetadataWriter { + public: + explicit QueryMetadataWriter(std::string traceDir, memory::MemoryPool* pool); + + void write( + const std::shared_ptr& queryCtx, + const core::PlanNodePtr& planNode); + + private: + const std::string traceDir_; + const std::shared_ptr fs_; + const std::string metaFilePath_; + memory::MemoryPool* const pool_; + bool finished_{false}; +}; +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceConfig.cpp b/velox/exec/trace/QueryTraceConfig.cpp new file mode 100644 index 0000000000000..2437a632fb432 --- /dev/null +++ b/velox/exec/trace/QueryTraceConfig.cpp @@ -0,0 +1,27 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryTraceConfig.h" + +namespace facebook::velox::exec::trace { + +QueryTraceConfig::QueryTraceConfig( + std::unordered_set _queryNodeIds, + std::string _queryTraceDir) + : queryNodes(std::move(_queryNodeIds)), + queryTraceDir(std::move(_queryTraceDir)) {} + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceConfig.h b/velox/exec/trace/QueryTraceConfig.h new file mode 100644 index 0000000000000..8afcbf22fb53d --- /dev/null +++ b/velox/exec/trace/QueryTraceConfig.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace facebook::velox::exec::trace { +struct QueryTraceConfig { + /// Target query trace nodes + std::unordered_set queryNodes; + /// Base dir of query trace, normmaly it is $prefix/$taskId. + std::string queryTraceDir; + + QueryTraceConfig( + std::unordered_set _queryNodeIds, + std::string _queryTraceDir); + + QueryTraceConfig() = default; +}; +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceTraits.h b/velox/exec/trace/QueryTraceTraits.h new file mode 100644 index 0000000000000..5e3a91f783d3e --- /dev/null +++ b/velox/exec/trace/QueryTraceTraits.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace facebook::velox::exec::trace { +/// Defines the shared constants used by query trace implementation. +struct QueryTraceTraits { + static inline const std::string kPlanNodeKey = "planNode"; + static inline const std::string kQueryConfigKey = "queryConfig"; + static inline const std::string kDataTypeKey = "rowType"; + static inline const std::string kConnectorPropertiesKey = + "connectorProperties"; + + static inline const std::string kQueryMetaFileName = "query_meta.json"; + static inline const std::string kDataSummaryFileName = "data_summary.json"; + static inline const std::string kDataFileName = "trace.data"; +}; +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/test/CMakeLists.txt b/velox/exec/trace/test/CMakeLists.txt new file mode 100644 index 0000000000000..9c4ffe9dd3585 --- /dev/null +++ b/velox/exec/trace/test/CMakeLists.txt @@ -0,0 +1,31 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +add_executable(velox_exec_trace_test QueryTraceTest.cpp) + +add_test( + NAME velox_exec_trace_test + COMMAND velox_exec_trace_test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +set_tests_properties(velox_exec_trace_test PROPERTIES TIMEOUT 3000) + +target_link_libraries( + velox_exec_trace_test + velox_exec + velox_exec_test_lib + velox_memory + velox_query_trace_exec + velox_query_trace_retrieve + velox_vector_fuzzer) diff --git a/velox/exec/trace/test/QueryTraceTest.cpp b/velox/exec/trace/test/QueryTraceTest.cpp new file mode 100644 index 0000000000000..765eeccaaf532 --- /dev/null +++ b/velox/exec/trace/test/QueryTraceTest.cpp @@ -0,0 +1,205 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include + +#include "velox/common/file/FileSystems.h" +#include "velox/exec/PartitionFunction.h" +#include "velox/exec/tests/utils/ArbitratorTestUtil.h" +#include "velox/exec/tests/utils/HiveConnectorTestBase.h" +#include "velox/exec/tests/utils/TempDirectoryPath.h" +#include "velox/exec/trace/QueryDataReader.h" +#include "velox/exec/trace/QueryDataWriter.h" +#include "velox/exec/trace/QueryMetadataReader.h" +#include "velox/exec/trace/QueryMetadataWriter.h" +#include "velox/serializers/PrestoSerializer.h" +#include "velox/vector/tests/utils/VectorTestBase.h" + +namespace facebook::velox::exec::test { +class QueryTracerTest : public HiveConnectorTestBase { + protected: + static void SetUpTestCase() { + memory::MemoryManager::testingSetInstance({}); + HiveConnectorTestBase::SetUpTestCase(); + filesystems::registerLocalFileSystem(); + if (!isRegisteredVectorSerde()) { + serializer::presto::PrestoVectorSerde::registerVectorSerde(); + } + Type::registerSerDe(); + common::Filter::registerSerDe(); + connector::hive::HiveTableHandle::registerSerDe(); + connector::hive::LocationHandle::registerSerDe(); + connector::hive::HiveColumnHandle::registerSerDe(); + connector::hive::HiveInsertTableHandle::registerSerDe(); + core::PlanNode::registerSerDe(); + core::ITypedExpr::registerSerDe(); + registerPartitionFunctionSerDe(); + } + + static VectorFuzzer::Options getFuzzerOptions() { + return VectorFuzzer::Options{ + .vectorSize = 16, + .nullRatio = 0.2, + .stringLength = 1024, + .stringVariableLength = false, + .allowLazyVector = false, + }; + } + + QueryTracerTest() : vectorFuzzer_{getFuzzerOptions(), pool_.get()} { + filesystems::registerLocalFileSystem(); + } + + RowTypePtr generateTypes(size_t numColumns) { + std::vector names; + names.reserve(numColumns); + std::vector types; + types.reserve(numColumns); + for (auto i = 0; i < numColumns; ++i) { + names.push_back(fmt::format("c{}", i)); + types.push_back(vectorFuzzer_.randType((2))); + } + return ROW(std::move(names), std::move(types)); + ; + } + + bool isSamePlan( + const core::PlanNodePtr& left, + const core::PlanNodePtr& right) { + if (left->id() != right->id() || left->name() != right->name()) { + return false; + } + + if (left->sources().size() != right->sources().size()) { + return false; + } + + for (auto i = 0; i < left->sources().size(); ++i) { + isSamePlan(left->sources().at(i), right->sources().at(i)); + } + return true; + } + + VectorFuzzer vectorFuzzer_; +}; + +TEST_F(QueryTracerTest, traceData) { + const auto rowType = generateTypes(5); + std::vector inputVectors; + constexpr auto numBatch = 3; + inputVectors.reserve(numBatch); + for (auto i = 0; i < numBatch; ++i) { + inputVectors.push_back(vectorFuzzer_.fuzzInputRow(rowType)); + } + + const auto outputDir = TempDirectoryPath::create(); + auto writer = trace::QueryDataWriter(outputDir->getPath(), pool()); + for (auto i = 0; i < numBatch; ++i) { + writer.write(inputVectors[i]); + } + writer.finish(); + + const auto reader = trace::QueryDataReader(outputDir->getPath(), pool()); + RowVectorPtr actual; + size_t numOutputVectors{0}; + while (reader.read(actual)) { + const auto expected = inputVectors[numOutputVectors]; + const auto size = actual->size(); + ASSERT_EQ(size, expected->size()); + for (auto i = 0; i < size; ++i) { + actual->compare(expected.get(), i, i, {.nullsFirst = true}); + } + ++numOutputVectors; + } + ASSERT_EQ(numOutputVectors, inputVectors.size()); +} + +TEST_F(QueryTracerTest, traceMetadata) { + const auto rowType = + ROW({"c0", "c1", "c2", "c3", "c4", "c5"}, + {BIGINT(), SMALLINT(), TINYINT(), VARCHAR(), VARCHAR(), VARCHAR()}); + std::vector rows; + constexpr auto numBatch = 1; + rows.reserve(numBatch); + for (auto i = 0; i < numBatch; ++i) { + rows.push_back(vectorFuzzer_.fuzzRow(rowType, 2)); + } + + const auto outputDir = TempDirectoryPath::create(); + auto planNodeIdGenerator = std::make_shared(); + const auto planNode = + PlanBuilder(planNodeIdGenerator) + .values(rows, false) + .project({"c0", "c1", "c2"}) + .hashJoin( + {"c0"}, + {"u0"}, + PlanBuilder(planNodeIdGenerator) + .values(rows, true) + .singleAggregation({"c0", "c1"}, {"min(c2)"}) + .project({"c0 AS u0", "c1 AS u1", "a0 AS u2"}) + .planNode(), + "c0 < 135", + {"c0", "c1", "c2"}, + core::JoinType::kInner) + .planNode(); + const auto expectedQueryConfigs = + std::unordered_map{ + {core::QueryConfig::kSpillEnabled, "true"}, + {core::QueryConfig::kSpillNumPartitionBits, "17"}, + {"key1", "value1"}, + }; + const auto expectedConnectorProperties = + std::unordered_map>{ + {"test_trace", + std::make_shared( + std::unordered_map{ + {"cKey1", "cVal1"}})}}; + const auto queryCtx = core::QueryCtx::create( + executor_.get(), + core::QueryConfig(expectedQueryConfigs), + expectedConnectorProperties); + auto writer = trace::QueryMetadataWriter(outputDir->getPath(), pool()); + writer.write(queryCtx, planNode); + + std::unordered_map acutalQueryConfigs; + std::unordered_map> + actualConnectorProperties; + core::PlanNodePtr actualQueryPlan; + auto reader = trace::QueryMetadataReader(outputDir->getPath(), pool()); + reader.read(acutalQueryConfigs, actualConnectorProperties, actualQueryPlan); + + ASSERT_TRUE(isSamePlan(actualQueryPlan, planNode)); + ASSERT_EQ(acutalQueryConfigs.size(), expectedQueryConfigs.size()); + for (const auto& [key, value] : acutalQueryConfigs) { + ASSERT_EQ(acutalQueryConfigs.at(key), expectedQueryConfigs.at(key)); + } + + ASSERT_EQ( + actualConnectorProperties.size(), expectedConnectorProperties.size()); + ASSERT_EQ(actualConnectorProperties.count("test_trace"), 1); + const auto expectedConnectorConfigs = + expectedConnectorProperties.at("test_trace")->rawConfigsCopy(); + const auto actualConnectorConfigs = + actualConnectorProperties.at("test_trace"); + for (const auto& [key, value] : actualConnectorConfigs) { + ASSERT_EQ(actualConnectorConfigs.at(key), expectedConnectorConfigs.at(key)); + } +} +} // namespace facebook::velox::exec::test From df771053f1987592fd7677960bce0482c9bbc36e Mon Sep 17 00:00:00 2001 From: Karthikeyan Natarajan Date: Wed, 21 Aug 2024 11:27:57 -0700 Subject: [PATCH 02/24] Add separate map for Custom Bridges (#10626) Summary: Since both Velox bridges, and custom bridges were stored in same map, which did not allow translators to replace/add custom bridges. This change fixes this issue by adding a new map for custom bridges. A unit test is added to replace the Velox operators with custom operators, and custom bridges, using driver adapter and translator. Background: To enable running velox operators using libcudf, velox operators will be replaced by custom operators using driver adapter. Bridges are not replaced by driver adapter. But these new operators should also have custom bridges associated with it, to communicate between Build and Probe operators. To create these bridges, we use translator interface provided by velox. Since already a Bridge is stored in a map `bridges` for velox bridges, the translator call does not replace existing bridge and fails silently. This PR separates this map to 2 maps, one for velox join node, another for custom bridges created by translators. This change also helps other driver adaptor implementation (such as wave) to communicate between pipelines using custom bridges. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10626 Reviewed By: Yuhta Differential Revision: D61549695 Pulled By: kgpai fbshipit-source-id: ca58f39f55bcf7a57fc457478a0b489b9eda9f98 --- velox/exec/Task.cpp | 59 +++- velox/exec/Task.h | 7 +- velox/exec/TaskStructs.h | 5 + velox/exec/tests/CMakeLists.txt | 9 + velox/exec/tests/OperatorReplacement.cpp | 386 +++++++++++++++++++++++ 5 files changed, 451 insertions(+), 15 deletions(-) create mode 100644 velox/exec/tests/OperatorReplacement.cpp diff --git a/velox/exec/Task.cpp b/velox/exec/Task.cpp index 8bf121569b248..38006f9baf531 100644 --- a/velox/exec/Task.cpp +++ b/velox/exec/Task.cpp @@ -1098,6 +1098,9 @@ std::vector> Task::createDriversLocked( for (auto& bridgeEntry : splitGroupState.bridges) { bridgeEntry.second->start(); } + for (auto& bridgeEntry : splitGroupState.custom_bridges) { + bridgeEntry.second->start(); + } return drivers; } @@ -1753,8 +1756,12 @@ void Task::addHashJoinBridgesLocked( const std::vector& planNodeIds) { auto& splitGroupState = splitGroupStates_[splitGroupId]; for (const auto& planNodeId : planNodeIds) { - splitGroupState.bridges.emplace( - planNodeId, std::make_shared()); + auto const inserted = + splitGroupState.bridges + .emplace(planNodeId, std::make_shared()) + .second; + VELOX_CHECK( + inserted, "Join bridge for node {} is already present", planNodeId); } } @@ -1764,7 +1771,13 @@ void Task::addCustomJoinBridgesLocked( auto& splitGroupState = splitGroupStates_[splitGroupId]; for (const auto& planNode : planNodes) { if (auto joinBridge = Operator::joinBridgeFromPlanNode(planNode)) { - splitGroupState.bridges.emplace(planNode->id(), std::move(joinBridge)); + auto const inserted = splitGroupState.custom_bridges + .emplace(planNode->id(), std::move(joinBridge)) + .second; + VELOX_CHECK( + inserted, + "Join bridge for node {} is already present", + planNode->id()); return; } } @@ -1773,7 +1786,7 @@ void Task::addCustomJoinBridgesLocked( std::shared_ptr Task::getCustomJoinBridge( uint32_t splitGroupId, const core::PlanNodeId& planNodeId) { - return getJoinBridgeInternal(splitGroupId, planNodeId); + return getCustomJoinBridgeInternal(splitGroupId, planNodeId); } void Task::addNestedLoopJoinBridgesLocked( @@ -1781,8 +1794,12 @@ void Task::addNestedLoopJoinBridgesLocked( const std::vector& planNodeIds) { auto& splitGroupState = splitGroupStates_[splitGroupId]; for (const auto& planNodeId : planNodeIds) { - splitGroupState.bridges.emplace( - planNodeId, std::make_shared()); + auto const inserted = + splitGroupState.bridges + .emplace(planNodeId, std::make_shared()) + .second; + VELOX_CHECK( + inserted, "Join bridge for node {} is already present", planNodeId); } } @@ -1795,7 +1812,8 @@ std::shared_ptr Task::getHashJoinBridge( std::shared_ptr Task::getHashJoinBridgeLocked( uint32_t splitGroupId, const core::PlanNodeId& planNodeId) { - return getJoinBridgeInternalLocked(splitGroupId, planNodeId); + return getJoinBridgeInternalLocked( + splitGroupId, planNodeId, &SplitGroupState::bridges); } std::shared_ptr Task::getNestedLoopJoinBridge( @@ -1809,26 +1827,28 @@ std::shared_ptr Task::getJoinBridgeInternal( uint32_t splitGroupId, const core::PlanNodeId& planNodeId) { std::lock_guard l(mutex_); - return getJoinBridgeInternalLocked(splitGroupId, planNodeId); + return getJoinBridgeInternalLocked( + splitGroupId, planNodeId, &SplitGroupState::bridges); } -template +template std::shared_ptr Task::getJoinBridgeInternalLocked( uint32_t splitGroupId, - const core::PlanNodeId& planNodeId) { + const core::PlanNodeId& planNodeId, + MemberType SplitGroupState::*bridges_member) { const auto& splitGroupState = splitGroupStates_[splitGroupId]; - auto it = splitGroupState.bridges.find(planNodeId); - if (it == splitGroupState.bridges.end()) { + auto it = (splitGroupState.*bridges_member).find(planNodeId); + if (it == (splitGroupState.*bridges_member).end()) { // We might be looking for a bridge between grouped and ungrouped execution. // It will belong to the 'ungrouped' state. if (isGroupedExecution() && splitGroupId != kUngroupedGroupId) { return getJoinBridgeInternalLocked( - kUngroupedGroupId, planNodeId); + kUngroupedGroupId, planNodeId, bridges_member); } } VELOX_CHECK( - it != splitGroupState.bridges.end(), + it != (splitGroupState.*bridges_member).end(), "Join bridge for plan node ID {} not found for group {}, task {}", planNodeId, splitGroupId, @@ -1842,6 +1862,14 @@ std::shared_ptr Task::getJoinBridgeInternalLocked( return bridge; } +std::shared_ptr Task::getCustomJoinBridgeInternal( + uint32_t splitGroupId, + const core::PlanNodeId& planNodeId) { + std::lock_guard l(mutex_); + return getJoinBridgeInternalLocked( + splitGroupId, planNodeId, &SplitGroupState::custom_bridges); +} + // static std::string Task::shortId(const std::string& id) { if (id.size() < 12) { @@ -1958,6 +1986,9 @@ ContinueFuture Task::terminate(TaskState terminalState) { for (auto& pair : splitGroupState.second.bridges) { oldBridges.emplace_back(std::move(pair.second)); } + for (auto& pair : splitGroupState.second.custom_bridges) { + oldBridges.emplace_back(std::move(pair.second)); + } splitGroupStates.push_back(std::move(splitGroupState.second)); } diff --git a/velox/exec/Task.h b/velox/exec/Task.h index dae427eb97c91..433df9a0451e5 100644 --- a/velox/exec/Task.h +++ b/velox/exec/Task.h @@ -835,8 +835,13 @@ class Task : public std::enable_shared_from_this { uint32_t splitGroupId, const core::PlanNodeId& planNodeId); - template + template std::shared_ptr getJoinBridgeInternalLocked( + uint32_t splitGroupId, + const core::PlanNodeId& planNodeId, + MemberType SplitGroupState::*bridges_member); + + std::shared_ptr getCustomJoinBridgeInternal( uint32_t splitGroupId, const core::PlanNodeId& planNodeId); diff --git a/velox/exec/TaskStructs.h b/velox/exec/TaskStructs.h index 5585b53098e2a..3ddc147b65274 100644 --- a/velox/exec/TaskStructs.h +++ b/velox/exec/TaskStructs.h @@ -88,7 +88,11 @@ struct LocalExchangeState { /// Stores inter-operator state (exchange, bridges) for split groups. struct SplitGroupState { /// Map from the plan node id of the join to the corresponding JoinBridge. + /// This map will contain only HashJoinBridge and NestedLoopJoinBridge. std::unordered_map> bridges; + /// This map will contain all other custom bridges. + std::unordered_map> + custom_bridges; /// Holds states for Task::allPeersFinished. std::unordered_map barriers; @@ -125,6 +129,7 @@ struct SplitGroupState { void clear() { if (!mixedExecutionMode) { bridges.clear(); + custom_bridges.clear(); barriers.clear(); } localMergeSources.clear(); diff --git a/velox/exec/tests/CMakeLists.txt b/velox/exec/tests/CMakeLists.txt index 00230f53464fa..32a46e398432b 100644 --- a/velox/exec/tests/CMakeLists.txt +++ b/velox/exec/tests/CMakeLists.txt @@ -327,3 +327,12 @@ add_test( WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries( cpr_http_client_test cpr::cpr GTest::gtest GTest::gtest_main) + +add_executable(velox_driver_test OperatorReplacement.cpp Main.cpp) +add_test( + NAME velox_driver_test + COMMAND velox_driver_test + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries( + velox_driver_test velox_exec velox_exec_test_lib GTest::gtest) diff --git a/velox/exec/tests/OperatorReplacement.cpp b/velox/exec/tests/OperatorReplacement.cpp new file mode 100644 index 0000000000000..dd39ea32e7f7b --- /dev/null +++ b/velox/exec/tests/OperatorReplacement.cpp @@ -0,0 +1,386 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "velox/exec/JoinBridge.h" +#include "velox/exec/tests/utils/OperatorTestBase.h" +#include "velox/exec/tests/utils/PlanBuilder.h" + +using namespace facebook::velox; +using namespace facebook::velox::exec; +using namespace facebook::velox::exec::test; + +namespace { +using ReplacedJoinNode = typename facebook::velox::core::HashJoinNode; + +class CustomJoinBridge : public JoinBridge { + public: + void setNumRows(std::optional numRows) { + std::vector promises; + { + std::lock_guard l(mutex_); + VELOX_CHECK(!numRows_.has_value(), "setNumRows may be called only once"); + numRows_ = numRows; + promises = std::move(promises_); + } + notify(std::move(promises)); + } + + std::optional numRowsOrFuture(ContinueFuture* future) { + std::lock_guard l(mutex_); + VELOX_CHECK(!cancelled_, "Getting data after the build side is aborted"); + if (numRows_.has_value()) { + return numRows_; + } + promises_.emplace_back("CustomJoinBridge::numRowsOrFuture"); + *future = promises_.back().getSemiFuture(); + return std::nullopt; + } + + private: + std::optional numRows_; +}; + +class CustomJoinBuild : public Operator { + public: + CustomJoinBuild( + int32_t operatorId, + DriverCtx* driverCtx, + std::shared_ptr joinNode) + : Operator( + driverCtx, + nullptr, + operatorId, + joinNode->id(), + "CustomJoinBuild") {} + CustomJoinBuild( + int32_t operatorId, + DriverCtx* driverCtx, + const core::PlanNodeId& joinNodeid) + : Operator( + driverCtx, + nullptr, + operatorId, + joinNodeid, + "CustomJoinBuild") {} + + void addInput(RowVectorPtr input) override { + auto inputSize = input->size(); + if (inputSize > 0) { + numRows_ += inputSize; + } + } + + bool needsInput() const override { + return !noMoreInput_; + } + + RowVectorPtr getOutput() override { + return nullptr; + } + + void noMoreInput() override { + Operator::noMoreInput(); + std::vector promises; + std::vector> peers; + // The last Driver to hit CustomJoinBuild::finish gathers the data from + // all build Drivers and hands it over to the probe side. At this + // point all build Drivers are continued and will free their + // state. allPeersFinished is true only for the last Driver of the + // build pipeline. + if (!operatorCtx_->task()->allPeersFinished( + planNodeId(), operatorCtx_->driver(), &future_, promises, peers)) { + return; + } + + for (auto& peer : peers) { + auto op = peer->findOperator(planNodeId()); + auto* build = dynamic_cast(op); + VELOX_CHECK(build); + numRows_ += build->numRows_; + } + + // Realize the promises so that the other Drivers (which were not + // the last to finish) can continue from the barrier and finish. + peers.clear(); + for (auto& promise : promises) { + promise.setValue(); + } + + auto joinBridge = operatorCtx_->task()->getCustomJoinBridge( + operatorCtx_->driverCtx()->splitGroupId, planNodeId()); + auto customJoinBridge = + std::dynamic_pointer_cast(joinBridge); + + // checks + VELOX_CHECK_NOT_NULL( + customJoinBridge, + "Join bridge for plan node ID is of the wrong type: {}", + planNodeId()); + customJoinBridge->setNumRows(std::make_optional(numRows_)); + } + + BlockingReason isBlocked(ContinueFuture* future) override { + if (!future_.valid()) { + return BlockingReason::kNotBlocked; + } + *future = std::move(future_); + return BlockingReason::kWaitForJoinBuild; + } + + bool isFinished() override { + return !future_.valid() && noMoreInput_; + } + + private: + int32_t numRows_ = 0; + + ContinueFuture future_{ContinueFuture::makeEmpty()}; +}; + +class CustomJoinProbe : public Operator { + public: + CustomJoinProbe( + int32_t operatorId, + DriverCtx* driverCtx, + std::shared_ptr joinNode) + : Operator( + driverCtx, + nullptr, + operatorId, + joinNode->id(), + "CustomJoinProbe") {} + CustomJoinProbe( + int32_t operatorId, + DriverCtx* driverCtx, + const core::PlanNodeId& joinNodeid) + : Operator( + driverCtx, + nullptr, + operatorId, + joinNodeid, + "CustomJoinProbe") {} + + bool needsInput() const override { + return !finished_ && input_ == nullptr; + } + + void addInput(RowVectorPtr input) override { + input_ = std::move(input); + } + + RowVectorPtr getOutput() override { + if (!input_) { + return nullptr; + } + + const auto inputSize = input_->size(); + if (remainingLimit_ <= inputSize) { + finished_ = true; + } + + if (remainingLimit_ >= inputSize) { + remainingLimit_ -= inputSize; + auto output = input_; + input_.reset(); + return output; + } + + // Return nullptr if there is no data to return. + if (remainingLimit_ == 0) { + input_.reset(); + return nullptr; + } + + auto output = std::make_shared( + input_->pool(), + input_->type(), + input_->nulls(), + remainingLimit_, + input_->children()); + input_.reset(); + remainingLimit_ = 0; + return output; + } + + BlockingReason isBlocked(ContinueFuture* future) override { + if (numRows_.has_value()) { + return BlockingReason::kNotBlocked; + } + + auto joinBridge = operatorCtx_->task()->getCustomJoinBridge( + operatorCtx_->driverCtx()->splitGroupId, planNodeId()); + auto customJoinBridge = + std::dynamic_pointer_cast(joinBridge); + + // checks + VELOX_CHECK_NOT_NULL( + customJoinBridge, + "Join bridge for plan node ID is of the wrong type: {}", + planNodeId()); + auto numRows = customJoinBridge->numRowsOrFuture(future); + + if (!numRows.has_value()) { + return BlockingReason::kWaitForJoinBuild; + } + numRows_ = std::move(numRows); + remainingLimit_ = numRows_.value(); + + return BlockingReason::kNotBlocked; + } + + bool isFinished() override { + return finished_ || (noMoreInput_ && input_ == nullptr); + } + + private: + int32_t remainingLimit_; + std::optional numRows_; + + bool finished_{false}; +}; + +class CustomJoinBridgeTranslator : public Operator::PlanNodeTranslator { + std::unique_ptr + toOperator(DriverCtx* ctx, int32_t id, const core::PlanNodePtr& node) { + if (auto joinNode = + std::dynamic_pointer_cast(node)) { + return std::make_unique(id, ctx, joinNode); + } + return nullptr; + } + + std::unique_ptr toJoinBridge(const core::PlanNodePtr& node) { + if (auto joinNode = + std::dynamic_pointer_cast(node)) { + auto joinBridge = std::make_unique(); + return std::move(joinBridge); + } + return nullptr; + } + + OperatorSupplier toOperatorSupplier(const core::PlanNodePtr& node) { + if (auto joinNode = + std::dynamic_pointer_cast(node)) { + return [joinNode](int32_t operatorId, DriverCtx* ctx) { + return std::make_unique(operatorId, ctx, joinNode); + }; + } + return nullptr; + } +}; + +bool CustomDriverAdapter( + const exec::DriverFactory& driverFactory_, + exec::Driver& driver_) { + auto operators = driver_.operators(); + // Make sure operator states are initialized. We will need to inspect some of + // them during the transformation. + driver_.initializeOperators(); + auto ctx = driver_.driverCtx(); + // Replace HashBuild and HashProbe operators with CustomHashBuild and + // CustomHashProbe operators. + for (int32_t operatorIndex = 0; operatorIndex < operators.size(); + ++operatorIndex) { + std::vector> replace_op; + + facebook::velox::exec::Operator* oper = operators[operatorIndex]; + VELOX_CHECK(oper); + if (auto joinBuildOp = + dynamic_cast(oper)) { + auto planid = joinBuildOp->planNodeId(); + auto id = joinBuildOp->operatorId(); + replace_op.push_back(std::make_unique(id, ctx, planid)); + replace_op[0]->initialize(); + auto replaced = driverFactory_.replaceOperators( + driver_, operatorIndex, operatorIndex + 1, std::move(replace_op)); + } else if ( + auto joinProbeOp = + dynamic_cast(oper)) { + auto planid = joinProbeOp->planNodeId(); + auto id = joinProbeOp->operatorId(); + replace_op.push_back(std::make_unique(id, ctx, planid)); + replace_op[0]->initialize(); + auto replaced = driverFactory_.replaceOperators( + driver_, operatorIndex, operatorIndex + 1, std::move(replace_op)); + } + } + return true; +} + +void registerCustomDriver() { + exec::DriverAdapter custAdapter{"opRep", {}, CustomDriverAdapter}; + exec::DriverFactory::registerAdapter(custAdapter); +} + +void registerOperatorReplacement() { + // Registering Translator + exec::Operator::registerOperator( + std::make_unique()); + // Registering Custom Driver Adapter + registerCustomDriver(); +} +} // namespace + +/// This test will show the operator replacement with other operators using +/// driver adapter and usage of custom join bridge for replaced velox +/// join operators. It uses same custom operators from CustomJoinTest, +/// which will emit number of input rows. +class OperatorReplacementTest : public OperatorTestBase { + protected: + void SetUp() override { + OperatorTestBase::SetUp(); + registerOperatorReplacement(); + } + + RowVectorPtr makeSimpleRowVector(vector_size_t size) { + return makeRowVector( + {makeFlatVector(size, [](auto row) { return row; })}); + } + + void testOperatorReplacement( + int32_t numThreads, + const std::vector& leftBatch, + const std::vector& rightBatch, + const std::string& referenceQuery) { + createDuckDbTable("t", {leftBatch}); + + auto planNodeIdGenerator = std::make_shared(); + + CursorParameters params; + params.maxDrivers = numThreads; + params.planNode = PlanBuilder(planNodeIdGenerator) + .values({leftBatch}, false) + .hashJoin( + {"c0"}, + {"u1"}, + PlanBuilder(planNodeIdGenerator) + .values(rightBatch, false) + .project({"c0 AS u1"}) + .planNode(), + "", + {"c0"}) + .project({"c0"}) + .planNode(); + + OperatorTestBase::assertQuery(params, referenceQuery); + } +}; + +TEST_F(OperatorReplacementTest, basic) { + auto leftBatch = {makeSimpleRowVector(100)}; + auto rightBatch = {makeSimpleRowVector(10)}; + testOperatorReplacement( + 1, leftBatch, rightBatch, "SELECT c0 FROM t LIMIT 10"); +} From 74a31830765341f7e0e5214248cbc070d5a5b153 Mon Sep 17 00:00:00 2001 From: hitarth Date: Wed, 21 Aug 2024 11:29:49 -0700 Subject: [PATCH 03/24] Handle array of map and array of array case accurately in parquet reader (#9728) Summary: Handle array of map and array of array case accurately in parquet reader while parsing schema. This should fix https://github.com/facebookincubator/velox/issues/9238 Pull Request resolved: https://github.com/facebookincubator/velox/pull/9728 Reviewed By: Yuhta Differential Revision: D61550387 Pulled By: kgpai fbshipit-source-id: 989dc46785371d1f86d4e277cfaf5eeb1761f311 --- velox/dwio/parquet/reader/ParquetReader.cpp | 28 +++- ...rray_of_map_of_int_key_array_value.parquet | Bin 0 -> 43558 bytes ...ray_of_map_of_int_key_struct_value.parquet | Bin 0 -> 57932 bytes .../examples/struct_of_array_of_array.parquet | Bin 0 -> 86749 bytes .../tests/reader/ParquetReaderTest.cpp | 149 ++++++++++++++++++ 5 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 velox/dwio/parquet/tests/examples/array_of_map_of_int_key_array_value.parquet create mode 100644 velox/dwio/parquet/tests/examples/array_of_map_of_int_key_struct_value.parquet create mode 100644 velox/dwio/parquet/tests/examples/struct_of_array_of_array.parquet diff --git a/velox/dwio/parquet/reader/ParquetReader.cpp b/velox/dwio/parquet/reader/ParquetReader.cpp index 635366f19f136..9200886e8eb4d 100644 --- a/velox/dwio/parquet/reader/ParquetReader.cpp +++ b/velox/dwio/parquet/reader/ParquetReader.cpp @@ -300,6 +300,33 @@ std::unique_ptr ReaderBase::getParquetColumnInfo( if (schemaElement.__isset.converted_type) { switch (schemaElement.converted_type) { + case thrift::ConvertedType::LIST: { + VELOX_CHECK_EQ(children.size(), 1); + const auto& child = children[0]; + isRepeated = true; + // In case the child is a MAP or current element is repeated then + // wrap child around additional ARRAY + if (child->type()->kind() == TypeKind::MAP || + schemaElement.repetition_type == + thrift::FieldRepetitionType::REPEATED) { + return std::make_unique( + TypeFactory::create(child->type()), + std::move(children), + curSchemaIdx, + maxSchemaElementIdx, + ParquetTypeWithId::kNonLeaf, + std::move(name), + std::nullopt, + std::nullopt, + maxRepeat + 1, + maxDefine, + isOptional, + isRepeated); + } + // Only special case list of map and list of list is handled here, + // other generic case is handled with case MAP + [[fallthrough]]; + } case thrift::ConvertedType::MAP_KEY_VALUE: // If the MAP_KEY_VALUE annotated group's parent is a MAP, it should // be the repeated key_value group that directly contains the key and @@ -337,7 +364,6 @@ std::unique_ptr ReaderBase::getParquetColumnInfo( // a MAP-annotated group. [[fallthrough]]; - case thrift::ConvertedType::LIST: case thrift::ConvertedType::MAP: { VELOX_CHECK_EQ(children.size(), 1); const auto& child = children[0]; diff --git a/velox/dwio/parquet/tests/examples/array_of_map_of_int_key_array_value.parquet b/velox/dwio/parquet/tests/examples/array_of_map_of_int_key_array_value.parquet new file mode 100644 index 0000000000000000000000000000000000000000..f76b2a89da6186329a15eec86b87e51f5abfe92d GIT binary patch literal 43558 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%0D7B+_GWeA5tVa^n%JZ6Z~FCY+~VV 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Ö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 literal 0 HcmV?d00001 diff --git a/velox/dwio/parquet/tests/examples/struct_of_array_of_array.parquet b/velox/dwio/parquet/tests/examples/struct_of_array_of_array.parquet new file mode 100644 index 0000000000000000000000000000000000000000..2b161e77d773e2a6469a1e5fc22cbfe919793336 GIT binary patch literal 86749 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 literal 0 HcmV?d00001 diff --git a/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp b/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp index e46dfadfa1ef4..b3a5ea7ade89c 100644 --- a/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp +++ b/velox/dwio/parquet/tests/reader/ParquetReaderTest.cpp @@ -1235,6 +1235,155 @@ TEST_F(ParquetReaderTest, testV2PageWithZeroMaxDefRep) { outputRowType, *rowReader, expected, *leafPool_); } +TEST_F(ParquetReaderTest, arrayOfMapOfIntKeyArrayValue) { + // The Schema is of type + // message hive_schema { + // optional group test (LIST) { + // repeated group array (MAP) { + // repeated group key_value (MAP_KEY_VALUE) { + // required binary key (UTF8); + // optional group value (LIST) { + // repeated int32 array; + // } + // } + // } + // } + // } + const std::string expectedVeloxType = + "ROW>>>"; + const std::string sample( + getExampleFilePath("array_of_map_of_int_key_array_value.parquet")); + facebook::velox::dwio::common::ReaderOptions readerOptions{leafPool_.get()}; + auto reader = createReader(sample, readerOptions); + EXPECT_EQ(reader->rowType()->toString(), expectedVeloxType); + auto numRows = reader->numberOfRows(); + auto type = reader->typeWithId(); + RowReaderOptions rowReaderOpts; + auto rowType = ROW({"test"}, {ARRAY(MAP(VARCHAR(), ARRAY(INTEGER())))}); + rowReaderOpts.setScanSpec(makeScanSpec(rowType)); + auto rowReader = reader->createRowReader(rowReaderOpts); + auto result = BaseVector::create(rowType, 10, leafPool_.get()); + constexpr int kBatchSize = 1000; + while (rowReader->next(kBatchSize, result)) { + } +} + +TEST_F(ParquetReaderTest, arrayOfMapOfIntKeyStructValue) { + // The Schema is of type + // message hive_schema { + // optional group test (LIST) { + // repeated group array (MAP) { + // repeated group key_value (MAP_KEY_VALUE) { + // required int32 key; + // optional group value { + // optional binary stringfield (UTF8); + // optional int64 longfield; + // } + // } + // } + // } + // } + const std::string expectedVeloxType = + "ROW>>>"; + const std::string sample( + getExampleFilePath("array_of_map_of_int_key_struct_value.parquet")); + facebook::velox::dwio::common::ReaderOptions readerOptions{leafPool_.get()}; + auto reader = createReader(sample, readerOptions); + EXPECT_EQ(reader->rowType()->toString(), expectedVeloxType); + auto numRows = reader->numberOfRows(); + auto type = reader->typeWithId(); + RowReaderOptions rowReaderOpts; + auto rowType = reader->rowType(); + rowReaderOpts.setScanSpec(makeScanSpec(rowType)); + auto rowReader = reader->createRowReader(rowReaderOpts); + auto result = BaseVector::create(rowType, 10, leafPool_.get()); + constexpr int kBatchSize = 1000; + while (rowReader->next(kBatchSize, result)) { + } +} + +TEST_F(ParquetReaderTest, struct_of_array_of_array) { + // The Schema is of type + // message hive_schema { + // optional group test { + // optional group stringarrayfield (LIST) { + // repeated group array (LIST) { + // repeated binary array (UTF8); + // } + // } + // optional group intarrayfield (LIST) { + // repeated group array (LIST) { + // repeated int32 array; + // } + // } + // } + // } + const std::string expectedVeloxType = + "ROW>,intarrayfield:ARRAY>>>"; + const std::string sample( + getExampleFilePath("struct_of_array_of_array.parquet")); + facebook::velox::dwio::common::ReaderOptions readerOptions{leafPool_.get()}; + auto reader = createReader(sample, readerOptions); + auto numRows = reader->numberOfRows(); + auto type = reader->typeWithId(); + EXPECT_EQ(type->size(), 1ULL); + EXPECT_EQ(reader->rowType()->toString(), expectedVeloxType); + + auto test_column = type->childAt(0); + EXPECT_EQ(test_column->type()->kind(), TypeKind::ROW); + EXPECT_EQ(type->childByName("test"), test_column); + + // test_column has 2 children + EXPECT_EQ(test_column->size(), 2ULL); + // explore 1st child of test_column + auto stringarrayfield_column = test_column->childAt(0); + EXPECT_EQ(stringarrayfield_column->type()->kind(), TypeKind::ARRAY); + + // stringarrayfield_column column has 1 child + EXPECT_EQ(stringarrayfield_column->size(), 1ULL); + // explore 1st child of stringarrayfield_column + auto array_column = stringarrayfield_column->childAt(0); + EXPECT_EQ(array_column->type()->kind(), TypeKind::ARRAY); + + // array_column column has 1 child + EXPECT_EQ(array_column->size(), 1ULL); + // explore 1st child of array_column + auto array_leaf_column = array_column->childAt(0); + EXPECT_EQ(array_leaf_column->type()->kind(), TypeKind::VARCHAR); + + // explore 2nd child of test_column + auto intarrayfield_column = test_column->childAt(1); + EXPECT_EQ(intarrayfield_column->type()->kind(), TypeKind::ARRAY); + EXPECT_EQ(test_column->childByName("intarrayfield"), intarrayfield_column); + + // intarrayfield_column column has 1 child + EXPECT_EQ(intarrayfield_column->size(), 1ULL); + // explore 1st child of intarrayfield_column + auto array_column_for_intarrayfield = intarrayfield_column->childAt(0); + EXPECT_EQ(array_column_for_intarrayfield->type()->kind(), TypeKind::ARRAY); + + // array_column_for_intarrayfield column has 1 child + EXPECT_EQ(array_column_for_intarrayfield->size(), 1ULL); + // explore 1st child + auto array_leaf_column_for_intarrayfield = + array_column_for_intarrayfield->childAt(0); + EXPECT_EQ( + array_leaf_column_for_intarrayfield->type()->kind(), TypeKind::INTEGER); + + RowReaderOptions rowReaderOpts; + auto rowType = + ROW({"test"}, + {ROW( + {"stringarrayfield", "intarrayfield"}, + {ARRAY(ARRAY(VARCHAR())), ARRAY(ARRAY(INTEGER()))})}); + rowReaderOpts.setScanSpec(makeScanSpec(rowType)); + auto rowReader = reader->createRowReader(rowReaderOpts); + auto result = BaseVector::create(rowType, 10, leafPool_.get()); + constexpr int kBatchSize = 1000; + while (rowReader->next(kBatchSize, result)) { + } +} + TEST_F(ParquetReaderTest, testLzoDataPage) { const std::string sample(getExampleFilePath("lzo.parquet")); From f369a2653d5bfd3460afcf0243bda2d98a72489b Mon Sep 17 00:00:00 2001 From: Chengcheng Jin Date: Wed, 21 Aug 2024 11:32:01 -0700 Subject: [PATCH 04/24] Fix spill time metric unit (#10783) Summary: Change the spill time metric from microseconds to nanosecond. This is a follow up of https://github.com/facebookincubator/velox/pull/9765/ Resolves: https://github.com/facebookincubator/velox/issues/10688 Pull Request resolved: https://github.com/facebookincubator/velox/pull/10783 Reviewed By: bikramSingh91 Differential Revision: D61582818 Pulled By: xiaoxmeng fbshipit-source-id: 59a7d0033a3ce1790c90498306c599a4f17f55a4 --- velox/common/base/SpillStats.cpp | 180 +++++++++--------- velox/common/base/SpillStats.h | 30 +-- velox/common/base/tests/SpillStatsTest.cpp | 76 ++++---- velox/common/time/Timer.h | 18 ++ .../hive/tests/HiveDataSinkTest.cpp | 12 +- velox/exec/HashProbe.cpp | 2 +- velox/exec/Operator.cpp | 51 ++--- velox/exec/SpillFile.cpp | 47 ++--- velox/exec/Spiller.cpp | 30 +-- velox/exec/tests/SpillTest.cpp | 56 +++--- velox/exec/tests/SpillerTest.cpp | 100 +++++----- 11 files changed, 303 insertions(+), 299 deletions(-) diff --git a/velox/common/base/SpillStats.cpp b/velox/common/base/SpillStats.cpp index 4056f5c411630..a6a3dae5f9d2c 100644 --- a/velox/common/base/SpillStats.cpp +++ b/velox/common/base/SpillStats.cpp @@ -42,34 +42,34 @@ SpillStats::SpillStats( uint64_t _spilledRows, uint32_t _spilledPartitions, uint64_t _spilledFiles, - uint64_t _spillFillTimeUs, - uint64_t _spillSortTimeUs, - uint64_t _spillSerializationTimeUs, + uint64_t _spillFillTimeNanos, + uint64_t _spillSortTimeNanos, + uint64_t _spillSerializationTimeNanos, uint64_t _spillWrites, - uint64_t _spillFlushTimeUs, - uint64_t _spillWriteTimeUs, + uint64_t _spillFlushTimeNanos, + uint64_t _spillWriteTimeNanos, uint64_t _spillMaxLevelExceededCount, uint64_t _spillReadBytes, uint64_t _spillReads, - uint64_t _spillReadTimeUs, - uint64_t _spillDeserializationTimeUs) + uint64_t _spillReadTimeNanos, + uint64_t _spillDeserializationTimeNanos) : spillRuns(_spillRuns), spilledInputBytes(_spilledInputBytes), spilledBytes(_spilledBytes), spilledRows(_spilledRows), spilledPartitions(_spilledPartitions), spilledFiles(_spilledFiles), - spillFillTimeUs(_spillFillTimeUs), - spillSortTimeUs(_spillSortTimeUs), - spillSerializationTimeUs(_spillSerializationTimeUs), + spillFillTimeNanos(_spillFillTimeNanos), + spillSortTimeNanos(_spillSortTimeNanos), + spillSerializationTimeNanos(_spillSerializationTimeNanos), spillWrites(_spillWrites), - spillFlushTimeUs(_spillFlushTimeUs), - spillWriteTimeUs(_spillWriteTimeUs), + spillFlushTimeNanos(_spillFlushTimeNanos), + spillWriteTimeNanos(_spillWriteTimeNanos), spillMaxLevelExceededCount(_spillMaxLevelExceededCount), spillReadBytes(_spillReadBytes), spillReads(_spillReads), - spillReadTimeUs(_spillReadTimeUs), - spillDeserializationTimeUs(_spillDeserializationTimeUs) {} + spillReadTimeNanos(_spillReadTimeNanos), + spillDeserializationTimeNanos(_spillDeserializationTimeNanos) {} SpillStats& SpillStats::operator+=(const SpillStats& other) { spillRuns += other.spillRuns; @@ -78,17 +78,17 @@ SpillStats& SpillStats::operator+=(const SpillStats& other) { spilledRows += other.spilledRows; spilledPartitions += other.spilledPartitions; spilledFiles += other.spilledFiles; - spillFillTimeUs += other.spillFillTimeUs; - spillSortTimeUs += other.spillSortTimeUs; - spillSerializationTimeUs += other.spillSerializationTimeUs; + spillFillTimeNanos += other.spillFillTimeNanos; + spillSortTimeNanos += other.spillSortTimeNanos; + spillSerializationTimeNanos += other.spillSerializationTimeNanos; spillWrites += other.spillWrites; - spillFlushTimeUs += other.spillFlushTimeUs; - spillWriteTimeUs += other.spillWriteTimeUs; + spillFlushTimeNanos += other.spillFlushTimeNanos; + spillWriteTimeNanos += other.spillWriteTimeNanos; spillMaxLevelExceededCount += other.spillMaxLevelExceededCount; spillReadBytes += other.spillReadBytes; spillReads += other.spillReads; - spillReadTimeUs += other.spillReadTimeUs; - spillDeserializationTimeUs += other.spillDeserializationTimeUs; + spillReadTimeNanos += other.spillReadTimeNanos; + spillDeserializationTimeNanos += other.spillDeserializationTimeNanos; return *this; } @@ -100,20 +100,20 @@ SpillStats SpillStats::operator-(const SpillStats& other) const { result.spilledRows = spilledRows - other.spilledRows; result.spilledPartitions = spilledPartitions - other.spilledPartitions; result.spilledFiles = spilledFiles - other.spilledFiles; - result.spillFillTimeUs = spillFillTimeUs - other.spillFillTimeUs; - result.spillSortTimeUs = spillSortTimeUs - other.spillSortTimeUs; - result.spillSerializationTimeUs = - spillSerializationTimeUs - other.spillSerializationTimeUs; + result.spillFillTimeNanos = spillFillTimeNanos - other.spillFillTimeNanos; + result.spillSortTimeNanos = spillSortTimeNanos - other.spillSortTimeNanos; + result.spillSerializationTimeNanos = + spillSerializationTimeNanos - other.spillSerializationTimeNanos; result.spillWrites = spillWrites - other.spillWrites; - result.spillFlushTimeUs = spillFlushTimeUs - other.spillFlushTimeUs; - result.spillWriteTimeUs = spillWriteTimeUs - other.spillWriteTimeUs; + result.spillFlushTimeNanos = spillFlushTimeNanos - other.spillFlushTimeNanos; + result.spillWriteTimeNanos = spillWriteTimeNanos - other.spillWriteTimeNanos; result.spillMaxLevelExceededCount = spillMaxLevelExceededCount - other.spillMaxLevelExceededCount; result.spillReadBytes = spillReadBytes - other.spillReadBytes; result.spillReads = spillReads - other.spillReads; - result.spillReadTimeUs = spillReadTimeUs - other.spillReadTimeUs; - result.spillDeserializationTimeUs = - spillDeserializationTimeUs - other.spillDeserializationTimeUs; + result.spillReadTimeNanos = spillReadTimeNanos - other.spillReadTimeNanos; + result.spillDeserializationTimeNanos = + spillDeserializationTimeNanos - other.spillDeserializationTimeNanos; return result; } @@ -135,17 +135,17 @@ bool SpillStats::operator<(const SpillStats& other) const { UPDATE_COUNTER(spilledRows); UPDATE_COUNTER(spilledPartitions); UPDATE_COUNTER(spilledFiles); - UPDATE_COUNTER(spillFillTimeUs); - UPDATE_COUNTER(spillSortTimeUs); - UPDATE_COUNTER(spillSerializationTimeUs); + UPDATE_COUNTER(spillFillTimeNanos); + UPDATE_COUNTER(spillSortTimeNanos); + UPDATE_COUNTER(spillSerializationTimeNanos); UPDATE_COUNTER(spillWrites); - UPDATE_COUNTER(spillFlushTimeUs); - UPDATE_COUNTER(spillWriteTimeUs); + UPDATE_COUNTER(spillFlushTimeNanos); + UPDATE_COUNTER(spillWriteTimeNanos); UPDATE_COUNTER(spillMaxLevelExceededCount); UPDATE_COUNTER(spillReadBytes); UPDATE_COUNTER(spillReads); - UPDATE_COUNTER(spillReadTimeUs); - UPDATE_COUNTER(spillDeserializationTimeUs); + UPDATE_COUNTER(spillReadTimeNanos); + UPDATE_COUNTER(spillDeserializationTimeNanos); #undef UPDATE_COUNTER VELOX_CHECK( !((gtCount > 0) && (ltCount > 0)), @@ -175,17 +175,17 @@ bool SpillStats::operator==(const SpillStats& other) const { spilledRows, spilledPartitions, spilledFiles, - spillFillTimeUs, - spillSortTimeUs, - spillSerializationTimeUs, + spillFillTimeNanos, + spillSortTimeNanos, + spillSerializationTimeNanos, spillWrites, - spillFlushTimeUs, - spillWriteTimeUs, + spillFlushTimeNanos, + spillWriteTimeNanos, spillMaxLevelExceededCount, spillReadBytes, spillReads, - spillReadTimeUs, - spillDeserializationTimeUs) == + spillReadTimeNanos, + spillDeserializationTimeNanos) == std::tie( other.spillRuns, other.spilledInputBytes, @@ -193,17 +193,17 @@ bool SpillStats::operator==(const SpillStats& other) const { other.spilledRows, other.spilledPartitions, other.spilledFiles, - other.spillFillTimeUs, - other.spillSortTimeUs, - other.spillSerializationTimeUs, + other.spillFillTimeNanos, + other.spillSortTimeNanos, + other.spillSerializationTimeNanos, other.spillWrites, - other.spillFlushTimeUs, - other.spillWriteTimeUs, + other.spillFlushTimeNanos, + other.spillWriteTimeNanos, spillMaxLevelExceededCount, spillReadBytes, spillReads, - spillReadTimeUs, - spillDeserializationTimeUs); + spillReadTimeNanos, + spillDeserializationTimeNanos); } void SpillStats::reset() { @@ -213,44 +213,44 @@ void SpillStats::reset() { spilledRows = 0; spilledPartitions = 0; spilledFiles = 0; - spillFillTimeUs = 0; - spillSortTimeUs = 0; - spillSerializationTimeUs = 0; + spillFillTimeNanos = 0; + spillSortTimeNanos = 0; + spillSerializationTimeNanos = 0; spillWrites = 0; - spillFlushTimeUs = 0; - spillWriteTimeUs = 0; + spillFlushTimeNanos = 0; + spillWriteTimeNanos = 0; spillMaxLevelExceededCount = 0; spillReadBytes = 0; spillReads = 0; - spillReadTimeUs = 0; - spillDeserializationTimeUs = 0; + spillReadTimeNanos = 0; + spillDeserializationTimeNanos = 0; } std::string SpillStats::toString() const { return fmt::format( "spillRuns[{}] spilledInputBytes[{}] spilledBytes[{}] spilledRows[{}] " - "spilledPartitions[{}] spilledFiles[{}] spillFillTimeUs[{}] " - "spillSortTime[{}] spillSerializationTime[{}] spillWrites[{}] " - "spillFlushTime[{}] spillWriteTime[{}] maxSpillExceededLimitCount[{}] " - "spillReadBytes[{}] spillReads[{}] spillReadTime[{}] " - "spillReadDeserializationTime[{}]", + "spilledPartitions[{}] spilledFiles[{}] spillFillTimeNanos[{}] " + "spillSortTimeNanos[{}] spillSerializationTimeNanos[{}] spillWrites[{}] " + "spillFlushTimeNanos[{}] spillWriteTimeNanos[{}] maxSpillExceededLimitCount[{}] " + "spillReadBytes[{}] spillReads[{}] spillReadTimeNanos[{}] " + "spillReadDeserializationTimeNanos[{}]", spillRuns, succinctBytes(spilledInputBytes), succinctBytes(spilledBytes), spilledRows, spilledPartitions, spilledFiles, - succinctMicros(spillFillTimeUs), - succinctMicros(spillSortTimeUs), - succinctMicros(spillSerializationTimeUs), + succinctNanos(spillFillTimeNanos), + succinctNanos(spillSortTimeNanos), + succinctNanos(spillSerializationTimeNanos), spillWrites, - succinctMicros(spillFlushTimeUs), - succinctMicros(spillWriteTimeUs), + succinctNanos(spillFlushTimeNanos), + succinctNanos(spillWriteTimeNanos), spillMaxLevelExceededCount, succinctBytes(spillReadBytes), spillReads, - succinctMicros(spillReadTimeUs), - succinctMicros(spillDeserializationTimeUs)); + succinctNanos(spillReadTimeNanos), + succinctNanos(spillDeserializationTimeNanos)); } void updateGlobalSpillRunStats(uint64_t numRuns) { @@ -260,52 +260,54 @@ void updateGlobalSpillRunStats(uint64_t numRuns) { void updateGlobalSpillAppendStats( uint64_t numRows, - uint64_t serializationTimeUs) { + uint64_t serializationTimeNs) { RECORD_METRIC_VALUE(kMetricSpilledRowsCount, numRows); RECORD_HISTOGRAM_METRIC_VALUE( - kMetricSpillSerializationTimeMs, serializationTimeUs / 1'000); + kMetricSpillSerializationTimeMs, serializationTimeNs / 1'000'000); auto statsLocked = localSpillStats().wlock(); statsLocked->spilledRows += numRows; - statsLocked->spillSerializationTimeUs += serializationTimeUs; + statsLocked->spillSerializationTimeNanos += serializationTimeNs; } void incrementGlobalSpilledPartitionStats() { ++localSpillStats().wlock()->spilledPartitions; } -void updateGlobalSpillFillTime(uint64_t timeUs) { - RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillFillTimeMs, timeUs / 1'000); - localSpillStats().wlock()->spillFillTimeUs += timeUs; +void updateGlobalSpillFillTime(uint64_t timeNs) { + RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillFillTimeMs, timeNs / 1'000'000); + localSpillStats().wlock()->spillFillTimeNanos += timeNs; } -void updateGlobalSpillSortTime(uint64_t timeUs) { - RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillSortTimeMs, timeUs / 1'000); - localSpillStats().wlock()->spillSortTimeUs += timeUs; +void updateGlobalSpillSortTime(uint64_t timeNs) { + RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillSortTimeMs, timeNs / 1'000'000); + localSpillStats().wlock()->spillSortTimeNanos += timeNs; } void updateGlobalSpillWriteStats( uint64_t spilledBytes, - uint64_t flushTimeUs, - uint64_t writeTimeUs) { + uint64_t flushTimeNs, + uint64_t writeTimeNs) { RECORD_METRIC_VALUE(kMetricSpillWritesCount); RECORD_METRIC_VALUE(kMetricSpilledBytes, spilledBytes); - RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillFlushTimeMs, flushTimeUs / 1'000); - RECORD_HISTOGRAM_METRIC_VALUE(kMetricSpillWriteTimeMs, writeTimeUs / 1'000); + RECORD_HISTOGRAM_METRIC_VALUE( + kMetricSpillFlushTimeMs, flushTimeNs / 1'000'000); + RECORD_HISTOGRAM_METRIC_VALUE( + kMetricSpillWriteTimeMs, writeTimeNs / 1'000'000); auto statsLocked = localSpillStats().wlock(); ++statsLocked->spillWrites; statsLocked->spilledBytes += spilledBytes; - statsLocked->spillFlushTimeUs += flushTimeUs; - statsLocked->spillWriteTimeUs += writeTimeUs; + statsLocked->spillFlushTimeNanos += flushTimeNs; + statsLocked->spillWriteTimeNanos += writeTimeNs; } void updateGlobalSpillReadStats( uint64_t spillReads, uint64_t spillReadBytes, - uint64_t spillRadTimeUs) { + uint64_t spillReadTimeNs) { auto statsLocked = localSpillStats().wlock(); statsLocked->spillReads += spillReads; statsLocked->spillReadBytes += spillReadBytes; - statsLocked->spillReadTimeUs += spillRadTimeUs; + statsLocked->spillReadTimeNanos += spillReadTimeNs; } void updateGlobalSpillMemoryBytes(uint64_t spilledInputBytes) { @@ -325,8 +327,8 @@ void updateGlobalMaxSpillLevelExceededCount( maxSpillLevelExceededCount; } -void updateGlobalSpillDeserializationTimeUs(uint64_t timeUs) { - localSpillStats().wlock()->spillDeserializationTimeUs += timeUs; +void updateGlobalSpillDeserializationTimeNs(uint64_t timeNs) { + localSpillStats().wlock()->spillDeserializationTimeNanos += timeNs; } SpillStats globalSpillStats() { diff --git a/velox/common/base/SpillStats.h b/velox/common/base/SpillStats.h index 8b9ad7d11cac8..c6bbe735b513e 100644 --- a/velox/common/base/SpillStats.h +++ b/velox/common/base/SpillStats.h @@ -39,19 +39,19 @@ struct SpillStats { /// The number of spilled files. uint64_t spilledFiles{0}; /// The time spent on filling rows for spilling. - uint64_t spillFillTimeUs{0}; + uint64_t spillFillTimeNanos{0}; /// The time spent on sorting rows for spilling. - uint64_t spillSortTimeUs{0}; + uint64_t spillSortTimeNanos{0}; /// The time spent on serializing rows for spilling. - uint64_t spillSerializationTimeUs{0}; + uint64_t spillSerializationTimeNanos{0}; /// The number of spill writer flushes, equivalent to number of write calls to /// underlying filesystem. uint64_t spillWrites{0}; /// The time spent on copy out serialized rows for disk write. If compression /// is enabled, this includes the compression time. - uint64_t spillFlushTimeUs{0}; + uint64_t spillFlushTimeNanos{0}; /// The time spent on writing spilled rows to disk. - uint64_t spillWriteTimeUs{0}; + uint64_t spillWriteTimeNanos{0}; /// The number of times that an hash build operator exceeds the max spill /// limit. uint64_t spillMaxLevelExceededCount{0}; @@ -61,9 +61,9 @@ struct SpillStats { /// to the underlying filesystem. uint64_t spillReads{0}; /// The time spent on read data from spilled files. - uint64_t spillReadTimeUs{0}; + uint64_t spillReadTimeNanos{0}; /// The time spent on deserializing rows read from spilled files. - uint64_t spillDeserializationTimeUs{0}; + uint64_t spillDeserializationTimeNanos{0}; SpillStats( uint64_t _spillRuns, @@ -72,17 +72,17 @@ struct SpillStats { uint64_t _spilledRows, uint32_t _spilledPartitions, uint64_t _spilledFiles, - uint64_t _spillFillTimeUs, - uint64_t _spillSortTimeUs, - uint64_t _spillSerializationTimeUs, + uint64_t _spillFillTimeNanos, + uint64_t _spillSortTimeNanos, + uint64_t _spillSerializationTimeNanos, uint64_t _spillWrites, - uint64_t _spillFlushTimeUs, - uint64_t _spillWriteTimeUs, + uint64_t _spillFlushTimeNanos, + uint64_t _spillWriteTimeNanos, uint64_t _spillMaxLevelExceededCount, uint64_t _spillReadBytes, uint64_t _spillReads, - uint64_t _spillReadTimeUs, - uint64_t _spillDeserializationTimeUs); + uint64_t _spillReadTimeNanos, + uint64_t _spillDeserializationTimeNanos); SpillStats() = default; @@ -157,7 +157,7 @@ void updateGlobalMaxSpillLevelExceededCount( uint64_t maxSpillLevelExceededCount); /// Increments the spill read deserialization time. -void updateGlobalSpillDeserializationTimeUs(uint64_t timeUs); +void updateGlobalSpillDeserializationTimeNs(uint64_t timeUs); /// Gets the cumulative global spill stats. SpillStats globalSpillStats(); diff --git a/velox/common/base/tests/SpillStatsTest.cpp b/velox/common/base/tests/SpillStatsTest.cpp index 274774298877f..114c728fe4f02 100644 --- a/velox/common/base/tests/SpillStatsTest.cpp +++ b/velox/common/base/tests/SpillStatsTest.cpp @@ -29,18 +29,18 @@ TEST(SpillStatsTest, spillStats) { stats1.spilledBytes = 1024; stats1.spilledPartitions = 1024; stats1.spilledFiles = 1023; - stats1.spillWriteTimeUs = 1023; - stats1.spillFlushTimeUs = 1023; + stats1.spillWriteTimeNanos = 1023; + stats1.spillFlushTimeNanos = 1023; stats1.spillWrites = 1023; - stats1.spillSortTimeUs = 1023; - stats1.spillFillTimeUs = 1023; + stats1.spillSortTimeNanos = 1023; + stats1.spillFillTimeNanos = 1023; stats1.spilledRows = 1023; - stats1.spillSerializationTimeUs = 1023; + stats1.spillSerializationTimeNanos = 1023; stats1.spillMaxLevelExceededCount = 3; stats1.spillReadBytes = 1024; stats1.spillReads = 10; - stats1.spillReadTimeUs = 100; - stats1.spillDeserializationTimeUs = 100; + stats1.spillReadTimeNanos = 100; + stats1.spillDeserializationTimeNanos = 100; ASSERT_FALSE(stats1.empty()); SpillStats stats2; stats2.spillRuns = 100; @@ -48,18 +48,18 @@ TEST(SpillStatsTest, spillStats) { stats2.spilledBytes = 1024; stats2.spilledPartitions = 1025; stats2.spilledFiles = 1026; - stats2.spillWriteTimeUs = 1026; - stats2.spillFlushTimeUs = 1027; + stats2.spillWriteTimeNanos = 1026; + stats2.spillFlushTimeNanos = 1027; stats2.spillWrites = 1028; - stats2.spillSortTimeUs = 1029; - stats2.spillFillTimeUs = 1030; + stats2.spillSortTimeNanos = 1029; + stats2.spillFillTimeNanos = 1030; stats2.spilledRows = 1031; - stats2.spillSerializationTimeUs = 1032; + stats2.spillSerializationTimeNanos = 1032; stats2.spillMaxLevelExceededCount = 4; stats2.spillReadBytes = 2048; stats2.spillReads = 10; - stats2.spillReadTimeUs = 100; - stats2.spillDeserializationTimeUs = 100; + stats2.spillReadTimeNanos = 100; + stats2.spillDeserializationTimeNanos = 100; ASSERT_TRUE(stats1 < stats2); ASSERT_TRUE(stats1 <= stats2); ASSERT_FALSE(stats1 > stats2); @@ -79,34 +79,34 @@ TEST(SpillStatsTest, spillStats) { ASSERT_EQ(delta.spilledBytes, 0); ASSERT_EQ(delta.spilledPartitions, 1); ASSERT_EQ(delta.spilledFiles, 3); - ASSERT_EQ(delta.spillWriteTimeUs, 3); - ASSERT_EQ(delta.spillFlushTimeUs, 4); + ASSERT_EQ(delta.spillWriteTimeNanos, 3); + ASSERT_EQ(delta.spillFlushTimeNanos, 4); ASSERT_EQ(delta.spillWrites, 5); - ASSERT_EQ(delta.spillSortTimeUs, 6); - ASSERT_EQ(delta.spillFillTimeUs, 7); + ASSERT_EQ(delta.spillSortTimeNanos, 6); + ASSERT_EQ(delta.spillFillTimeNanos, 7); ASSERT_EQ(delta.spilledRows, 8); - ASSERT_EQ(delta.spillSerializationTimeUs, 9); + ASSERT_EQ(delta.spillSerializationTimeNanos, 9); ASSERT_EQ(delta.spillReadBytes, 1024); ASSERT_EQ(delta.spillReads, 0); - ASSERT_EQ(delta.spillReadTimeUs, 0); - ASSERT_EQ(delta.spillDeserializationTimeUs, 0); + ASSERT_EQ(delta.spillReadTimeNanos, 0); + ASSERT_EQ(delta.spillDeserializationTimeNanos, 0); delta = stats1 - stats2; ASSERT_EQ(delta.spilledInputBytes, 0); ASSERT_EQ(delta.spilledBytes, 0); ASSERT_EQ(delta.spilledPartitions, -1); ASSERT_EQ(delta.spilledFiles, -3); - ASSERT_EQ(delta.spillWriteTimeUs, -3); - ASSERT_EQ(delta.spillFlushTimeUs, -4); + ASSERT_EQ(delta.spillWriteTimeNanos, -3); + ASSERT_EQ(delta.spillFlushTimeNanos, -4); ASSERT_EQ(delta.spillWrites, -5); - ASSERT_EQ(delta.spillSortTimeUs, -6); - ASSERT_EQ(delta.spillFillTimeUs, -7); + ASSERT_EQ(delta.spillSortTimeNanos, -6); + ASSERT_EQ(delta.spillFillTimeNanos, -7); ASSERT_EQ(delta.spilledRows, -8); - ASSERT_EQ(delta.spillSerializationTimeUs, -9); + ASSERT_EQ(delta.spillSerializationTimeNanos, -9); ASSERT_EQ(delta.spillMaxLevelExceededCount, -1); ASSERT_EQ(delta.spillReadBytes, -1024); ASSERT_EQ(delta.spillReads, 0); - ASSERT_EQ(delta.spillReadTimeUs, 0); - ASSERT_EQ(delta.spillDeserializationTimeUs, 0); + ASSERT_EQ(delta.spillReadTimeNanos, 0); + ASSERT_EQ(delta.spillDeserializationTimeNanos, 0); stats1.spilledInputBytes = 2060; stats1.spilledBytes = 1030; stats1.spillReadBytes = 4096; @@ -123,19 +123,19 @@ TEST(SpillStatsTest, spillStats) { stats2.toString(), "spillRuns[100] spilledInputBytes[2.00KB] spilledBytes[1.00KB] " "spilledRows[1031] spilledPartitions[1025] spilledFiles[1026] " - "spillFillTimeUs[1.03ms] spillSortTime[1.03ms] " - "spillSerializationTime[1.03ms] spillWrites[1028] spillFlushTime[1.03ms] " - "spillWriteTime[1.03ms] maxSpillExceededLimitCount[4] " - "spillReadBytes[2.00KB] spillReads[10] spillReadTime[100us] " - "spillReadDeserializationTime[100us]"); + "spillFillTimeNanos[1.03us] spillSortTimeNanos[1.03us] " + "spillSerializationTimeNanos[1.03us] spillWrites[1028] spillFlushTimeNanos[1.03us] " + "spillWriteTimeNanos[1.03us] maxSpillExceededLimitCount[4] " + "spillReadBytes[2.00KB] spillReads[10] spillReadTimeNanos[100ns] " + "spillReadDeserializationTimeNanos[100ns]"); ASSERT_EQ( fmt::format("{}", stats2), "spillRuns[100] spilledInputBytes[2.00KB] spilledBytes[1.00KB] " "spilledRows[1031] spilledPartitions[1025] spilledFiles[1026] " - "spillFillTimeUs[1.03ms] spillSortTime[1.03ms] " - "spillSerializationTime[1.03ms] spillWrites[1028] " - "spillFlushTime[1.03ms] spillWriteTime[1.03ms] " + "spillFillTimeNanos[1.03us] spillSortTimeNanos[1.03us] " + "spillSerializationTimeNanos[1.03us] spillWrites[1028] " + "spillFlushTimeNanos[1.03us] spillWriteTimeNanos[1.03us] " "maxSpillExceededLimitCount[4] " - "spillReadBytes[2.00KB] spillReads[10] spillReadTime[100us] " - "spillReadDeserializationTime[100us]"); + "spillReadBytes[2.00KB] spillReads[10] spillReadTimeNanos[100ns] " + "spillReadDeserializationTimeNanos[100ns]"); } diff --git a/velox/common/time/Timer.h b/velox/common/time/Timer.h index e668d188b1a1a..ce3d8ceb03860 100644 --- a/velox/common/time/Timer.h +++ b/velox/common/time/Timer.h @@ -44,6 +44,24 @@ class MicrosecondTimer { uint64_t* timer_; }; +class NanosecondTimer { + public: + explicit NanosecondTimer(uint64_t* timer) : timer_(timer) { + start_ = std::chrono::steady_clock::now(); + } + + ~NanosecondTimer() { + auto duration = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_); + + (*timer_) += duration.count(); + } + + private: + std::chrono::steady_clock::time_point start_; + uint64_t* timer_; +}; + /// Measures the time between construction and destruction with CPU clock /// counter (rdtsc on X86) and increments a user-supplied counter with the cycle /// count. diff --git a/velox/connectors/hive/tests/HiveDataSinkTest.cpp b/velox/connectors/hive/tests/HiveDataSinkTest.cpp index c8d959b2f5c9c..96f85b02b9402 100644 --- a/velox/connectors/hive/tests/HiveDataSinkTest.cpp +++ b/velox/connectors/hive/tests/HiveDataSinkTest.cpp @@ -492,10 +492,10 @@ TEST_F(HiveDataSinkTest, basic) { stats.toString(), "numWrittenBytes 0B numWrittenFiles 0 spillRuns[0] spilledInputBytes[0B] " "spilledBytes[0B] spilledRows[0] spilledPartitions[0] spilledFiles[0] " - "spillFillTimeUs[0us] spillSortTime[0us] spillSerializationTime[0us] " - "spillWrites[0] spillFlushTime[0us] spillWriteTime[0us] " + "spillFillTimeNanos[0ns] spillSortTimeNanos[0ns] spillSerializationTimeNanos[0ns] " + "spillWrites[0] spillFlushTimeNanos[0ns] spillWriteTimeNanos[0ns] " "maxSpillExceededLimitCount[0] spillReadBytes[0B] spillReads[0] " - "spillReadTime[0us] spillReadDeserializationTime[0us]"); + "spillReadTimeNanos[0ns] spillReadDeserializationTimeNanos[0ns]"); const int numBatches = 10; const auto vectors = createVectors(500, numBatches); @@ -540,10 +540,10 @@ TEST_F(HiveDataSinkTest, basicBucket) { stats.toString(), "numWrittenBytes 0B numWrittenFiles 0 spillRuns[0] spilledInputBytes[0B] " "spilledBytes[0B] spilledRows[0] spilledPartitions[0] spilledFiles[0] " - "spillFillTimeUs[0us] spillSortTime[0us] spillSerializationTime[0us] " - "spillWrites[0] spillFlushTime[0us] spillWriteTime[0us] " + "spillFillTimeNanos[0ns] spillSortTimeNanos[0ns] spillSerializationTimeNanos[0ns] " + "spillWrites[0] spillFlushTimeNanos[0ns] spillWriteTimeNanos[0ns] " "maxSpillExceededLimitCount[0] spillReadBytes[0B] spillReads[0] " - "spillReadTime[0us] spillReadDeserializationTime[0us]"); + "spillReadTimeNanos[0ns] spillReadDeserializationTimeNanos[0ns]"); const int numBatches = 10; const auto vectors = createVectors(500, numBatches); diff --git a/velox/exec/HashProbe.cpp b/velox/exec/HashProbe.cpp index f9df0370635d5..95f060b789148 100644 --- a/velox/exec/HashProbe.cpp +++ b/velox/exec/HashProbe.cpp @@ -1512,7 +1512,7 @@ void HashProbe::noMoreInputInternal() { spillInputPartitionIds_.size(), inputSpiller_->spilledPartitionSet().size()); inputSpiller_->finishSpill(spillPartitionSet_); - VELOX_CHECK_EQ(spillStats_.rlock()->spillSortTimeUs, 0); + VELOX_CHECK_EQ(spillStats_.rlock()->spillSortTimeNanos, 0); } const bool hasSpillEnabled = spillEnabled(); diff --git a/velox/exec/Operator.cpp b/velox/exec/Operator.cpp index 9f46464cf2b54..0eacee7ca4a89 100644 --- a/velox/exec/Operator.cpp +++ b/velox/exec/Operator.cpp @@ -300,55 +300,40 @@ void Operator::recordSpillStats() { lockedStats->spilledRows += lockedSpillStats->spilledRows; lockedStats->spilledPartitions += lockedSpillStats->spilledPartitions; lockedStats->spilledFiles += lockedSpillStats->spilledFiles; - if (lockedSpillStats->spillFillTimeUs != 0) { + if (lockedSpillStats->spillFillTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillFillTime, RuntimeCounter{ - static_cast( - lockedSpillStats->spillFillTimeUs * - Timestamp::kNanosecondsInMicrosecond), - RuntimeCounter::Unit::kNanos}); + static_cast(lockedSpillStats->spillFillTimeNanos)}); } - if (lockedSpillStats->spillSortTimeUs != 0) { + if (lockedSpillStats->spillSortTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillSortTime, RuntimeCounter{ - static_cast( - lockedSpillStats->spillSortTimeUs * - Timestamp::kNanosecondsInMicrosecond), - RuntimeCounter::Unit::kNanos}); + static_cast(lockedSpillStats->spillSortTimeNanos)}); } - if (lockedSpillStats->spillSerializationTimeUs != 0) { + if (lockedSpillStats->spillSerializationTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillSerializationTime, - RuntimeCounter{ - static_cast( - lockedSpillStats->spillSerializationTimeUs * - Timestamp::kNanosecondsInMicrosecond), - RuntimeCounter::Unit::kNanos}); + RuntimeCounter{static_cast( + lockedSpillStats->spillSerializationTimeNanos)}); } - if (lockedSpillStats->spillFlushTimeUs != 0) { + if (lockedSpillStats->spillFlushTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillFlushTime, RuntimeCounter{ - static_cast( - lockedSpillStats->spillFlushTimeUs * - Timestamp::kNanosecondsInMicrosecond), - RuntimeCounter::Unit::kNanos}); + static_cast(lockedSpillStats->spillFlushTimeNanos)}); } if (lockedSpillStats->spillWrites != 0) { lockedStats->addRuntimeStat( kSpillWrites, RuntimeCounter{static_cast(lockedSpillStats->spillWrites)}); } - if (lockedSpillStats->spillWriteTimeUs != 0) { + if (lockedSpillStats->spillWriteTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillWriteTime, RuntimeCounter{ - static_cast( - lockedSpillStats->spillWriteTimeUs * - Timestamp::kNanosecondsInMicrosecond), - RuntimeCounter::Unit::kNanos}); + static_cast(lockedSpillStats->spillWriteTimeNanos)}); } if (lockedSpillStats->spillRuns != 0) { lockedStats->addRuntimeStat( @@ -380,22 +365,18 @@ void Operator::recordSpillStats() { RuntimeCounter{static_cast(lockedSpillStats->spillReads)}); } - if (lockedSpillStats->spillReadTimeUs != 0) { + if (lockedSpillStats->spillReadTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillReadTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillReadTimeUs) * - Timestamp::kNanosecondsInMicrosecond, - RuntimeCounter::Unit::kNanos}); + static_cast(lockedSpillStats->spillReadTimeNanos)}); } - if (lockedSpillStats->spillDeserializationTimeUs != 0) { + if (lockedSpillStats->spillDeserializationTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillDeserializationTime, - RuntimeCounter{ - static_cast(lockedSpillStats->spillDeserializationTimeUs) * - Timestamp::kNanosecondsInMicrosecond, - RuntimeCounter::Unit::kNanos}); + RuntimeCounter{static_cast( + lockedSpillStats->spillDeserializationTimeNanos)}); } lockedSpillStats->reset(); } diff --git a/velox/exec/SpillFile.cpp b/velox/exec/SpillFile.cpp index 912a11f0c4826..8aaf6a2f86d51 100644 --- a/velox/exec/SpillFile.cpp +++ b/velox/exec/SpillFile.cpp @@ -144,21 +144,21 @@ uint64_t SpillWriter::flush() { IOBufOutputStream out( *pool_, nullptr, std::max(64 * 1024, batch_->size())); - uint64_t flushTimeUs{0}; + uint64_t flushTimeNs{0}; { - MicrosecondTimer timer(&flushTimeUs); + NanosecondTimer timer(&flushTimeNs); batch_->flush(&out); } batch_.reset(); - uint64_t writeTimeUs{0}; + uint64_t writeTimeNs{0}; uint64_t writtenBytes{0}; auto iobuf = out.getIOBuf(); { - MicrosecondTimer timer(&writeTimeUs); + NanosecondTimer timer(&writeTimeNs); writtenBytes = file->write(std::move(iobuf)); } - updateWriteStats(writtenBytes, flushTimeUs, writeTimeUs); + updateWriteStats(writtenBytes, flushTimeNs, writeTimeNs); updateAndCheckSpillLimitCb_(writtenBytes); return writtenBytes; } @@ -168,9 +168,9 @@ uint64_t SpillWriter::write( const folly::Range& indices) { checkNotFinished(); - uint64_t timeUs{0}; + uint64_t timeNs{0}; { - MicrosecondTimer timer(&timeUs); + NanosecondTimer timer(&timeNs); if (batch_ == nullptr) { serializer::presto::PrestoVectorSerde::PrestoOptions options = { kDefaultUseLosslessTimestamp, compressionKind_, true /*nullsFirst*/}; @@ -182,7 +182,7 @@ uint64_t SpillWriter::write( } batch_->append(rows, indices); } - updateAppendStats(rows->size(), timeUs); + updateAppendStats(rows->size(), timeNs); if (batch_->size() < writeBufferSize_) { return 0; } @@ -191,24 +191,24 @@ uint64_t SpillWriter::write( void SpillWriter::updateAppendStats( uint64_t numRows, - uint64_t serializationTimeUs) { + uint64_t serializationTimeNs) { auto statsLocked = stats_->wlock(); statsLocked->spilledRows += numRows; - statsLocked->spillSerializationTimeUs += serializationTimeUs; - common::updateGlobalSpillAppendStats(numRows, serializationTimeUs); + statsLocked->spillSerializationTimeNanos += serializationTimeNs; + common::updateGlobalSpillAppendStats(numRows, serializationTimeNs); } void SpillWriter::updateWriteStats( uint64_t spilledBytes, - uint64_t flushTimeUs, - uint64_t fileWriteTimeUs) { + uint64_t flushTimeNs, + uint64_t fileWriteTimeNs) { auto statsLocked = stats_->wlock(); statsLocked->spilledBytes += spilledBytes; - statsLocked->spillFlushTimeUs += flushTimeUs; - statsLocked->spillWriteTimeUs += fileWriteTimeUs; + statsLocked->spillFlushTimeNanos += flushTimeNs; + statsLocked->spillWriteTimeNanos += fileWriteTimeNs; ++statsLocked->spillWrites; common::updateGlobalSpillWriteStats( - spilledBytes, flushTimeUs, fileWriteTimeUs); + spilledBytes, flushTimeNs, fileWriteTimeNs); } void SpillWriter::updateSpilledFileStats(uint64_t fileSize) { @@ -313,14 +313,14 @@ bool SpillReadFile::nextBatch(RowVectorPtr& rowVector) { return false; } - uint64_t timeUs{0}; + uint64_t timeNs{0}; { - MicrosecondTimer timer{&timeUs}; + NanosecondTimer timer{&timeNs}; VectorStreamGroup::read( input_.get(), pool_, type_, &rowVector, &readOptions_); } - stats_->wlock()->spillDeserializationTimeUs += timeUs; - common::updateGlobalSpillDeserializationTimeUs(timeUs); + stats_->wlock()->spillDeserializationTimeNanos += timeNs; + common::updateGlobalSpillDeserializationTimeNs(timeNs); return true; } @@ -328,10 +328,13 @@ void SpillReadFile::recordSpillStats() { VELOX_CHECK(input_->atEnd()); const auto readStats = input_->stats(); common::updateGlobalSpillReadStats( - readStats.numReads, readStats.readBytes, readStats.readTimeUs); + readStats.numReads, + readStats.readBytes, + readStats.readTimeUs * Timestamp::kNanosecondsInMicrosecond); auto lockedSpillStats = stats_->wlock(); lockedSpillStats->spillReads += readStats.numReads; - lockedSpillStats->spillReadTimeUs += readStats.readTimeUs; + lockedSpillStats->spillReadTimeNanos += + readStats.readTimeUs * Timestamp::kNanosecondsInMicrosecond; lockedSpillStats->spillReadBytes += readStats.readBytes; } } // namespace facebook::velox::exec diff --git a/velox/exec/Spiller.cpp b/velox/exec/Spiller.cpp index 67a2196c67fad..96c38f67e2ed3 100644 --- a/velox/exec/Spiller.cpp +++ b/velox/exec/Spiller.cpp @@ -413,9 +413,9 @@ void Spiller::ensureSorted(SpillRun& run) { return; } - uint64_t sortTimeUs{0}; + uint64_t sortTimeNs{0}; { - MicrosecondTimer timer(&sortTimeUs); + NanosecondTimer timer(&sortTimeNs); gfx::timsort( run.rows.begin(), run.rows.end(), @@ -428,7 +428,7 @@ void Spiller::ensureSorted(SpillRun& run) { // NOTE: Always set a non-zero sort time to avoid flakiness in tests which // check sort time. - updateSpillSortTime(std::max(1, sortTimeUs)); + updateSpillSortTime(std::max(1, sortTimeNs)); } std::unique_ptr Spiller::writeSpill(int32_t partition) { @@ -525,14 +525,14 @@ void Spiller::runSpill(bool lastRun) { } } -void Spiller::updateSpillFillTime(uint64_t timeUs) { - spillStats_->wlock()->spillFillTimeUs += timeUs; - common::updateGlobalSpillFillTime(timeUs); +void Spiller::updateSpillFillTime(uint64_t timeNs) { + spillStats_->wlock()->spillFillTimeNanos += timeNs; + common::updateGlobalSpillFillTime(timeNs); } -void Spiller::updateSpillSortTime(uint64_t timeUs) { - spillStats_->wlock()->spillSortTimeUs += timeUs; - common::updateGlobalSpillSortTime(timeUs); +void Spiller::updateSpillSortTime(uint64_t timeNs) { + spillStats_->wlock()->spillSortTimeNanos += timeNs; + common::updateGlobalSpillSortTime(timeNs); } bool Spiller::needSort() const { @@ -644,9 +644,9 @@ bool Spiller::fillSpillRuns(RowContainerIterator* iterator) { checkEmptySpillRuns(); bool lastRun{false}; - uint64_t execTimeUs{0}; + uint64_t execTimeNs{0}; { - MicrosecondTimer timer(&execTimeUs); + NanosecondTimer timer(&execTimeNs); // Number of rows to hash and divide into spill partitions at a time. constexpr int32_t kHashBatchSize = 4096; @@ -690,7 +690,7 @@ bool Spiller::fillSpillRuns(RowContainerIterator* iterator) { } } } - updateSpillFillTime(execTimeUs); + updateSpillFillTime(execTimeNs); return lastRun; } @@ -698,16 +698,16 @@ bool Spiller::fillSpillRuns(RowContainerIterator* iterator) { void Spiller::fillSpillRun(std::vector& rows) { VELOX_CHECK_EQ(bits_.numPartitions(), 1); checkEmptySpillRuns(); - uint64_t execTimeUs{0}; + uint64_t execTimeNs{0}; { - MicrosecondTimer timer(&execTimeUs); + NanosecondTimer timer(&execTimeNs); spillRuns_[0].rows = SpillRows(rows.begin(), rows.end(), spillRuns_[0].rows.get_allocator()); for (const auto* row : rows) { spillRuns_[0].numBytes += container_->rowSize(row); } } - updateSpillFillTime(execTimeUs); + updateSpillFillTime(execTimeNs); } std::string Spiller::toString() const { diff --git a/velox/exec/tests/SpillTest.cpp b/velox/exec/tests/SpillTest.cpp index e7cbe7a006137..e611bdb103cb4 100644 --- a/velox/exec/tests/SpillTest.cpp +++ b/velox/exec/tests/SpillTest.cpp @@ -293,12 +293,12 @@ class SpillTest : public ::testing::TestWithParam, ASSERT_GT(stats.spillWrites, 0); // NOTE: On fast machines we might have sub-microsecond in each write, // resulting in 0us total write time. - ASSERT_GE(stats.spillWriteTimeUs, 0); - ASSERT_GE(stats.spillFlushTimeUs, 0); + ASSERT_GE(stats.spillWriteTimeNanos, 0); + ASSERT_GE(stats.spillFlushTimeNanos, 0); ASSERT_GT(stats.spilledRows, 0); // NOTE: the following stats are not collected by spill state. - ASSERT_EQ(stats.spillFillTimeUs, 0); - ASSERT_EQ(stats.spillSortTimeUs, 0); + ASSERT_EQ(stats.spillFillTimeNanos, 0); + ASSERT_EQ(stats.spillSortTimeNanos, 0); const auto newGStats = common::globalSpillStats(); ASSERT_EQ( prevGStats.spilledPartitions + stats.spilledPartitions, @@ -310,19 +310,19 @@ class SpillTest : public ::testing::TestWithParam, ASSERT_EQ( prevGStats.spillWrites + stats.spillWrites, newGStats.spillWrites); ASSERT_EQ( - prevGStats.spillWriteTimeUs + stats.spillWriteTimeUs, - newGStats.spillWriteTimeUs); + prevGStats.spillWriteTimeNanos + stats.spillWriteTimeNanos, + newGStats.spillWriteTimeNanos); ASSERT_EQ( - prevGStats.spillFlushTimeUs + stats.spillFlushTimeUs, - newGStats.spillFlushTimeUs); + prevGStats.spillFlushTimeNanos + stats.spillFlushTimeNanos, + newGStats.spillFlushTimeNanos); ASSERT_EQ( prevGStats.spilledRows + stats.spilledRows, newGStats.spilledRows); ASSERT_EQ( - prevGStats.spillFillTimeUs + stats.spillFillTimeUs, - newGStats.spillFillTimeUs); + prevGStats.spillFillTimeNanos + stats.spillFillTimeNanos, + newGStats.spillFillTimeNanos); ASSERT_EQ( - prevGStats.spillSortTimeUs + stats.spillSortTimeUs, - newGStats.spillSortTimeUs); + prevGStats.spillSortTimeNanos + stats.spillSortTimeNanos, + newGStats.spillSortTimeNanos); // Verifies the spill file id for (auto& partitionNum : state_->spilledPartitionSet()) { @@ -395,35 +395,33 @@ class SpillTest : public ::testing::TestWithParam, const auto finalStats = spillStats_.copy(); ASSERT_EQ(finalStats.spillReadBytes, finalStats.spilledBytes); ASSERT_GT(finalStats.spillReads, 0); - ASSERT_GT(finalStats.spillReadTimeUs, 0); - ASSERT_GT(finalStats.spillDeserializationTimeUs, 0); - + ASSERT_GT(finalStats.spillReadTimeNanos, 0); + ASSERT_GT(finalStats.spillDeserializationTimeNanos, 0); ASSERT_EQ( finalStats.toString(), fmt::format( - "spillRuns[{}] spilledInputBytes[{}] spilledBytes[{}] " - "spilledRows[{}] spilledPartitions[{}] spilledFiles[{}] " - "spillFillTimeUs[{}] spillSortTime[{}] spillSerializationTime[{}] " - "spillWrites[{}] spillFlushTime[{}] spillWriteTime[{}] " - "maxSpillExceededLimitCount[0] spillReadBytes[{}] spillReads[{}] " - "spillReadTime[{}] spillReadDeserializationTime[{}]", + "spillRuns[{}] spilledInputBytes[{}] spilledBytes[{}] spilledRows[{}] " + "spilledPartitions[{}] spilledFiles[{}] spillFillTimeNanos[{}] " + "spillSortTimeNanos[{}] spillSerializationTimeNanos[{}] spillWrites[{}] " + "spillFlushTimeNanos[{}] spillWriteTimeNanos[{}] maxSpillExceededLimitCount[0] " + "spillReadBytes[{}] spillReads[{}] spillReadTimeNanos[{}] " + "spillReadDeserializationTimeNanos[{}]", finalStats.spillRuns, succinctBytes(finalStats.spilledInputBytes), succinctBytes(finalStats.spilledBytes), finalStats.spilledRows, finalStats.spilledPartitions, finalStats.spilledFiles, - succinctMicros(finalStats.spillFillTimeUs), - succinctMicros(finalStats.spillSortTimeUs), - succinctMicros(finalStats.spillSerializationTimeUs), + succinctNanos(finalStats.spillFillTimeNanos), + succinctNanos(finalStats.spillSortTimeNanos), + succinctNanos(finalStats.spillSerializationTimeNanos), finalStats.spillWrites, - succinctMicros(finalStats.spillFlushTimeUs), - succinctMicros(finalStats.spillWriteTimeUs), + succinctNanos(finalStats.spillFlushTimeNanos), + succinctNanos(finalStats.spillWriteTimeNanos), succinctBytes(finalStats.spillReadBytes), finalStats.spillReads, - succinctMicros(finalStats.spillReadTimeUs), - succinctMicros(finalStats.spillDeserializationTimeUs))); - + succinctNanos(finalStats.spillReadTimeNanos), + succinctNanos(finalStats.spillDeserializationTimeNanos))); // Verify the spilled files are still there after spill state destruction. for (const auto& spilledFile : spilledFileSet) { ASSERT_TRUE(fs->exists(spilledFile)); diff --git a/velox/exec/tests/SpillerTest.cpp b/velox/exec/tests/SpillerTest.cpp index a88c62f9a493f..4d1db05164b18 100644 --- a/velox/exec/tests/SpillerTest.cpp +++ b/velox/exec/tests/SpillerTest.cpp @@ -290,15 +290,15 @@ class SpillerTest : public exec::test::RowContainerTestBase { ASSERT_EQ(stats.spilledBytes, totalSpilledBytes); ASSERT_EQ(stats.spillReadBytes, totalSpilledBytes); - ASSERT_GT(stats.spillWriteTimeUs, 0); + ASSERT_GT(stats.spillWriteTimeNanos, 0); if (type_ == Spiller::Type::kAggregateOutput) { - ASSERT_EQ(stats.spillSortTimeUs, 0); + ASSERT_EQ(stats.spillSortTimeNanos, 0); } else { - ASSERT_GT(stats.spillSortTimeUs, 0); + ASSERT_GT(stats.spillSortTimeNanos, 0); } - ASSERT_GT(stats.spillFlushTimeUs, 0); - ASSERT_GT(stats.spillFillTimeUs, 0); - ASSERT_GT(stats.spillSerializationTimeUs, 0); + ASSERT_GT(stats.spillFlushTimeNanos, 0); + ASSERT_GT(stats.spillFillTimeNanos, 0); + ASSERT_GT(stats.spillSerializationTimeNanos, 0); ASSERT_GT(stats.spillWrites, 0); const auto newGStats = common::globalSpillStats(); @@ -316,29 +316,30 @@ class SpillerTest : public exec::test::RowContainerTestBase { newGStats.spillReadBytes); ASSERT_EQ(prevGStats.spillReads + stats.spillReads, newGStats.spillReads); ASSERT_EQ( - prevGStats.spillReadTimeUs + stats.spillReadTimeUs, - newGStats.spillReadTimeUs); + prevGStats.spillReadTimeNanos + stats.spillReadTimeNanos, + newGStats.spillReadTimeNanos); ASSERT_EQ( - prevGStats.spillDeserializationTimeUs + - stats.spillDeserializationTimeUs, - newGStats.spillDeserializationTimeUs); + prevGStats.spillDeserializationTimeNanos + + stats.spillDeserializationTimeNanos, + newGStats.spillDeserializationTimeNanos); ASSERT_EQ( - prevGStats.spillWriteTimeUs + stats.spillWriteTimeUs, - newGStats.spillWriteTimeUs); + prevGStats.spillWriteTimeNanos + stats.spillWriteTimeNanos, + newGStats.spillWriteTimeNanos); ASSERT_EQ( - prevGStats.spillSortTimeUs + stats.spillSortTimeUs, - newGStats.spillSortTimeUs); + prevGStats.spillSortTimeNanos + stats.spillSortTimeNanos, + newGStats.spillSortTimeNanos); ASSERT_EQ( - prevGStats.spillFlushTimeUs + stats.spillFlushTimeUs, - newGStats.spillFlushTimeUs) - << prevGStats.spillFlushTimeUs << " " << stats.spillFlushTimeUs << " " - << newGStats.spillFlushTimeUs; + prevGStats.spillFlushTimeNanos + stats.spillFlushTimeNanos, + newGStats.spillFlushTimeNanos) + << prevGStats.spillFlushTimeNanos << " " << stats.spillFlushTimeNanos + << " " << newGStats.spillFlushTimeNanos; ASSERT_EQ( - prevGStats.spillFillTimeUs + stats.spillFillTimeUs, - newGStats.spillFillTimeUs); + prevGStats.spillFillTimeNanos + stats.spillFillTimeNanos, + newGStats.spillFillTimeNanos); ASSERT_EQ( - prevGStats.spillSerializationTimeUs + stats.spillSerializationTimeUs, - newGStats.spillSerializationTimeUs); + prevGStats.spillSerializationTimeNanos + + stats.spillSerializationTimeNanos, + newGStats.spillSerializationTimeNanos); ASSERT_EQ( prevGStats.spillWrites + stats.spillWrites, newGStats.spillWrites); @@ -846,33 +847,33 @@ class SpillerTest : public exec::test::RowContainerTestBase { if (numAppendBatches == 0) { ASSERT_EQ(stats.spilledRows, 0); ASSERT_EQ(stats.spilledBytes, 0); - ASSERT_EQ(stats.spillWriteTimeUs, 0); - ASSERT_EQ(stats.spillFlushTimeUs, 0); - ASSERT_EQ(stats.spillSerializationTimeUs, 0); + ASSERT_EQ(stats.spillWriteTimeNanos, 0); + ASSERT_EQ(stats.spillFlushTimeNanos, 0); + ASSERT_EQ(stats.spillSerializationTimeNanos, 0); ASSERT_EQ(stats.spillWrites, 0); } else { ASSERT_GT(stats.spilledRows, 0); ASSERT_GT(stats.spilledBytes, 0); - ASSERT_GT(stats.spillWriteTimeUs, 0); - ASSERT_GT(stats.spillFlushTimeUs, 0); - ASSERT_GT(stats.spillSerializationTimeUs, 0); + ASSERT_GT(stats.spillWriteTimeNanos, 0); + ASSERT_GT(stats.spillFlushTimeNanos, 0); + ASSERT_GT(stats.spillSerializationTimeNanos, 0); ASSERT_GT(stats.spillWrites, 0); } } else { ASSERT_GT(stats.spilledRows, 0); ASSERT_GT(stats.spilledBytes, 0); - ASSERT_GT(stats.spillWriteTimeUs, 0); - ASSERT_GT(stats.spillFlushTimeUs, 0); - ASSERT_GT(stats.spillSerializationTimeUs, 0); + ASSERT_GT(stats.spillWriteTimeNanos, 0); + ASSERT_GT(stats.spillFlushTimeNanos, 0); + ASSERT_GT(stats.spillSerializationTimeNanos, 0); ASSERT_GT(stats.spillWrites, 0); } ASSERT_GT(stats.spilledPartitions, 0); - ASSERT_EQ(stats.spillSortTimeUs, 0); + ASSERT_EQ(stats.spillSortTimeNanos, 0); if (type_ == Spiller::Type::kHashJoinBuild || type_ == Spiller::Type::kRowNumber) { - ASSERT_GT(stats.spillFillTimeUs, 0); + ASSERT_GT(stats.spillFillTimeNanos, 0); } else { - ASSERT_EQ(stats.spillFillTimeUs, 0); + ASSERT_EQ(stats.spillFillTimeNanos, 0); } const auto newGStats = common::globalSpillStats(); @@ -886,22 +887,23 @@ class SpillerTest : public exec::test::RowContainerTestBase { ASSERT_EQ( prevGStats.spilledBytes + stats.spilledBytes, newGStats.spilledBytes); ASSERT_EQ( - prevGStats.spillWriteTimeUs + stats.spillWriteTimeUs, - newGStats.spillWriteTimeUs); + prevGStats.spillWriteTimeNanos + stats.spillWriteTimeNanos, + newGStats.spillWriteTimeNanos); ASSERT_EQ( - prevGStats.spillSortTimeUs + stats.spillSortTimeUs, - newGStats.spillSortTimeUs); + prevGStats.spillSortTimeNanos + stats.spillSortTimeNanos, + newGStats.spillSortTimeNanos); ASSERT_EQ( - prevGStats.spillFlushTimeUs + stats.spillFlushTimeUs, - newGStats.spillFlushTimeUs) - << prevGStats.spillFlushTimeUs << " " << stats.spillFlushTimeUs << " " - << newGStats.spillFlushTimeUs; + prevGStats.spillFlushTimeNanos + stats.spillFlushTimeNanos, + newGStats.spillFlushTimeNanos) + << prevGStats.spillFlushTimeNanos << " " << stats.spillFlushTimeNanos + << " " << newGStats.spillFlushTimeNanos; ASSERT_EQ( - prevGStats.spillFillTimeUs + stats.spillFillTimeUs, - newGStats.spillFillTimeUs); + prevGStats.spillFillTimeNanos + stats.spillFillTimeNanos, + newGStats.spillFillTimeNanos); ASSERT_EQ( - prevGStats.spillSerializationTimeUs + stats.spillSerializationTimeUs, - newGStats.spillSerializationTimeUs); + prevGStats.spillSerializationTimeNanos + + stats.spillSerializationTimeNanos, + newGStats.spillSerializationTimeNanos); ASSERT_EQ( prevGStats.spillWrites + stats.spillWrites, newGStats.spillWrites); @@ -1464,7 +1466,7 @@ TEST_P(AggregationOutputOnly, basic) { ASSERT_EQ(stats.spilledFiles, 1) << stats.toString(); ASSERT_EQ(stats.spilledRows, expectedNumSpilledRows) << stats.toString(); } - ASSERT_EQ(stats.spillSortTimeUs, 0); + ASSERT_EQ(stats.spillSortTimeNanos, 0); } } @@ -1571,7 +1573,7 @@ TEST_P(OrderByOutputOnly, basic) { ASSERT_EQ(stats.spilledFiles, 1) << stats.toString(); ASSERT_EQ(stats.spilledRows, expectedNumSpilledRows) << stats.toString(); } - ASSERT_EQ(stats.spillSortTimeUs, 0); + ASSERT_EQ(stats.spillSortTimeNanos, 0); } } From 8b75147a8183442b8dc4bb978aca446628490f26 Mon Sep 17 00:00:00 2001 From: Kevin Wilfong Date: Wed, 21 Aug 2024 13:54:47 -0700 Subject: [PATCH 05/24] Cache the hash map in SubscriptUtil if a single map is reused with complex keys (#10414) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10414 Today in SubscriptUtil, if the map passed in has primitive keys and the same MapVector is passed in multiple times, we cache the hash maps for optimized lookups across batches. If the map has complex keys, and base MapVector has a single value, we construct a local hash map to optimize the lookups within a batch. This change merges the two approaches for maps with Complex keys, so if we see the same Vector passed in multiple times where the base MapVector has a single value, we cache the hash map so we don't need to reconstruct it for every batch. In some cases we've seen this can significantly speed up Presto queries, particularly because the cost of hashing complex types to construct the hash map can be fairly high. We could go a step further and, like for maps with primitive keys, cache maps with complex keys regardless of the number of values in the base MapVector, but I haven't seen any cases that would benefit from this yet so I'm not sure what the tradeoffs in terms of memory and construction cost would look like, so extending the original optimizations seems like a good starting point. Reviewed By: Yuhta Differential Revision: D59474490 fbshipit-source-id: 67da5f560b6522dbfa96af3774a4f9b1f738d7fa --- velox/functions/lib/SubscriptUtil.cpp | 89 +++++----- velox/functions/lib/SubscriptUtil.h | 89 +++++++--- .../MapSubscriptCachingBenchmark.cpp | 21 ++- .../prestosql/tests/ElementAtTest.cpp | 157 +++++++++++++++++- velox/vector/fuzzer/VectorFuzzer.cpp | 7 +- 5 files changed, 289 insertions(+), 74 deletions(-) diff --git a/velox/functions/lib/SubscriptUtil.cpp b/velox/functions/lib/SubscriptUtil.cpp index 8c9b60ccdb0b2..51931384855f3 100644 --- a/velox/functions/lib/SubscriptUtil.cpp +++ b/velox/functions/lib/SubscriptUtil.cpp @@ -58,7 +58,7 @@ struct SimpleType { template VectorPtr applyMapTyped( bool triggerCaching, - std::shared_ptr& cachedLookupTablePtr, + std::shared_ptr& cachedLookupTablePtr, const SelectivityVector& rows, const VectorPtr& mapArg, const VectorPtr& indexArg, @@ -66,14 +66,14 @@ VectorPtr applyMapTyped( static constexpr vector_size_t kMinCachedMapSize = 100; using TKey = typename TypeTraits::NativeType; - LookupTable* typedLookupTable = nullptr; + detail::LookupTable* typedLookupTable = nullptr; if (triggerCaching) { if (!cachedLookupTablePtr) { cachedLookupTablePtr = - std::make_shared>(*context.pool()); + std::make_shared>(*context.pool()); } - typedLookupTable = cachedLookupTablePtr->typedTable(); + typedLookupTable = cachedLookupTablePtr->typedTable(); } auto* pool = context.pool(); @@ -178,39 +178,13 @@ VectorPtr applyMapTyped( nullsBuilder.build(), indices, rows.end(), baseMap->mapValues()); } -// A flat vector of map keys, an index into that vector and an index into -// the original map keys vector that may have encodings. -struct MapKey { - const BaseVector* baseVector; - const vector_size_t baseIndex; - const vector_size_t index; - - size_t hash() const { - return baseVector->hashValueAt(baseIndex); - } - - bool operator==(const MapKey& other) const { - return baseVector->equalValueAt( - other.baseVector, baseIndex, other.baseIndex); - } - - bool operator<(const MapKey& other) const { - return baseVector->compare(other.baseVector, baseIndex, other.baseIndex) < - 0; - } -}; - -struct MapKeyHasher { - size_t operator()(const MapKey& key) const { - return key.hash(); - } -}; - VectorPtr applyMapComplexType( const SelectivityVector& rows, const VectorPtr& mapArg, const VectorPtr& indexArg, - exec::EvalCtx& context) { + exec::EvalCtx& context, + bool triggerCaching, + std::shared_ptr& cachedLookupTablePtr) { auto* pool = context.pool(); // Use indices with the mapValues wrapped in a dictionary vector. @@ -247,18 +221,42 @@ VectorPtr applyMapComplexType( // Fast path for the case of a single map. It may be constant or dictionary // encoded. Use hash table for quick search. if (baseMap->size() == 1) { - folly::F14FastSet set; - auto numKeys = rawSizes[0]; - set.reserve(numKeys * 1.3); - for (auto i = 0; i < numKeys; ++i) { - set.insert(MapKey{mapKeysBase, mapKeysIndices[i], i}); + detail::ComplexKeyHashMap hashMap{detail::MapKeyAllocator(*pool)}; + detail::ComplexKeyHashMap* hashMapPtr = &hashMap; + + if (triggerCaching) { + if (!cachedLookupTablePtr) { + cachedLookupTablePtr = + std::make_shared>(*context.pool()); + } + + detail::LookupTable* typedLookupTable = + cachedLookupTablePtr->typedTable(); + + static constexpr vector_size_t kMapIndex = 0; + + if (!typedLookupTable->containsMapAtIndex(kMapIndex)) { + typedLookupTable->ensureMapAtIndex(kMapIndex); + } + + auto& map = typedLookupTable->getMapAtIndex(kMapIndex); + hashMapPtr = ↦ } + + if (hashMapPtr->empty()) { + auto numKeys = rawSizes[0]; + hashMapPtr->reserve(numKeys * 1.3); + for (auto i = 0; i < numKeys; ++i) { + hashMapPtr->insert(detail::MapKey{mapKeysBase, mapKeysIndices[i], i}); + } + } + rows.applyToSelected([&](vector_size_t row) { VELOX_CHECK_EQ(0, mapIndices[row]); auto searchIndex = searchIndices[row]; - auto it = set.find(MapKey{searchBase, searchIndex, row}); - if (it != set.end()) { + auto it = hashMapPtr->find(detail::MapKey{searchBase, searchIndex, row}); + if (it != hashMapPtr->end()) { rawIndices[row] = it->index; } else { nullsBuilder.setNull(row); @@ -302,6 +300,8 @@ VectorPtr applyMapComplexType( } // namespace +namespace detail { + VectorPtr MapSubscript::applyMap( const SelectivityVector& rows, std::vector& args, @@ -312,20 +312,20 @@ VectorPtr MapSubscript::applyMap( // Ensure map key type and second argument are the same. VELOX_CHECK(mapArg->type()->childAt(0)->equivalent(*indexArg->type())); + bool triggerCaching = shouldTriggerCaching(mapArg); if (indexArg->type()->isPrimitiveType()) { - bool triggerCaching = shouldTriggerCaching(mapArg); - return VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH( applyMapTyped, indexArg->typeKind(), triggerCaching, - const_cast&>(lookupTable_), + lookupTable_, rows, mapArg, indexArg, context); } else { - return applyMapComplexType(rows, mapArg, indexArg, context); + return applyMapComplexType( + rows, mapArg, indexArg, context, triggerCaching, lookupTable_); } } @@ -369,5 +369,6 @@ const std::exception_ptr& negativeSubscriptError() { static std::exception_ptr error = makeNegativeSubscriptError(); return error; } +} // namespace detail } // namespace facebook::velox::functions diff --git a/velox/functions/lib/SubscriptUtil.h b/velox/functions/lib/SubscriptUtil.h index 18daf68e03a9c..2700d0c1bf070 100644 --- a/velox/functions/lib/SubscriptUtil.h +++ b/velox/functions/lib/SubscriptUtil.h @@ -30,28 +30,64 @@ namespace facebook::velox::functions { +namespace detail { // Below functions return a stock instance of each of the possible errors in // SubscriptImpl const std::exception_ptr& zeroSubscriptError(); const std::exception_ptr& badSubscriptError(); const std::exception_ptr& negativeSubscriptError(); -template +// A flat vector of map keys, an index into that vector and an index into +// the original map keys vector that may have encodings. +struct MapKey { + const BaseVector* baseVector; + const vector_size_t baseIndex; + const vector_size_t index; + + size_t hash() const { + return baseVector->hashValueAt(baseIndex); + } + + bool operator==(const MapKey& other) const { + return baseVector->equalValueAt( + other.baseVector, baseIndex, other.baseIndex); + } + + bool operator<(const MapKey& other) const { + return baseVector->compare(other.baseVector, baseIndex, other.baseIndex) < + 0; + } +}; + +struct MapKeyHasher { + size_t operator()(const MapKey& key) const { + return key.hash(); + } +}; + +using MapKeyAllocator = memory::StlAllocator; + +using ComplexKeyHashMap = folly::F14FastSet< + detail::MapKey, + detail::MapKeyHasher, + folly::f14::DefaultKeyEqual, + MapKeyAllocator>; + +template class LookupTable; class LookupTableBase { public: - template - LookupTable* typedTable() { - return static_cast*>(this); + template + LookupTable* typedTable() { + return static_cast*>(this); } virtual ~LookupTableBase() {} }; -template +// NativeType should by TypeTraits::NativeType for the key's TypeKind. +template class LookupTable : public LookupTableBase { - using key_t = typename TypeTraits::NativeType; - public: LookupTable(memory::MemoryPool& pool) : pool_(pool), @@ -75,11 +111,22 @@ class LookupTable : public LookupTableBase { } private: - using inner_allocator_t = - memory::StlAllocator>; - - using inner_map_t = typename util::floating_point:: - HashMapNaNAwareTypeTraits::Type; + // If the NativeType is not void, we can materialize the key in memory + // directly, so we can use a HashMap keyed on the native value. If it is void + // then we have to use MapKey as the key to wrap the Vector and avoid + // materializing the key in memory. + using inner_allocator_t = std::conditional_t< + std::is_same_v, + MapKeyAllocator, + memory::StlAllocator>>; + + using inner_map_t = std::conditional_t< + std::is_same_v, + ComplexKeyHashMap, + typename util::floating_point::HashMapNaNAwareTypeTraits< + NativeType, + vector_size_t, + inner_allocator_t>::Type>; using outer_allocator_t = memory::StlAllocator>; @@ -123,9 +170,8 @@ class MapSubscript { return false; } - if (!mapArg->type()->childAt(0)->isPrimitiveType() && - !!mapArg->type()->childAt(0)->isBoolean()) { - // Disable caching if the key type is not primitive or is boolean. + if (mapArg->type()->childAt(0)->isBoolean()) { + // Disable caching if the key type is boolean. allowCaching_ = false; return false; } @@ -158,6 +204,7 @@ class MapSubscript { // Materialized cached version of firstSeenMap_ used to optimize the lookup. mutable std::shared_ptr lookupTable_; }; +} // namespace detail /// Generic subscript/element_at implementation for both array and map data /// types. @@ -179,7 +226,7 @@ template < class SubscriptImpl : public exec::Subscript { public: explicit SubscriptImpl(bool allowCaching) - : mapSubscript_(MapSubscript(allowCaching)) {} + : mapSubscript_(detail::MapSubscript(allowCaching)) {} void apply( const SelectivityVector& rows, @@ -286,7 +333,7 @@ class SubscriptImpl : public exec::Subscript { const auto adjustedIndex = adjustIndex(decodedIndices->valueAt(0), isZeroSubscriptError); if (isZeroSubscriptError) { - context.setErrors(rows, zeroSubscriptError()); + context.setErrors(rows, detail::zeroSubscriptError()); allFailed = true; } @@ -307,7 +354,7 @@ class SubscriptImpl : public exec::Subscript { const auto adjustedIndex = adjustIndex(originalIndex, isZeroSubscriptError); if (isZeroSubscriptError) { - context.setVeloxExceptionError(row, zeroSubscriptError()); + context.setVeloxExceptionError(row, detail::zeroSubscriptError()); return; } const auto elementIndex = getIndex( @@ -372,7 +419,7 @@ class SubscriptImpl : public exec::Subscript { index += arraySize; } } else { - context.setVeloxExceptionError(row, negativeSubscriptError()); + context.setVeloxExceptionError(row, detail::negativeSubscriptError()); return -1; } } @@ -383,7 +430,7 @@ class SubscriptImpl : public exec::Subscript { if constexpr (allowOutOfBound) { return -1; } else { - context.setVeloxExceptionError(row, badSubscriptError()); + context.setVeloxExceptionError(row, detail::badSubscriptError()); return -1; } } @@ -394,7 +441,7 @@ class SubscriptImpl : public exec::Subscript { } private: - MapSubscript mapSubscript_; + detail::MapSubscript mapSubscript_; }; } // namespace facebook::velox::functions diff --git a/velox/functions/prestosql/benchmarks/MapSubscriptCachingBenchmark.cpp b/velox/functions/prestosql/benchmarks/MapSubscriptCachingBenchmark.cpp index e46b9d01d66b3..63568002bb60a 100644 --- a/velox/functions/prestosql/benchmarks/MapSubscriptCachingBenchmark.cpp +++ b/velox/functions/prestosql/benchmarks/MapSubscriptCachingBenchmark.cpp @@ -37,6 +37,7 @@ extern void registerSubscriptFunction( int main(int argc, char** argv) { folly::Init init(&argc, &argv); + memory::MemoryManager::testingSetInstance({}); ExpressionBenchmarkBuilder benchmarkBuilder; facebook::velox::functions::prestosql::registerAllScalarFunctions(); @@ -53,7 +54,8 @@ int main(int argc, char** argv) { VectorFuzzer::Options options; options.vectorSize = 1000; options.containerLength = mapLength; - options.complexElementsMaxSize = 10000000000; + // Make sure it's big enough for nested complex types. + options.complexElementsMaxSize = baseVectorSize * mapLength * mapLength; options.containerVariableLength = false; VectorFuzzer fuzzer(options, pool); @@ -112,6 +114,23 @@ int main(int argc, char** argv) { createSetsForType(INTEGER()); createSetsForType(VARCHAR()); + // For complex types, caching only applies if the Vector has a single constant + // value, so we only run with a baseVectorSize of 1. Also, due to the + // cost of the cardinality explosion from having nested complex types, we + // limit the number of iterations to 100. + auto createSetsForComplexType = [&](const auto& keyType) { + createSet(MAP(keyType, INTEGER()), 10, 1, 100); + + createSet(MAP(keyType, INTEGER()), 100, 1, 100); + + createSet(MAP(keyType, INTEGER()), 1000, 1, 100); + + createSet(MAP(keyType, INTEGER()), 10000, 1, 100); + }; + + createSetsForComplexType(ARRAY(BIGINT())); + createSetsForComplexType(MAP(INTEGER(), VARCHAR())); + benchmarkBuilder.registerBenchmarks(); benchmarkBuilder.testBenchmarks(); diff --git a/velox/functions/prestosql/tests/ElementAtTest.cpp b/velox/functions/prestosql/tests/ElementAtTest.cpp index 384ab8e580705..1a7a6b8d003d2 100644 --- a/velox/functions/prestosql/tests/ElementAtTest.cpp +++ b/velox/functions/prestosql/tests/ElementAtTest.cpp @@ -143,7 +143,8 @@ class ElementAtTest : public FunctionBaseTest { auto keys = makeFlatVector(std::vector({kSNaN})); std::vector args = {inputMap, keys}; - facebook::velox::functions::MapSubscript mapSubscriptWithCaching(true); + facebook::velox::functions::detail::MapSubscript mapSubscriptWithCaching( + true); auto checkStatus = [&](bool cachingEnabled, bool materializedMapIsNull, @@ -1030,7 +1031,7 @@ TEST_F(ElementAtTest, errorStatesArray) { [](auto row) { return row == 40; }); } -TEST_F(ElementAtTest, testCachingOptimzation) { +TEST_F(ElementAtTest, testCachingOptimization) { std::vector>>> inputMapVectorData; inputMapVectorData.push_back({}); @@ -1068,7 +1069,8 @@ TEST_F(ElementAtTest, testCachingOptimzation) { auto keys = makeFlatVector({0, 0, 0}); std::vector args = {inputMap, keys}; - facebook::velox::functions::MapSubscript mapSubscriptWithCaching(true); + facebook::velox::functions::detail::MapSubscript mapSubscriptWithCaching( + true); auto checkStatus = [&](bool cachingEnabled, bool materializedMapIsNull, @@ -1100,8 +1102,7 @@ TEST_F(ElementAtTest, testCachingOptimzation) { // Test the cached map content. auto verfyCachedContent = [&]() { auto& cachedMapTyped = - *static_cast< - facebook::velox::functions::LookupTable*>( + *static_cast*>( mapSubscriptWithCaching.lookupTable().get()) ->map(); @@ -1182,3 +1183,149 @@ TEST_F(ElementAtTest, floatingPointCornerCases) { testFloatingPointCornerCases(); testFloatingPointCornerCases(); } + +TEST_F(ElementAtTest, testCachingOptimizationComplexKey) { + std::vector> keys; + std::vector values; + for (int i = 0; i < 999; i += 3) { + // [0, 1, 2] -> 1000 + // [3, 4, 5] -> 1003 + // ... + keys.push_back({i, i + 1, i + 2}); + values.push_back(i + 1000); + } + + // Make a dummy eval context. + exec::ExprSet exprSet({}, &execCtx_); + auto inputs = makeRowVector({}); + exec::EvalCtx evalCtx(&execCtx_, &exprSet, inputs.get()); + + SelectivityVector rows(1); + auto keysVector = makeArrayVector(keys); + auto valuesVector = makeFlatVector(values); + auto inputMap = makeMapVector({0}, keysVector, valuesVector); + + auto inputKeys = makeArrayVector({{0, 1, 2}}); + std::vector args{inputMap, inputKeys}; + + facebook::velox::functions::detail::MapSubscript mapSubscriptWithCaching( + true); + + auto checkStatus = [&](bool cachingEnabled, + bool materializedMapIsNull, + const VectorPtr& firtSeen) { + EXPECT_EQ(cachingEnabled, mapSubscriptWithCaching.cachingEnabled()); + EXPECT_EQ(firtSeen, mapSubscriptWithCaching.firstSeenMap()); + EXPECT_EQ( + materializedMapIsNull, + nullptr == mapSubscriptWithCaching.lookupTable()); + }; + + // Initial state. + checkStatus(true, true, nullptr); + + auto result1 = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + // Nothing has been materialized yet since the input is seen only once. + checkStatus(true, true, args[0]); + + auto result2 = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(true, false, args[0]); + + auto result3 = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(true, false, args[0]); + + // all the result should be the same. + test::assertEqualVectors(result1, result2); + test::assertEqualVectors(result2, result3); + + // Test the cached map content. + auto verfyCachedContent = [&]() { + auto& cachedMap = mapSubscriptWithCaching.lookupTable() + ->typedTable() + ->getMapAtIndex(0); + + for (int i = 0; i < keysVector->size(); i += 3) { + EXPECT_NE( + cachedMap.end(), + cachedMap.find(facebook::velox::functions::detail::MapKey{ + keysVector.get(), 0, 0})); + } + }; + + verfyCachedContent(); + // Pass different map with same base. + { + auto dictInput = BaseVector::wrapInDictionary( + nullptr, makeIndices({0, 0, 0}), 1, inputMap); + + SelectivityVector rows(3); + std::vector args{ + dictInput, makeArrayVector({{0, 1, 2}, {0, 1, 2}, {0, 1, 2}})}; + auto result = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + // Last seen map will keep pointing to the original map since both have + // the same base. + checkStatus(true, false, inputMap); + + auto expectedResult = makeFlatVector({1000, 1000, 1000}); + test::assertEqualVectors(expectedResult, result); + verfyCachedContent(); + } + + { + auto constantInput = BaseVector::wrapInConstant(3, 0, inputMap); + + SelectivityVector rows(3); + std::vector args{ + constantInput, + makeArrayVector({{0, 1, 2}, {0, 1, 2}, {0, 1, 2}})}; + auto result = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + // Last seen map will keep pointing to the original map since both have + // the same base. + checkStatus(true, false, inputMap); + + auto expectedResult = makeFlatVector({1000, 1000, 1000}); + test::assertEqualVectors(expectedResult, result); + verfyCachedContent(); + } + + // Pass a different map, caching will be disabled. + { + args[0] = makeMapVector({0}, keysVector, valuesVector); + auto result = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(false, true, nullptr); + test::assertEqualVectors(result, result1); + } + + { + args[0] = makeMapVector({0}, keysVector, valuesVector); + auto result = mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(false, true, nullptr); + test::assertEqualVectors(result, result1); + } + + for (int i = 0; i < 999; i += 3) { + // [0, 1, 2] -> 0 + // [2, 3, 4] -> 1 + // ... + keys.push_back({i * 2, i * 2 + 1, i * 2 + 2}); + values.push_back(i); + } + + for (int i = 0; i < 30; i += 3) { + // [0, 1, 2] -> 0 + // [3, 4, 5] -> 3 + // ... + keys.push_back({i, i + 1, i + 2}); + values.push_back(i); + } + + args[0] = makeMapVector({0, 333, 666}, keysVector, valuesVector); + auto resultWithMoreVectors = + mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(false, true, nullptr); + + auto resultWithMoreVectors1 = + mapSubscriptWithCaching.applyMap(rows, args, evalCtx); + checkStatus(false, true, nullptr); + test::assertEqualVectors(resultWithMoreVectors, resultWithMoreVectors1); +} diff --git a/velox/vector/fuzzer/VectorFuzzer.cpp b/velox/vector/fuzzer/VectorFuzzer.cpp index 7708b56b60015..d8fcd38d180cb 100644 --- a/velox/vector/fuzzer/VectorFuzzer.cpp +++ b/velox/vector/fuzzer/VectorFuzzer.cpp @@ -135,9 +135,10 @@ Timestamp randTimestamp(FuzzerGenerator& rng, VectorFuzzer::Options opts) { size_t getElementsVectorLength( const VectorFuzzer::Options& opts, vector_size_t size) { - if (opts.containerVariableLength == false && - size * opts.containerLength > opts.complexElementsMaxSize) { - VELOX_USER_FAIL( + if (!opts.containerVariableLength) { + VELOX_USER_CHECK_LE( + size * opts.containerLength, + opts.complexElementsMaxSize, "Requested fixed opts.containerVariableLength can't be satisfied: " "increase opts.complexElementsMaxSize, reduce opts.containerLength" " or make opts.containerVariableLength=true"); From 2796200da36c7d033d45066e00ed09e96eb6c614 Mon Sep 17 00:00:00 2001 From: Ke Date: Thu, 22 Aug 2024 01:04:48 -0700 Subject: [PATCH 06/24] Add complex type inputs to array_except, array_intersect and arrays_overlap (#10743) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10743 Reviewed By: kevinwilfong Differential Revision: D61246379 Pulled By: kewang1024 fbshipit-source-id: 1f2fb793a43e18abe51bfba97ba1558b8204c563 --- .../prestosql/ArrayIntersectExcept.cpp | 125 ++++++++++++++---- .../prestosql/tests/ArrayExceptTest.cpp | 64 +++++++++ .../prestosql/tests/ArrayIntersectTest.cpp | 59 +++++++++ .../prestosql/tests/ArraysOverlapTest.cpp | 65 +++++++++ velox/type/Type.h | 64 +++++++++ 5 files changed, 350 insertions(+), 27 deletions(-) diff --git a/velox/functions/prestosql/ArrayIntersectExcept.cpp b/velox/functions/prestosql/ArrayIntersectExcept.cpp index c13c812914dc3..2dfcef7f3feac 100644 --- a/velox/functions/prestosql/ArrayIntersectExcept.cpp +++ b/velox/functions/prestosql/ArrayIntersectExcept.cpp @@ -20,12 +20,23 @@ namespace facebook::velox::functions { namespace { +constexpr vector_size_t kInitialSetSize{128}; + template struct SetWithNull { SetWithNull(vector_size_t initialSetSize = kInitialSetSize) { set.reserve(initialSetSize); } + bool insert(const DecodedVector* decodedElements, vector_size_t offset) { + return set.insert(decodedElements->valueAt(offset)).second; + } + + size_t count(const DecodedVector* decodedElements, vector_size_t offset) + const { + return set.count(decodedElements->valueAt(offset)); + } + void reset() { set.clear(); hasNull = false; @@ -37,7 +48,65 @@ struct SetWithNull { util::floating_point::HashSetNaNAware set; bool hasNull{false}; - static constexpr vector_size_t kInitialSetSize{128}; +}; + +struct ComplexTypeEntry { + const uint64_t hash; + const BaseVector* baseVector; + const vector_size_t index; +}; + +template <> +struct SetWithNull { + struct Hash { + size_t operator()(const ComplexTypeEntry& entry) const { + return entry.hash; + } + }; + + struct EqualTo { + bool operator()(const ComplexTypeEntry& left, const ComplexTypeEntry& right) + const { + return left.baseVector + ->equalValueAt( + right.baseVector, + left.index, + right.index, + CompareFlags::NullHandlingMode::kNullAsValue) + .value(); + } + }; + + folly::F14FastSet set; + bool hasNull{false}; + + SetWithNull(vector_size_t initialSetSize = kInitialSetSize) { + set.reserve(initialSetSize); + } + + bool insert(const DecodedVector* decodedElements, vector_size_t offset) { + const auto vector = decodedElements->base(); + const auto index = decodedElements->index(offset); + const uint64_t hash = vector->hashValueAt(index); + return set.insert(ComplexTypeEntry{hash, vector, index}).second; + } + + size_t count(const DecodedVector* decodedElements, vector_size_t offset) + const { + const auto vector = decodedElements->base(); + const auto index = decodedElements->index(offset); + const uint64_t hash = vector->hashValueAt(index); + return set.count(ComplexTypeEntry{hash, vector, index}); + } + + void reset() { + set.clear(); + hasNull = false; + } + + bool empty() const { + return !hasNull && set.empty(); + } }; // Generates a set based on the elements of an ArrayVector. Note that we take @@ -57,7 +126,7 @@ void generateSet( if (arrayElements->isNullAt(i)) { rightSet.hasNull = true; } else { - rightSet.set.insert(arrayElements->template valueAt(i)); + rightSet.insert(arrayElements, i); } } } @@ -186,19 +255,17 @@ class ArrayIntersectExceptFunction : public exec::VectorFunction { } } } else { - auto val = decodedLeftElements->valueAt(i); // For array_intersect, add the element if it is found (not found // for array_except) in the right-hand side, and wasn't added already // (check outputSet). bool addValue = false; if constexpr (isIntersect) { - addValue = rightSet.set.count(val) > 0; + addValue = rightSet.count(decodedLeftElements, i) > 0; } else { - addValue = rightSet.set.count(val) == 0; + addValue = rightSet.count(decodedLeftElements, i) == 0; } if (addValue) { - auto it = outputSet.set.insert(val); - if (it.second) { + if (outputSet.insert(decodedLeftElements, i)) { rawNewIndices[indicesCursor++] = i; } } @@ -294,7 +361,7 @@ class ArraysOverlapFunction : public exec::VectorFunction { hasNull = true; continue; } - if (rightSet.set.count(decodedLeftElements->valueAt(i)) > 0) { + if (rightSet.count(decodedLeftElements, i) > 0) { // Found an overlapping element. Add to result set. resultBoolVector->set(row, true); return; @@ -396,7 +463,10 @@ SetWithNull validateConstantVectorAndGenerateSet( template std::shared_ptr createTypedArraysIntersectExcept( const std::vector& inputArgs) { - using T = typename TypeTraits::NativeType; + using T = std::conditional_t< + TypeTraits::isPrimitiveType, + typename TypeTraits::NativeType, + ComplexTypeEntry>; VELOX_CHECK_EQ(inputArgs.size(), 2); BaseVector* rhs = inputArgs[1].constantValue.get(); @@ -424,7 +494,7 @@ std::shared_ptr createArrayIntersect( validateMatchingArrayTypes(inputArgs, name, 2); auto elementType = inputArgs.front().type->childAt(0); - return VELOX_DYNAMIC_SCALAR_TEMPLATE_TYPE_DISPATCH( + return VELOX_DYNAMIC_TEMPLATE_TYPE_DISPATCH( createTypedArraysIntersectExcept, /* isIntersect */ true, elementType->kind(), @@ -438,7 +508,7 @@ std::shared_ptr createArrayExcept( validateMatchingArrayTypes(inputArgs, name, 2); auto elementType = inputArgs.front().type->childAt(0); - return VELOX_DYNAMIC_SCALAR_TEMPLATE_TYPE_DISPATCH( + return VELOX_DYNAMIC_TEMPLATE_TYPE_DISPATCH( createTypedArraysIntersectExcept, /* isIntersect */ false, elementType->kind(), @@ -446,18 +516,15 @@ std::shared_ptr createArrayExcept( } std::vector> signatures( - const std::string& returnTypeTemplate) { - std::vector> signatures; - for (const auto& type : exec::primitiveTypeNames()) { - signatures.push_back( - exec::FunctionSignatureBuilder() - .returnType( - fmt::format(fmt::runtime(returnTypeTemplate.c_str()), type)) - .argumentType(fmt::format("array({})", type)) - .argumentType(fmt::format("array({})", type)) - .build()); - } - return signatures; + const std::string& returnType) { + return std::vector>{ + exec::FunctionSignatureBuilder() + .typeVariable("T") + .returnType(returnType) + .argumentType("array(T)") + .argumentType("array(T)") + .build(), + }; } template @@ -466,7 +533,11 @@ const std::shared_ptr createTypedArraysOverlap( VELOX_CHECK_EQ(inputArgs.size(), 2); auto left = inputArgs[0].constantValue.get(); auto right = inputArgs[1].constantValue.get(); - using T = typename TypeTraits::NativeType; + using T = std::conditional_t< + TypeTraits::isPrimitiveType, + typename TypeTraits::NativeType, + ComplexTypeEntry>; + if (left == nullptr && right == nullptr) { return std::make_shared>(); } @@ -484,7 +555,7 @@ std::shared_ptr createArraysOverlapFunction( validateMatchingArrayTypes(inputArgs, name, 2); auto elementType = inputArgs.front().type->childAt(0); - return VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH( + return VELOX_DYNAMIC_TYPE_DISPATCH( createTypedArraysOverlap, elementType->kind(), inputArgs); } } // namespace @@ -496,11 +567,11 @@ VELOX_DECLARE_STATEFUL_VECTOR_FUNCTION( VELOX_DECLARE_STATEFUL_VECTOR_FUNCTION( udf_array_intersect, - signatures("array({})"), + signatures("array(T)"), createArrayIntersect); VELOX_DECLARE_STATEFUL_VECTOR_FUNCTION( udf_array_except, - signatures("array({})"), + signatures("array(T)"), createArrayExcept); } // namespace facebook::velox::functions diff --git a/velox/functions/prestosql/tests/ArrayExceptTest.cpp b/velox/functions/prestosql/tests/ArrayExceptTest.cpp index aef7de5d2e161..2a23e14788f3f 100644 --- a/velox/functions/prestosql/tests/ArrayExceptTest.cpp +++ b/velox/functions/prestosql/tests/ArrayExceptTest.cpp @@ -24,6 +24,9 @@ using namespace facebook::velox::functions::test; namespace { +template +using Pair = std::pair>; + class ArrayExceptTest : public FunctionBaseTest { protected: void testExpr( @@ -278,6 +281,67 @@ TEST_F(ArrayExceptTest, varbinary) { testExpr(expected, "array_except(c0, c1)", {right, left}); } +TEST_F(ArrayExceptTest, complexTypeArray) { + auto left = makeNestedArrayVectorFromJson({ + "[null, [1, 2, 3], [null, null]]", + "[[1], [2], []]", + "[[1, null, 3]]", + "[[1, null, 3]]", + }); + + auto right = makeNestedArrayVectorFromJson({ + "[[1, 2, 3]]", + "[[1]]", + "[[1, null, 3], [1, 2]]", + "[[1, null, 3, null]]", + }); + + auto expected = makeNestedArrayVectorFromJson({ + "[null, [null, null]]", + "[[2], []]", + "[]", + "[[1, null, 3]]", + }); + testExpr(expected, "array_except(c0, c1)", {left, right}); +} + +TEST_F(ArrayExceptTest, complexTypeMap) { + std::vector> a{{"blue", 1}, {"red", 2}}; + std::vector> b{{"blue", 2}, {"red", 2}}; + std::vector> c{{"green", std::nullopt}}; + std::vector> d{{"yellow", 4}, {"purple", 5}}; + std::vector>>> leftData{ + {b, a}, {b}, {c, a}}; + std::vector>>> rightData{ + {a, b}, {}, {a}}; + std::vector>>> expectedData{ + {}, {b}, {c}}; + + auto left = makeArrayOfMapVector(leftData); + auto right = makeArrayOfMapVector(rightData); + auto expected = makeArrayOfMapVector(expectedData); + + testExpr(expected, "array_except(c0, c1)", {left, right}); +} + +TEST_F(ArrayExceptTest, complexTypeRow) { + RowTypePtr rowType = ROW({INTEGER(), VARCHAR()}); + + using ArrayOfRow = std::vector>>; + std::vector leftData = { + {{{1, "red"}}, {{2, "blue"}}, {{3, "green"}}}, + {{{1, "red"}}, {{2, "blue"}}, {}}, + {{{1, "red"}}, std::nullopt, std::nullopt}}; + std::vector rightData = { + {{{2, "blue"}}, {{1, "red"}}}, {{}, {{1, "green"}}}, {{{1, "red"}}}}; + std::vector expectedData = { + {{{3, "green"}}}, {{{1, "red"}}, {{2, "blue"}}}, {std::nullopt}}; + auto left = makeArrayOfRowVector(leftData, rowType); + auto right = makeArrayOfRowVector(rightData, rowType); + auto expected = makeArrayOfRowVector(expectedData, rowType); + testExpr(expected, "array_except(c0, c1)", {left, right}); +} + // When one of the arrays is constant. TEST_F(ArrayExceptTest, constant) { auto array1 = makeNullableArrayVector({ diff --git a/velox/functions/prestosql/tests/ArrayIntersectTest.cpp b/velox/functions/prestosql/tests/ArrayIntersectTest.cpp index 8260f9fbdf9c2..3ae43b8acb815 100644 --- a/velox/functions/prestosql/tests/ArrayIntersectTest.cpp +++ b/velox/functions/prestosql/tests/ArrayIntersectTest.cpp @@ -24,6 +24,9 @@ using namespace facebook::velox::functions::test; namespace { +template +using Pair = std::pair>; + class ArrayIntersectTest : public FunctionBaseTest { protected: void testExpr( @@ -257,6 +260,62 @@ TEST_F(ArrayIntersectTest, varbinary) { testExpr(expected, "array_intersect(c0, c1)", {right, left}); } +TEST_F(ArrayIntersectTest, complexTypeArray) { + auto left = makeNestedArrayVectorFromJson({ + "[null, [1, 2, 3], [null, null]]", + "[[1], [2], []]", + "[[1, null, 3]]", + "[[1, null, 3]]", + }); + + auto right = makeNestedArrayVectorFromJson({ + "[[1, 2, 3]]", + "[[1]]", + "[[1, null, 3], [1, 2]]", + "[[1, null, 3, null]]", + }); + + auto expected = makeNestedArrayVectorFromJson( + {"[[1, 2, 3]]", "[[1]]", "[[1, null, 3]]", "[]"}); + testExpr(expected, "array_intersect(c0, c1)", {left, right}); +} + +TEST_F(ArrayIntersectTest, complexTypeMap) { + std::vector> a{{"blue", 1}, {"red", 2}}; + std::vector> b{{"green", std::nullopt}}; + std::vector> c{{"yellow", 4}, {"purple", 5}}; + std::vector>>> leftData{ + {b, a}, {b}, {c, a}}; + std::vector>>> rightData{ + {a, b}, {}, {a}}; + std::vector>>> expectedData{ + {b, a}, {}, {a}}; + + auto left = makeArrayOfMapVector(leftData); + auto right = makeArrayOfMapVector(rightData); + auto expected = makeArrayOfMapVector(expectedData); + + testExpr(expected, "array_intersect(c0, c1)", {left, right}); +} + +TEST_F(ArrayIntersectTest, complexTypeRow) { + RowTypePtr rowType = ROW({INTEGER(), VARCHAR()}); + + using ArrayOfRow = std::vector>>; + std::vector leftData = { + {{{1, "red"}}, {{2, "blue"}}, {{3, "green"}}}, + {{{1, "red"}}, {{2, "blue"}}, {}}, + {{{1, "red"}}, std::nullopt, std::nullopt}}; + std::vector rightData = { + {{{2, "blue"}}, {{1, "red"}}}, {{}, {{1, "green"}}}, {{{1, "red"}}}}; + std::vector expectedData = { + {{{1, "red"}}, {{2, "blue"}}}, {{}}, {{{1, "red"}}}}; + auto left = makeArrayOfRowVector(leftData, rowType); + auto right = makeArrayOfRowVector(rightData, rowType); + auto expected = makeArrayOfRowVector(expectedData, rowType); + testExpr(expected, "array_intersect(c0, c1)", {left, right}); +} + // When one of the arrays is constant. TEST_F(ArrayIntersectTest, constant) { auto array1 = makeNullableArrayVector({ diff --git a/velox/functions/prestosql/tests/ArraysOverlapTest.cpp b/velox/functions/prestosql/tests/ArraysOverlapTest.cpp index 002a6175cfcc5..002490be3ab8a 100644 --- a/velox/functions/prestosql/tests/ArraysOverlapTest.cpp +++ b/velox/functions/prestosql/tests/ArraysOverlapTest.cpp @@ -23,6 +23,9 @@ using namespace facebook::velox::test; using namespace facebook::velox::functions::test; namespace { +template +using Pair = std::pair>; + class ArraysOverlapTest : public FunctionBaseTest { protected: void testExpr( @@ -210,6 +213,68 @@ TEST_F(ArraysOverlapTest, longStrings) { testExpr(expected, "arrays_overlap(C1, C0)", {array1, array2}); } +TEST_F(ArraysOverlapTest, complexTypeArray) { + auto left = makeNestedArrayVectorFromJson({ + "[null, [1, 2, 3], [null, null]]", + "[[1], [2], []]", + "[[1, null, 3]]", + "[[1, null, 3]]", + "[null]", + }); + + auto right = makeNestedArrayVectorFromJson({ + "[[1, 2, 3]]", + "[[1]]", + "[[1, 2]]", + "[[1, null, 3]]", + "[[]]", + }); + + auto expected = + makeNullableFlatVector({true, true, false, true, std::nullopt}); + testExpr(expected, "arrays_overlap(c0, c1)", {left, right}); +} + +TEST_F(ArraysOverlapTest, complexTypeMap) { + std::vector> a{{"blue", 1}, {"red", 2}}; + std::vector> b{{"green", std::nullopt}}; + std::vector> c{{"yellow", 4}, {"purple", 5}}; + + std::vector>>> leftData{ + {b, a}, {b}, {c, a}}; + std::vector>>> rightData{ + {a, b}, {}, {b}}; + + auto left = makeArrayOfMapVector(leftData); + auto right = makeArrayOfMapVector(rightData); + auto expected = makeNullableFlatVector({true, false, false}); + + testExpr(expected, "arrays_overlap(c0, c1)", {left, right}); +} + +TEST_F(ArraysOverlapTest, complexTypeRow) { + RowTypePtr rowType = ROW({INTEGER(), VARCHAR()}); + + using ArrayOfRow = std::vector>>; + std::vector leftData = { + {{{1, "red"}}, {{2, "blue"}}, {{3, "green"}}}, + {{{1, "red"}}, {{2, "blue"}}, std::nullopt}, + {{{1, "red"}}, std::nullopt, std::nullopt}, + {{{1, "red"}}, {{}}, {{}}}}; + std::vector rightData = { + {{{2, "blue"}}, {{1, "red"}}}, + {{{1, "green"}}}, + {{{1, "red"}}}, + {{{2, "red"}}}}; + + auto left = makeArrayOfRowVector(leftData, rowType); + auto right = makeArrayOfRowVector(rightData, rowType); + auto expected = + makeNullableFlatVector({true, std::nullopt, true, false}); + + testExpr(expected, "arrays_overlap(c0, c1)", {left, right}); +} + //// When one of the arrays is constant. TEST_F(ArraysOverlapTest, constant) { auto array1 = makeNullableArrayVector({ diff --git a/velox/type/Type.h b/velox/type/Type.h index 8a004dec043a8..b7bc6bbdbd3d7 100644 --- a/velox/type/Type.h +++ b/velox/type/Type.h @@ -1502,6 +1502,70 @@ std::shared_ptr OPAQUE() { } \ }() +#define VELOX_DYNAMIC_TEMPLATE_TYPE_DISPATCH(TEMPLATE_FUNC, T, typeKind, ...) \ + [&]() { \ + switch (typeKind) { \ + case ::facebook::velox::TypeKind::BOOLEAN: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::INTEGER: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::TINYINT: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::SMALLINT: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::BIGINT: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::HUGEINT: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::REAL: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::DOUBLE: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::VARCHAR: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::VARBINARY: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::TIMESTAMP: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::MAP: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::ARRAY: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + case ::facebook::velox::TypeKind::ROW: { \ + return TEMPLATE_FUNC( \ + __VA_ARGS__); \ + } \ + default: \ + VELOX_FAIL("not a known type kind: {}", mapTypeKindToName(typeKind)); \ + } \ + }() + #define VELOX_DYNAMIC_SCALAR_TYPE_DISPATCH_ALL(TEMPLATE_FUNC, typeKind, ...) \ [&]() { \ if ((typeKind) == ::facebook::velox::TypeKind::UNKNOWN) { \ From 024d10d5dfc513b455423a2632a09b4501356e02 Mon Sep 17 00:00:00 2001 From: Ke Date: Thu, 22 Aug 2024 01:23:23 -0700 Subject: [PATCH 07/24] Support UNKNOWN type in map_concat function (#10795) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10795 Reviewed By: zacw7 Differential Revision: D61609911 Pulled By: kewang1024 fbshipit-source-id: ea9faa297cf70c7206f530d5b861dc15c4c7b9fd --- velox/functions/lib/MapConcat.cpp | 2 +- velox/functions/lib/tests/MapConcatTest.cpp | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/velox/functions/lib/MapConcat.cpp b/velox/functions/lib/MapConcat.cpp index 81c415f693c3e..aac38fbc93154 100644 --- a/velox/functions/lib/MapConcat.cpp +++ b/velox/functions/lib/MapConcat.cpp @@ -157,7 +157,7 @@ class MapConcatFunction : public exec::VectorFunction { static std::vector> signatures() { // map(K,V), map(K,V), ... -> map(K,V) return {exec::FunctionSignatureBuilder() - .knownTypeVariable("K") + .typeVariable("K") .typeVariable("V") .returnType("map(K,V)") .argumentType("map(K,V)") diff --git a/velox/functions/lib/tests/MapConcatTest.cpp b/velox/functions/lib/tests/MapConcatTest.cpp index 9a46d90b5c946..c269391e1252c 100644 --- a/velox/functions/lib/tests/MapConcatTest.cpp +++ b/velox/functions/lib/tests/MapConcatTest.cpp @@ -283,3 +283,14 @@ TEST_F(MapConcatTest, nullEntry) { ASSERT_EQ(result->size(), 1); EXPECT_EQ(result->sizeAt(0), 0); } + +TEST_F(MapConcatTest, unknownType) { + // MAP_CONCAT(MAP[], MAP[]) + auto emptyMapVector = + VectorTestBase::makeMapVector({{}}); + auto expectedMap = + VectorTestBase::makeMapVector({{}}); + auto result = evaluate( + "map_concat(c0, c1)", makeRowVector({emptyMapVector, emptyMapVector})); + facebook::velox::test::assertEqualVectors(expectedMap, result); +} From 9387a9d49944cc0b7419251e1a22a884ed1135ce Mon Sep 17 00:00:00 2001 From: Jialiang Tan Date: Thu, 22 Aug 2024 03:08:17 -0700 Subject: [PATCH 08/24] Let shrinkCapacity not shrink free cap (#10780) Summary: Shrinking free capacity when calling global shrinkCapacity method does not help with system memory pressure. Remove the free capacity shrink at the beginning of the method to help with actual memory pressure release as this method is only called by push back mechanism when there is memory pressure. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10780 Reviewed By: xiaoxmeng Differential Revision: D61496392 Pulled By: tanjialiang fbshipit-source-id: a178f45c904ba3b21af3128d19beedf47360119a --- velox/common/memory/MemoryArbitrator.h | 26 ++- velox/common/memory/SharedArbitrator.cpp | 39 ++--- .../memory/tests/MemoryArbitratorTest.cpp | 24 --- .../memory/tests/MockSharedArbitratorTest.cpp | 155 +++--------------- 4 files changed, 61 insertions(+), 183 deletions(-) diff --git a/velox/common/memory/MemoryArbitrator.h b/velox/common/memory/MemoryArbitrator.h index d4b266208f4a1..20b50e6012886 100644 --- a/velox/common/memory/MemoryArbitrator.h +++ b/velox/common/memory/MemoryArbitrator.h @@ -126,15 +126,23 @@ class MemoryArbitrator { /// pool. The function returns the actual freed capacity from 'pool'. virtual uint64_t shrinkCapacity(MemoryPool* pool, uint64_t targetBytes) = 0; - /// Invoked by the memory manager to shrink memory capacity from memory pools - /// by reclaiming free and used memory. The freed memory capacity is given - /// back to the arbitrator. If 'targetBytes' is zero, then try to reclaim all - /// the memory from 'pools'. The function returns the actual freed memory - /// capacity in bytes. If 'allowSpill' is true, it reclaims the used memory by - /// spilling. If 'allowAbort' is true, it reclaims the used memory by aborting - /// the queries with the most memory usage. If both are true, it first - /// reclaims the used memory by spilling and then abort queries to reach the - /// reclaim target. + /// Invoked by the memory manager to globally shrink memory from + /// memory pools by reclaiming only used memory, to reduce system memory + /// pressure. The freed memory capacity is given back to the arbitrator. If + /// 'targetBytes' is zero, then try to reclaim all the memory from 'pools'. + /// The function returns the actual freed memory capacity in bytes. If + /// 'allowSpill' is true, it reclaims the used memory by spilling. If + /// 'allowAbort' is true, it reclaims the used memory by aborting the queries + /// with the most memory usage. If both are true, it first reclaims the used + /// memory by spilling and then abort queries to reach the reclaim target. + /// + /// NOTE: The actual reclaimed used memory (hence system memory) may be less + /// than 'targetBytes' due to the accounting of free capacity reclaimed. This + /// is okay because when this method is called, system is normally under + /// memory pressure, and there normally isn't much free capacity to reclaim. + /// So the reclaimed used memory in this case should be very close to + /// 'targetBytes' if enough used memory is reclaimable. We should improve this + /// in the future. virtual uint64_t shrinkCapacity( uint64_t targetBytes, bool allowSpill = true, diff --git a/velox/common/memory/SharedArbitrator.cpp b/velox/common/memory/SharedArbitrator.cpp index fc785576c9e2d..cd51312f1bcf4 100644 --- a/velox/common/memory/SharedArbitrator.cpp +++ b/velox/common/memory/SharedArbitrator.cpp @@ -496,37 +496,38 @@ uint64_t SharedArbitrator::shrinkCapacity( ArbitrationOperation op(requestBytes); ScopedArbitration scopedArbitration(this, &op); - uint64_t fastReclaimTargetBytes = - std::max(memoryPoolTransferCapacity_, requestBytes); - std::lock_guard exclusiveLock(arbitrationLock_); getCandidates(&op); - uint64_t freedBytes = - reclaimFreeMemoryFromCandidates(&op, fastReclaimTargetBytes, false); - auto freeGuard = folly::makeGuard([&]() { - // Returns the freed memory capacity back to the arbitrator. - if (freedBytes > 0) { - incrementFreeCapacity(freedBytes); - } - }); - if (freedBytes >= op.requestBytes) { - return freedBytes; - } + uint64_t unreturnedFreedBytes{0}; + uint64_t totalFreedBytes{0}; + RECORD_METRIC_VALUE(kMetricArbitratorSlowGlobalArbitrationCount); + if (allowSpill) { - reclaimUsedMemoryFromCandidatesBySpill(&op, freedBytes); - if (freedBytes >= op.requestBytes) { - return freedBytes; + reclaimUsedMemoryFromCandidatesBySpill(&op, unreturnedFreedBytes); + totalFreedBytes += unreturnedFreedBytes; + if (unreturnedFreedBytes > 0) { + incrementFreeCapacity(unreturnedFreedBytes); + unreturnedFreedBytes = 0; + } + if (totalFreedBytes >= op.requestBytes) { + return totalFreedBytes; } if (allowAbort) { // Candidate stats may change after spilling. getCandidates(&op); } } + if (allowAbort) { - reclaimUsedMemoryFromCandidatesByAbort(&op, freedBytes); + reclaimUsedMemoryFromCandidatesByAbort(&op, unreturnedFreedBytes); + totalFreedBytes += unreturnedFreedBytes; + if (unreturnedFreedBytes > 0) { + incrementFreeCapacity(unreturnedFreedBytes); + unreturnedFreedBytes = 0; + } } - return freedBytes; + return totalFreedBytes; } void SharedArbitrator::testingFreeCapacity(uint64_t capacity) { diff --git a/velox/common/memory/tests/MemoryArbitratorTest.cpp b/velox/common/memory/tests/MemoryArbitratorTest.cpp index 30ff9acda2868..848ab33f79603 100644 --- a/velox/common/memory/tests/MemoryArbitratorTest.cpp +++ b/velox/common/memory/tests/MemoryArbitratorTest.cpp @@ -285,30 +285,6 @@ TEST_F(MemoryArbitrationTest, reservedCapacityFreeByPoolRelease) { ASSERT_EQ(arbitrator->stats().freeCapacityBytes, 6 << 20); } -TEST_F(MemoryArbitrationTest, reservedCapacityFreeByPoolShrink) { - MemoryManagerOptions options; - options.arbitratorKind = "SHARED"; - options.arbitratorReservedCapacity = 4 << 20; - options.arbitratorCapacity = 8 << 20; - options.allocatorCapacity = options.arbitratorCapacity; - options.memoryPoolInitCapacity = 2 << 20; - options.memoryPoolReservedCapacity = 1 << 20; - - MemoryManager manager(options); - auto* arbitrator = manager.arbitrator(); - const int numPools = 6; - std::vector> pools; - for (int i = 0; i < numPools; ++i) { - pools.push_back(manager.addRootPool("", kMaxMemory)); - ASSERT_GE(pools.back()->capacity(), 1 << 20); - } - ASSERT_EQ(arbitrator->stats().freeCapacityBytes, 0); - pools.push_back(manager.addRootPool("", kMaxMemory)); - - ASSERT_GE(pools.back()->capacity(), 0); - ASSERT_EQ(arbitrator->shrinkCapacity(1 << 20), 2 << 20); -} - TEST_F(MemoryArbitrationTest, arbitratorStats) { const MemoryArbitrator::Stats emptyStats; ASSERT_TRUE(emptyStats.empty()); diff --git a/velox/common/memory/tests/MockSharedArbitratorTest.cpp b/velox/common/memory/tests/MockSharedArbitratorTest.cpp index 9f0356f8ca100..3b701391fab9c 100644 --- a/velox/common/memory/tests/MockSharedArbitratorTest.cpp +++ b/velox/common/memory/tests/MockSharedArbitratorTest.cpp @@ -795,10 +795,12 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { for (const auto& testTask : testTasks) { tasksOss << "["; tasksOss << testTask.debugString(); - tasksOss << "], "; + tasksOss << "], \n"; } return fmt::format( - "testTasks: [{}], targetBytes: {}, expectedFreedBytes: {}, expectedFreeCapacity: {}, expectedReservedFreeCapacity: {}, allowSpill: {}, allowAbort: {}", + "testTasks: \n[{}], \ntargetBytes: {}, \nexpectedFreedBytes: {}, " + "\nexpectedFreeCapacity: {}, \nexpectedReservedFreeCapacity: {}, \n" + "allowSpill: {}, \nallowAbort: {}", tasksOss.str(), succinctBytes(targetBytes), succinctBytes(expectedFreedBytes), @@ -808,28 +810,6 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { allowAbort); } } testSettings[] = { - {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolReserveCapacity, false}}, - 0, - 18 << 20, - 24 << 20, - reservedMemoryCapacity, - true, - false}, - - {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, - 0, - 18 << 20, - 24 << 20, - reservedMemoryCapacity, - true, - false}, - {{{memoryPoolInitCapacity, false, memoryPoolInitCapacity, @@ -843,9 +823,9 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { memoryPoolReserveCapacity, false}}, 0, - 12 << 20, - 18 << 20, - reservedMemoryCapacity, + 0, + 6 << 20, + 6 << 20, true, false}, @@ -862,8 +842,8 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { memoryPoolReserveCapacity, false}}, 0, - 20 << 20, - 26 << 20, + 8 << 20, + 14 << 20, reservedMemoryCapacity, true, false}, @@ -881,9 +861,9 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { memoryPoolReserveCapacity, false}}, 0, - 12 << 20, - 18 << 20, - reservedMemoryCapacity, + 0, + 6 << 20, + 6 << 20, false, false}, @@ -900,9 +880,9 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { memoryPoolReserveCapacity, false}}, 0, - 12 << 20, - 18 << 20, - reservedMemoryCapacity, + 0, + 6 << 20, + 6 << 20, true, false}, @@ -941,26 +921,15 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, 16 << 20, - 16 << 20, - 22 << 20, - reservedMemoryCapacity, + 0, + 6 << 20, + 6 << 20, false, false}, {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, - 16 << 20, - 16 << 20, - 22 << 20, - reservedMemoryCapacity, - true, - false}, - - {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, + {memoryPoolInitCapacity, false, 1 << 10, memoryPoolInitCapacity, true}, + {memoryPoolInitCapacity, false, 1 << 10, memoryPoolInitCapacity, true}, {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, 16 << 20, 16 << 20, @@ -970,34 +939,12 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { true}, {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, - 14 << 20, - 14 << 20, - 20 << 20, - reservedMemoryCapacity, - false, - false}, - - {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, - 12 << 20, - 12 << 20, - 18 << 20, - reservedMemoryCapacity, - true, - false}, - - {{{memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, true, 0, memoryPoolInitCapacity, false}, - {memoryPoolInitCapacity, false, 0, memoryPoolInitCapacity, false}, + {memoryPoolInitCapacity, false, 1 << 10, memoryPoolInitCapacity, true}, + {memoryPoolInitCapacity, false, 1 << 10, memoryPoolInitCapacity, true}, {memoryPoolInitCapacity, true, 0, memoryPoolReserveCapacity, false}}, 14 << 20, - 14 << 20, - 20 << 20, + 16 << 20, + 22 << 20, reservedMemoryCapacity, true, true}, @@ -1029,33 +976,6 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { true, false}, - {{{memoryPoolInitCapacity, - true, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - true, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - false, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - true, - memoryPoolReserveCapacity, - memoryPoolReserveCapacity, - false}}, - 14 << 20, - 0, - 6 << 20, - 6 << 20, - false, - false}, - {{{memoryPoolInitCapacity, true, memoryPoolInitCapacity, @@ -1083,33 +1003,6 @@ TEST_F(MockSharedArbitrationTest, shrinkPools) { false, true}, - {{{memoryPoolInitCapacity, - false, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - false, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - false, - memoryPoolInitCapacity, - memoryPoolInitCapacity, - false}, - {memoryPoolInitCapacity, - false, - memoryPoolReserveCapacity, - memoryPoolReserveCapacity, - false}}, - 14 << 20, - 0, - 6 << 20, - 6 << 20, - false, - false}, - {{{memoryPoolInitCapacity, false, memoryPoolInitCapacity, From 3325e9666c8de70c3a94846b0ce2ac7a2250b9a1 Mon Sep 17 00:00:00 2001 From: Jimmy Lu Date: Thu, 22 Aug 2024 06:53:09 -0700 Subject: [PATCH 09/24] Fix null compacting for ARRAY and MAP selective column reader (#10805) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10805 For array/map column readers, we were using `resultNulls_` as nulls. This is working correctly in most of the cases, except when during `read` call, we decide to reuse reader nulls, but later there is another filter shrink the row set, and cause that we need to compact the reader nulls into result nulls. Although we set the bits in `resultNulls_` correctly, the `returnReaderNulls_` is not reset in this case and we still use the reader nulls in result vector. Fix this by reusing the same implementation in struct column reader on all complex type column readers. Also clean up the unused `compactComplexValues` method. Reviewed By: HuamengJiang Differential Revision: D61620252 fbshipit-source-id: b5d35c7f94a622a8f9365e2fff7e89e441d113b0 --- velox/dwio/common/SelectiveColumnReader.cpp | 25 +++++++++ velox/dwio/common/SelectiveColumnReader.h | 13 ++--- .../common/SelectiveColumnReaderInternal.h | 52 ------------------- .../common/SelectiveRepeatedColumnReader.cpp | 7 +-- .../common/SelectiveStructColumnReader.cpp | 10 +--- .../dwio/common/SelectiveStructColumnReader.h | 3 +- velox/exec/tests/TableScanTest.cpp | 25 +++++++++ 7 files changed, 59 insertions(+), 76 deletions(-) diff --git a/velox/dwio/common/SelectiveColumnReader.cpp b/velox/dwio/common/SelectiveColumnReader.cpp index e8cebff97ebf5..278f09291355b 100644 --- a/velox/dwio/common/SelectiveColumnReader.cpp +++ b/velox/dwio/common/SelectiveColumnReader.cpp @@ -151,6 +151,31 @@ const uint64_t* SelectiveColumnReader::shouldMoveNulls(RowSet rows) { return moveFrom; } +void SelectiveColumnReader::setComplexNulls(RowSet rows, VectorPtr& result) + const { + if (!nullsInReadRange_) { + result->clearNulls(0, rows.size()); + return; + } + const bool dense = 1 + rows.back() == rows.size(); + auto& nulls = result->nulls(); + if (dense && + !(nulls && nulls->isMutable() && + nulls->capacity() >= bits::nbytes(rows.size()))) { + result->setNulls(nullsInReadRange_); + return; + } + auto* readerNulls = nullsInReadRange_->as(); + auto* resultNulls = result->mutableNulls(rows.size())->asMutable(); + if (dense) { + bits::copyBits(readerNulls, 0, resultNulls, 0, rows.size()); + return; + } + for (vector_size_t i = 0; i < rows.size(); ++i) { + bits::setBit(resultNulls, i, bits::isBitSet(readerNulls, rows[i])); + } +} + void SelectiveColumnReader::getIntValues( RowSet rows, const TypePtr& requestedType, diff --git a/velox/dwio/common/SelectiveColumnReader.h b/velox/dwio/common/SelectiveColumnReader.h index b5c96a278c177..e1190f19f1f38 100644 --- a/velox/dwio/common/SelectiveColumnReader.h +++ b/velox/dwio/common/SelectiveColumnReader.h @@ -548,17 +548,14 @@ class SelectiveColumnReader { template void compactScalarValues(RowSet rows, bool isFinal); - // Compacts values extracted for a complex type column with - // filter. The values for 'rows' are shifted to be consecutive at - // indices [0..rows.size() - 1]'. 'move' is a function that takes - // two indices source and target and moves the value at source to - // target. target is <= source for all calls. - template - void compactComplexValues(RowSet rows, Move move, bool isFinal); - template void upcastScalarValues(RowSet rows); + // For complex type column, we need to compact only nulls if the rows are + // shrinked. Child fields are handled recursively in their own column + // readers. + void setComplexNulls(RowSet rows, VectorPtr& result) const; + // Return the source null bits if compactScalarValues and upcastScalarValues // should move null flags. Return nullptr if nulls does not need to be moved. // Checks consistency of nulls-related state. diff --git a/velox/dwio/common/SelectiveColumnReaderInternal.h b/velox/dwio/common/SelectiveColumnReaderInternal.h index 01819fa41c31f..a5656053cfd10 100644 --- a/velox/dwio/common/SelectiveColumnReaderInternal.h +++ b/velox/dwio/common/SelectiveColumnReaderInternal.h @@ -306,58 +306,6 @@ inline int32_t sizeOfIntKind(TypeKind kind) { } } -template -void SelectiveColumnReader::compactComplexValues( - RowSet rows, - Move move, - bool isFinal) { - VELOX_CHECK_LE(rows.size(), outputRows_.size()); - VELOX_CHECK(!rows.empty()); - if (rows.size() == outputRows_.size()) { - return; - } - RowSet sourceRows; - // The row numbers corresponding to elements in 'values_' are in - // 'valueRows_' if values have been accessed before. Otherwise - // they are in 'outputRows_' if these are non-empty (there is a - // filter) and in 'inputRows_' otherwise. - if (!valueRows_.empty()) { - sourceRows = valueRows_; - } else if (!outputRows_.empty()) { - sourceRows = outputRows_; - } else { - sourceRows = inputRows_; - } - if (valueRows_.empty()) { - valueRows_.resize(rows.size()); - } - vector_size_t rowIndex = 0; - auto nextRow = rows[rowIndex]; - auto* moveNullsFrom = shouldMoveNulls(rows); - for (size_t i = 0; i < numValues_; i++) { - if (sourceRows[i] < nextRow) { - continue; - } - - VELOX_DCHECK(sourceRows[i] == nextRow); - // The value at i is moved to be the value at 'rowIndex'. - move(i, rowIndex); - if (moveNullsFrom && rowIndex != i) { - bits::setBit(rawResultNulls_, rowIndex, bits::isBitSet(moveNullsFrom, i)); - } - if (!isFinal) { - valueRows_[rowIndex] = nextRow; - } - rowIndex++; - if (rowIndex >= rows.size()) { - break; - } - nextRow = rows[rowIndex]; - } - numValues_ = rows.size(); - valueRows_.resize(numValues_); -} - template void SelectiveColumnReader::filterNulls( RowSet rows, diff --git a/velox/dwio/common/SelectiveRepeatedColumnReader.cpp b/velox/dwio/common/SelectiveRepeatedColumnReader.cpp index eae6e50758d51..4b7713433d6d5 100644 --- a/velox/dwio/common/SelectiveRepeatedColumnReader.cpp +++ b/velox/dwio/common/SelectiveRepeatedColumnReader.cpp @@ -153,9 +153,6 @@ void SelectiveRepeatedColumnReader::makeOffsetsAndSizes( rawOffsets[i] = nestedRowIndex; if (nulls && bits::isBitNull(nulls, row)) { rawSizes[i] = 0; - if (!returnReaderNulls_) { - bits::setNull(rawResultNulls_, i); - } anyNulls_ = true; } else { currentOffset += allLengths_[row]; @@ -240,7 +237,7 @@ void SelectiveListColumnReader::getValues(RowSet rows, VectorPtr* result) { prepareResult(*result, requestedType_, rows.size(), &memoryPool_); auto* resultArray = result->get()->asUnchecked(); makeOffsetsAndSizes(rows, *resultArray); - result->get()->setNulls(resultNulls()); + setComplexNulls(rows, *result); if (child_ && !nestedRows_.empty()) { auto& elements = resultArray->elements(); prepareStructResult(requestedType_->childAt(0), &elements); @@ -321,7 +318,7 @@ void SelectiveMapColumnReader::getValues(RowSet rows, VectorPtr* result) { prepareResult(*result, requestedType_, rows.size(), &memoryPool_); auto* resultMap = result->get()->asUnchecked(); makeOffsetsAndSizes(rows, *resultMap); - result->get()->setNulls(resultNulls()); + setComplexNulls(rows, *result); VELOX_CHECK( keyReader_ && elementReader_, "keyReader_ and elementReaer_ must exist in " diff --git a/velox/dwio/common/SelectiveStructColumnReader.cpp b/velox/dwio/common/SelectiveStructColumnReader.cpp index e0509810fc871..7df3242bae989 100644 --- a/velox/dwio/common/SelectiveStructColumnReader.cpp +++ b/velox/dwio/common/SelectiveStructColumnReader.cpp @@ -386,15 +386,7 @@ void SelectiveStructColumnReaderBase::getValues( if (!rows.size()) { return; } - if (nullsInReadRange_) { - auto readerNulls = nullsInReadRange_->as(); - auto* nulls = resultRow->mutableNulls(rows.size())->asMutable(); - for (size_t i = 0; i < rows.size(); ++i) { - bits::setBit(nulls, i, bits::isBitSet(readerNulls, rows[i])); - } - } else { - resultRow->clearNulls(0, rows.size()); - } + setComplexNulls(rows, *result); bool lazyPrepared = false; for (auto& childSpec : scanSpec_->children()) { VELOX_TRACE_HISTORY_PUSH("getValues %s", childSpec->fieldName().c_str()); diff --git a/velox/dwio/common/SelectiveStructColumnReader.h b/velox/dwio/common/SelectiveStructColumnReader.h index b74f89929eed3..be7efd1045f3a 100644 --- a/velox/dwio/common/SelectiveStructColumnReader.h +++ b/velox/dwio/common/SelectiveStructColumnReader.h @@ -390,7 +390,6 @@ SelectiveFlatMapColumnReaderHelper::calculateOffsets( for (vector_size_t i = 0; i < rows.size(); ++i) { if (!reader_.returnReaderNulls_ && nulls && bits::isBitNull(nulls, rows[i])) { - bits::setNull(reader_.rawResultNulls_, i); reader_.anyNulls_ = true; } offsets[i] = numNestedRows; @@ -546,7 +545,7 @@ void SelectiveFlatMapColumnReaderHelper::getValues( std::copy_backward( rawOffsets, rawOffsets + rows.size() - 1, rawOffsets + rows.size()); rawOffsets[0] = 0; - result->get()->setNulls(reader_.resultNulls()); + reader_.setComplexNulls(rows, *result); } } // namespace facebook::velox::dwio::common diff --git a/velox/exec/tests/TableScanTest.cpp b/velox/exec/tests/TableScanTest.cpp index 51228943bfdb6..145ede14480d8 100644 --- a/velox/exec/tests/TableScanTest.cpp +++ b/velox/exec/tests/TableScanTest.cpp @@ -3153,6 +3153,31 @@ TEST_F(TableScanTest, mapIsNullFilter) { "SELECT * FROM tmp WHERE c0 is null"); } +TEST_F(TableScanTest, compactComplexNulls) { + constexpr int kSize = 10; + auto iota = makeFlatVector(kSize, folly::identity); + std::vector offsets(kSize); + for (int i = 0; i < kSize; ++i) { + offsets[i] = (i + 1) / 2 * 2; + } + auto c0 = makeRowVector( + { + makeArrayVector(offsets, iota, {1, 3, 5, 7, 9}), + iota, + }, + [](auto i) { return i == 2; }); + auto data = makeRowVector({c0}); + auto schema = asRowType(data->type()); + auto file = TempFilePath::create(); + writeToFile(file->getPath(), {data}); + auto plan = PlanBuilder().tableScan(schema, {"(c0).c1 > 0"}).planNode(); + auto split = makeHiveConnectorSplit(file->getPath()); + const vector_size_t indices[] = {1, 3, 4, 5, 6, 7, 8, 9}; + auto expected = makeRowVector({wrapInDictionary( + makeIndices(8, [&](auto i) { return indices[i]; }), c0)}); + AssertQueryBuilder(plan).split(split).assertResults(expected); +} + TEST_F(TableScanTest, remainingFilter) { auto rowType = ROW( {"c0", "c1", "c2", "c3"}, {INTEGER(), INTEGER(), DOUBLE(), BOOLEAN()}); From d0fc5e3397ea5211c042a1390ae1130c86d289f2 Mon Sep 17 00:00:00 2001 From: Krishna Pai Date: Thu, 22 Aug 2024 08:05:10 -0700 Subject: [PATCH 10/24] Ensure we throw on casting unicode to integral types (#10804) Summary: Velox currently uses folly for casting varchar to integral type whereas Presto supports a lot more (see : https://github.com/facebookincubator/velox/issues/10803) . Folly only supports ascii characters at the moment and thus if we get unicode chars this can cause potential correctness issues if wrapped in a try cast. This PR thus throws a system error if we encounter a unicode char during conversion to ensure we dont return incorrect results till unicode support for casting is added. This change only affects PrestoCastPolicy and thus Presto. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10804 Reviewed By: kagamiori Differential Revision: D61627251 Pulled By: kgpai fbshipit-source-id: dd026ccb1f5e6fe5459ef3777f87e19ab6c5cb21 --- velox/expression/CastExpr-inl.h | 12 +++++++++++ velox/expression/tests/CastExprTest.cpp | 6 ++++++ .../functions/prestosql/tests/CastBaseTest.h | 21 +++++++++++++------ velox/type/Conversions.h | 6 ++++++ 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/velox/expression/CastExpr-inl.h b/velox/expression/CastExpr-inl.h index e7ddbd5ae56a4..d22b4adac8704 100644 --- a/velox/expression/CastExpr-inl.h +++ b/velox/expression/CastExpr-inl.h @@ -303,6 +303,18 @@ void CastExpr::applyCastKernel( setResultOrError(castResult, row); return; } + + if constexpr ( + ToKind == TypeKind::TINYINT || ToKind == TypeKind::SMALLINT || + ToKind == TypeKind::INTEGER || ToKind == TypeKind::BIGINT || + ToKind == TypeKind::HUGEINT) { + if constexpr (TPolicy::throwOnUnicode) { + VELOX_CHECK( + functions::stringCore::isAscii( + inputRowValue.data(), inputRowValue.size()), + "Unicode characters are not supported for conversion to integer types"); + } + } } const auto castResult = diff --git a/velox/expression/tests/CastExprTest.cpp b/velox/expression/tests/CastExprTest.cpp index efa036d32455d..f7bea8890f0a2 100644 --- a/velox/expression/tests/CastExprTest.cpp +++ b/velox/expression/tests/CastExprTest.cpp @@ -1022,6 +1022,12 @@ TEST_F(CastExprTest, primitiveInvalidCornerCases) { "bigint", {"infinity"}, "Invalid leading character"); testInvalidCast( "bigint", {"nan"}, "Invalid leading character"); + testInvalidCast( + "bigint", + {"Ù£"}, + "Unicode characters are not supported for conversion to integer types", + VARCHAR(), + true); } // To floating-point. diff --git a/velox/functions/prestosql/tests/CastBaseTest.h b/velox/functions/prestosql/tests/CastBaseTest.h index 6f1d6b102229c..7359f50ad7474 100644 --- a/velox/functions/prestosql/tests/CastBaseTest.h +++ b/velox/functions/prestosql/tests/CastBaseTest.h @@ -186,12 +186,21 @@ class CastBaseTest : public FunctionBaseTest { const std::string& typeString, const std::vector>& input, const std::string& expectedErrorMessage, - const TypePtr& fromType = CppToType::create()) { - VELOX_ASSERT_THROW( - evaluate( - fmt::format("cast(c0 as {})", typeString), - makeRowVector({makeNullableFlatVector(input, fromType)})), - expectedErrorMessage); + const TypePtr& fromType = CppToType::create(), + const bool isRunTimeThrow = false) { + if (!isRunTimeThrow) { + VELOX_ASSERT_USER_THROW( + evaluate( + fmt::format("cast(c0 as {})", typeString), + makeRowVector({makeNullableFlatVector(input, fromType)})), + expectedErrorMessage); + } else { + VELOX_ASSERT_RUNTIME_THROW( + evaluate( + fmt::format("cast(c0 as {})", typeString), + makeRowVector({makeNullableFlatVector(input, fromType)})), + expectedErrorMessage); + } } void testCast( diff --git a/velox/type/Conversions.h b/velox/type/Conversions.h index e0ac382a16c1f..171bd553c3de1 100644 --- a/velox/type/Conversions.h +++ b/velox/type/Conversions.h @@ -34,16 +34,22 @@ namespace facebook::velox::util { struct PrestoCastPolicy { static constexpr bool truncate = false; static constexpr bool legacyCast = false; + // Throws if we encounter unicode when converting to int + // See issue : https://github.com/facebookincubator/velox/issues/10803 + // Remove when unicode support is added. + static constexpr bool throwOnUnicode = true; }; struct SparkCastPolicy { static constexpr bool truncate = true; static constexpr bool legacyCast = false; + static constexpr bool throwOnUnicode = false; }; struct LegacyCastPolicy { static constexpr bool truncate = false; static constexpr bool legacyCast = true; + static constexpr bool throwOnUnicode = false; }; template From b228e09210689bca37533f1677c606b5edc8c7e3 Mon Sep 17 00:00:00 2001 From: Deepak Majeti Date: Thu, 22 Aug 2024 16:25:26 -0700 Subject: [PATCH 11/24] Set ARROW_DEPENDENCY_SOURCE=AUTO for arrow build (#10819) Summary: The build fails with conda. See https://github.com/facebookincubator/velox/issues/10818 Resolves https://github.com/facebookincubator/velox/issues/10818 Pull Request resolved: https://github.com/facebookincubator/velox/pull/10819 Reviewed By: kgpai Differential Revision: D61685854 Pulled By: xiaoxmeng fbshipit-source-id: 3891d5997d058e31f46d7f5e8eb74d7e9bb8c1ba --- CMake/resolve_dependency_modules/arrow/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMake/resolve_dependency_modules/arrow/CMakeLists.txt b/CMake/resolve_dependency_modules/arrow/CMakeLists.txt index 56b673e87a391..ddf1fac71b86c 100644 --- a/CMake/resolve_dependency_modules/arrow/CMakeLists.txt +++ b/CMake/resolve_dependency_modules/arrow/CMakeLists.txt @@ -24,6 +24,7 @@ if(VELOX_ENABLE_ARROW) set(ARROW_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/arrow_ep") set(ARROW_CMAKE_ARGS -DARROW_PARQUET=OFF + -DARROW_DEPENDENCY_SOURCE=AUTO -DARROW_WITH_THRIFT=ON -DARROW_WITH_LZ4=ON -DARROW_WITH_SNAPPY=ON From b81dcaa9b1f8dc75e857d1e0215a0a5a56035c39 Mon Sep 17 00:00:00 2001 From: yingsu00 Date: Thu, 22 Aug 2024 23:14:45 -0700 Subject: [PATCH 12/24] Allow column names to contain space (#10784) Summary: For Hive tables with higher version than 0.13 allows the table and column names to contain spaces. This change allows spaces to be part of unquoted subscript characters Pull Request resolved: https://github.com/facebookincubator/velox/pull/10784 Reviewed By: Yuhta Differential Revision: D61671346 Pulled By: kgpai fbshipit-source-id: 2836fdee33420665faef4355397a76c4e50d786b --- velox/type/Tokenizer.cpp | 2 +- velox/type/tests/SubfieldTest.cpp | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/velox/type/Tokenizer.cpp b/velox/type/Tokenizer.cpp index f740596cede7e..24505ab32a768 100644 --- a/velox/type/Tokenizer.cpp +++ b/velox/type/Tokenizer.cpp @@ -157,7 +157,7 @@ bool Tokenizer::isUnquotedPathCharacter(char c) { } bool Tokenizer::isUnquotedSubscriptCharacter(char c) { - return c == '-' || c == '_' || isalnum(c); + return c == '-' || c == '_' || c == ' ' || isalnum(c); } std::unique_ptr Tokenizer::matchQuotedSubscript() { diff --git a/velox/type/tests/SubfieldTest.cpp b/velox/type/tests/SubfieldTest.cpp index ea2393d2e17d1..7edeab16aeb80 100644 --- a/velox/type/tests/SubfieldTest.cpp +++ b/velox/type/tests/SubfieldTest.cpp @@ -57,6 +57,9 @@ void testColumnName( } TEST(SubfieldTest, columnNamesWithSpecialCharacters) { + testColumnName("two words"); + testColumnName("two words"); + testColumnName("one two three"); testColumnName("$bucket"); testColumnName("apollo-11"); testColumnName("a/b/c:12"); From 2111fb59adf5e1afef3578d953c423f794dee503 Mon Sep 17 00:00:00 2001 From: Jialiang Tan Date: Thu, 22 Aug 2024 23:17:40 -0700 Subject: [PATCH 13/24] Add disable pool management option to memory manager (#10817) Summary: Add this disable pool management option to give users an option to have better memory pool performance (creation), especially for high QPS use cases. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10817 Reviewed By: xiaoxmeng Differential Revision: D61679884 Pulled By: tanjialiang fbshipit-source-id: dadec0a66ed0b78da56560b68b049487324bef76 --- velox/common/memory/Memory.cpp | 45 ++++++++++------ velox/common/memory/Memory.h | 14 +++-- velox/common/memory/MemoryArbitrator.h | 2 +- .../common/memory/tests/MemoryManagerTest.cpp | 53 +++++++++++++++++++ 4 files changed, 95 insertions(+), 19 deletions(-) diff --git a/velox/common/memory/Memory.cpp b/velox/common/memory/Memory.cpp index fed1d3c2b6dc8..15213843b7a2a 100644 --- a/velox/common/memory/Memory.cpp +++ b/velox/common/memory/Memory.cpp @@ -133,12 +133,12 @@ std::vector> createSharedLeafMemoryPools( MemoryManager::MemoryManager(const MemoryManagerOptions& options) : allocator_{createAllocator(options)}, - poolInitCapacity_(options.memoryPoolInitCapacity), arbitrator_(createArbitrator(options)), alignment_(std::max(MemoryAllocator::kMinAlignment, options.alignment)), checkUsageLeak_(options.checkUsageLeak), debugEnabled_(options.debugEnabled), coreOnAllocationFailureEnabled_(options.coreOnAllocationFailureEnabled), + disableMemoryPoolTracking_(options.disableMemoryPoolTracking), poolDestructionCb_([&](MemoryPool* pool) { dropPool(pool); }), sysRoot_{std::make_shared( this, @@ -246,6 +246,25 @@ uint16_t MemoryManager::alignment() const { return alignment_; } +std::shared_ptr MemoryManager::createRootPool( + std::string poolName, + std::unique_ptr& reclaimer, + MemoryPool::Options& options) { + auto pool = std::make_shared( + this, + poolName, + MemoryPool::Kind::kAggregate, + nullptr, + std::move(reclaimer), + poolDestructionCb_, + options); + VELOX_CHECK_EQ(pool->capacity(), 0); + arbitrator_->addPool(pool); + RECORD_HISTOGRAM_METRIC_VALUE( + kMetricMemoryPoolInitialCapacityBytes, pool->capacity()); + return pool; +} + std::shared_ptr MemoryManager::addRootPool( const std::string& name, int64_t maxCapacity, @@ -263,23 +282,16 @@ std::shared_ptr MemoryManager::addRootPool( options.debugEnabled = debugEnabled_; options.coreOnAllocationFailureEnabled = coreOnAllocationFailureEnabled_; + if (disableMemoryPoolTracking_) { + return createRootPool(poolName, reclaimer, options); + } + std::unique_lock guard{mutex_}; if (pools_.find(poolName) != pools_.end()) { VELOX_FAIL("Duplicate root pool name found: {}", poolName); } - auto pool = std::make_shared( - this, - poolName, - MemoryPool::Kind::kAggregate, - nullptr, - std::move(reclaimer), - poolDestructionCb_, - options); + auto pool = createRootPool(poolName, reclaimer, options); pools_.emplace(poolName, pool); - VELOX_CHECK_EQ(pool->capacity(), 0); - arbitrator_->addPool(pool); - RECORD_HISTOGRAM_METRIC_VALUE( - kMetricMemoryPoolInitialCapacityBytes, pool->capacity()); return pool; } @@ -302,6 +314,11 @@ uint64_t MemoryManager::shrinkPools( } void MemoryManager::dropPool(MemoryPool* pool) { + VELOX_DCHECK_EQ(pool->reservedBytes(), 0); + arbitrator_->removePool(pool); + if (disableMemoryPoolTracking_) { + return; + } VELOX_CHECK_NOT_NULL(pool); std::unique_lock guard{mutex_}; auto it = pools_.find(pool->name()); @@ -309,8 +326,6 @@ void MemoryManager::dropPool(MemoryPool* pool) { VELOX_FAIL("The dropped memory pool {} not found", pool->name()); } pools_.erase(it); - VELOX_DCHECK_EQ(pool->reservedBytes(), 0); - arbitrator_->removePool(pool); } MemoryPool& MemoryManager::deprecatedSharedLeafPool() { diff --git a/velox/common/memory/Memory.h b/velox/common/memory/Memory.h index 5ce372e77dc48..ac1085f473bfd 100644 --- a/velox/common/memory/Memory.h +++ b/velox/common/memory/Memory.h @@ -81,6 +81,9 @@ struct MemoryManagerOptions { /// Terminates the process and generates a core file on an allocation failure bool coreOnAllocationFailureEnabled{false}; + /// Disables the memory manager's tracking on memory pools. + bool disableMemoryPoolTracking{false}; + /// ================== 'MemoryAllocator' settings ================== /// Specifies the max memory allocation capacity in bytes enforced by @@ -344,21 +347,26 @@ class MemoryManager { } private: + std::shared_ptr createRootPool( + std::string poolName, + std::unique_ptr& reclaimer, + MemoryPool::Options& options); + void dropPool(MemoryPool* pool); // Returns the shared references to all the alive memory pools in 'pools_'. std::vector> getAlivePools() const; const std::shared_ptr allocator_; - // Specifies the capacity to allocate from 'arbitrator_' for a newly created - // root memory pool. - const uint64_t poolInitCapacity_; + // If not null, used to arbitrate the memory capacity among 'pools_'. const std::unique_ptr arbitrator_; const uint16_t alignment_; const bool checkUsageLeak_; const bool debugEnabled_; const bool coreOnAllocationFailureEnabled_; + const bool disableMemoryPoolTracking_; + // The destruction callback set for the allocated root memory pools which are // tracked by 'pools_'. It is invoked on the root pool destruction and removes // the pool from 'pools_'. diff --git a/velox/common/memory/MemoryArbitrator.h b/velox/common/memory/MemoryArbitrator.h index 20b50e6012886..e5b9dc35dc580 100644 --- a/velox/common/memory/MemoryArbitrator.h +++ b/velox/common/memory/MemoryArbitrator.h @@ -64,7 +64,7 @@ class MemoryArbitrator { MemoryArbitrationStateCheckCB arbitrationStateCheckCb{nullptr}; /// Additional configs that are arbitrator implementation specific. - std::unordered_map extraConfigs; + std::unordered_map extraConfigs{}; }; using Factory = std::function( diff --git a/velox/common/memory/tests/MemoryManagerTest.cpp b/velox/common/memory/tests/MemoryManagerTest.cpp index 348ed8db33775..02ac9a11fb2ab 100644 --- a/velox/common/memory/tests/MemoryManagerTest.cpp +++ b/velox/common/memory/tests/MemoryManagerTest.cpp @@ -620,4 +620,57 @@ TEST_F(MemoryManagerTest, quotaEnforcement) { } } } + +TEST_F(MemoryManagerTest, disableMemoryPoolTracking) { + const std::string kSharedKind{"SHARED"}; + const std::string kNoopKind{""}; + MemoryManagerOptions options; + options.disableMemoryPoolTracking = true; + options.allocatorCapacity = 64LL << 20; + options.arbitratorCapacity = 64LL << 20; + std::vector arbitratorKinds{kNoopKind, kSharedKind}; + for (auto arbitratorKind : arbitratorKinds) { + options.arbitratorKind = arbitratorKind; + MemoryManager manager{options}; + auto root0 = manager.addRootPool("root_0", 35LL << 20); + auto leaf0 = root0->addLeafChild("leaf_0"); + + // Not throwing since there is no duplicate check. + auto root0Dup = manager.addRootPool("root_0", 35LL << 20); + + // 1TB capacity is allowed since there is no capacity check. + auto root1 = manager.addRootPool("root_1", 1LL << 40); + auto leaf1 = root1->addLeafChild("leaf_1"); + + ASSERT_EQ(root0->capacity(), 35LL << 20); + if (arbitratorKind == kSharedKind) { + ASSERT_EQ(root0Dup->capacity(), 29LL << 20); + ASSERT_EQ(root1->capacity(), 0); + } else { + ASSERT_EQ(root0Dup->capacity(), 35LL << 20); + ASSERT_EQ(root1->capacity(), 1LL << 40); + } + + ASSERT_EQ(manager.capacity(), 64LL << 20); + ASSERT_EQ(manager.shrinkPools(), 0); + // Default 1 system pool with 1 leaf child + ASSERT_EQ(manager.numPools(), 1); + + VELOX_ASSERT_THROW( + leaf0->allocate(38LL << 20), "Exceeded memory pool capacity"); + if (arbitratorKind == kSharedKind) { + VELOX_ASSERT_THROW( + leaf1->allocate(256LL << 20), "Exceeded memory pool capacity"); + } else { + VELOX_ASSERT_THROW( + leaf1->allocate(256LL << 20), "Exceeded memory allocator limit"); + } + + ASSERT_NO_THROW(leaf0.reset()); + ASSERT_NO_THROW(leaf1.reset()); + ASSERT_NO_THROW(root0.reset()); + ASSERT_NO_THROW(root0Dup.reset()); + ASSERT_NO_THROW(root1.reset()); + } +} } // namespace facebook::velox::memory From eaec1d349f19f8d5954e57e9c007117066742e1a Mon Sep 17 00:00:00 2001 From: Zuyu ZHANG Date: Fri, 23 Aug 2024 00:25:29 -0700 Subject: [PATCH 14/24] Fix build errors in Presto due to misconfigured generated proto file paths (#10807) Summary: A follow-up fix for https://github.com/facebookincubator/velox/issues/10357 In Presto build, the following two CMake arguments for `velox` are different, although both are the same in the `velox` standalone build: * `CMAKE_BINARY_DIR`: presto/presto-native-execution/cmake-build-debug * `PROJECT_BINARY_DIR`: presto/presto-native-execution/cmake-build-debug/velox ```sh Building CXX object velox/velox/functions/sparksql/fuzzer/CMakeFiles/velox_spark_query_runner.dir/__/__/__/__/spark/connect/catalog.pb.cc.o FAILED: clang++: error: no such file or directory: 'presto/presto-native-execution/cmake-build-debug/velox/spark/connect/catalog.pb.cc' clang++: error: no input files $ ls presto/presto-native-execution/cmake-build-debug/spark/connect/catalog.* catalog.grpc.pb.cc catalog.grpc.pb.h catalog.pb.cc catalog.pb.h ``` Pull Request resolved: https://github.com/facebookincubator/velox/pull/10807 Reviewed By: xiaoxmeng Differential Revision: D61705399 Pulled By: kgpai fbshipit-source-id: 34828de6f513ef072c1c9a0a1aa5510a4da86739 --- velox/functions/sparksql/fuzzer/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/velox/functions/sparksql/fuzzer/CMakeLists.txt b/velox/functions/sparksql/fuzzer/CMakeLists.txt index 917469a2e9454..75347a517eddf 100644 --- a/velox/functions/sparksql/fuzzer/CMakeLists.txt +++ b/velox/functions/sparksql/fuzzer/CMakeLists.txt @@ -68,7 +68,7 @@ endforeach() # Generate Spark connect hearders and sources. add_custom_command( OUTPUT ${PROTO_OUTPUT_FILES} - COMMAND protobuf::protoc ${PROTO_PATH_ARGS} --cpp_out ${CMAKE_BINARY_DIR} + COMMAND protobuf::protoc ${PROTO_PATH_ARGS} --cpp_out ${PROJECT_BINARY_DIR} ${PROTO_FILES_FULL} DEPENDS protobuf::protoc COMMENT "Running PROTO compiler" @@ -78,7 +78,7 @@ add_custom_command( add_custom_command( OUTPUT ${GRPC_OUTPUT_FILES} COMMAND - protobuf::protoc ${PROTO_PATH_ARGS} --grpc_out=${CMAKE_BINARY_DIR} + protobuf::protoc ${PROTO_PATH_ARGS} --grpc_out=${PROJECT_BINARY_DIR} --plugin=protoc-gen-grpc=$ ${PROTO_FILES_FULL} DEPENDS protobuf::protoc From b5cd1e896bc4c5fea56042410bcf89bfe075af40 Mon Sep 17 00:00:00 2001 From: Krishna Pai Date: Fri, 23 Aug 2024 08:23:13 -0700 Subject: [PATCH 15/24] Refactor RowTranslationUtil::toElementRows (#10820) Summary: This Pr fixes the comments on RowTranslationUtil and also ensures that it's used only for map/array vectors. We also add some debug time safeguards for the index mappings. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10820 Reviewed By: spershin Differential Revision: D61693258 Pulled By: kgpai fbshipit-source-id: 46165cf170a3b037154e8bf62b05a2e7f9f74a1a --- velox/functions/lib/RowsTranslationUtil.h | 38 ++++++++++++++++------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/velox/functions/lib/RowsTranslationUtil.h b/velox/functions/lib/RowsTranslationUtil.h index bd374bb9529b2..97185203ba28c 100644 --- a/velox/functions/lib/RowsTranslationUtil.h +++ b/velox/functions/lib/RowsTranslationUtil.h @@ -22,26 +22,40 @@ namespace facebook::velox::functions { -/// Returns SelectivityVector for the nested vector with all rows corresponding -/// to specified top-level rows selected. The optional topLevelRowMapping is -/// used to pass the dictionary indices if the topLevelVector is dictionary -/// encoded. +/// This function returns a SelectivityVector for ARRAY/MAP vectors that selects +/// all rows corresponding to the specified rows. If the base vector was +/// dictionary encoded, an optional rowMapping parameter can be used to pass +/// the dictionary indices. In this case, it is important to ensure that the +/// topLevelRows parameter has already filtered out any null rows added by the +/// dictionary encoding. template SelectivityVector toElementRows( vector_size_t size, - const SelectivityVector& topLevelRows, - const T* topLevelVector, - const vector_size_t* topLevelRowMapping = nullptr) { - auto rawNulls = topLevelVector->rawNulls(); - auto rawSizes = topLevelVector->rawSizes(); - auto rawOffsets = topLevelVector->rawOffsets(); + const SelectivityVector& topLevelNonNullRows, + const T* arrayBaseVector, + const vector_size_t* rowMapping = nullptr) { + VELOX_CHECK( + arrayBaseVector->encoding() == VectorEncoding::Simple::MAP || + arrayBaseVector->encoding() == VectorEncoding::Simple::ARRAY); + + auto rawNulls = arrayBaseVector->rawNulls(); + auto rawSizes = arrayBaseVector->rawSizes(); + auto rawOffsets = arrayBaseVector->rawOffsets(); + const auto sizeRange = + arrayBaseVector->sizes()->template asRange().end(); + const auto offsetRange = + arrayBaseVector->offsets()->template asRange().end(); SelectivityVector elementRows(size, false); - topLevelRows.applyToSelected([&](vector_size_t row) { - auto index = topLevelRowMapping ? topLevelRowMapping[row] : row; + topLevelNonNullRows.applyToSelected([&](vector_size_t row) { + auto index = rowMapping ? rowMapping[row] : row; if (rawNulls && bits::isBitNull(rawNulls, index)) { return; } + + VELOX_DCHECK_LE(index, sizeRange); + VELOX_DCHECK_LE(index, offsetRange); + auto size = rawSizes[index]; auto offset = rawOffsets[index]; elementRows.setValidRange(offset, offset + size, true); From d46459c32967d3cfdfba54840b032cd3910c43f4 Mon Sep 17 00:00:00 2001 From: Yang Zhang Date: Fri, 23 Aug 2024 09:55:40 -0700 Subject: [PATCH 16/24] Add write IO metrics and print physical written bytes in PlanNodeStats#toString (#10806) Summary: Count write IO metrics when invoke WriteFile#append, and print physical written bytes when invoke printPlanWithStats. Fixes: https://github.com/facebookincubator/velox/issues/10794 Pull Request resolved: https://github.com/facebookincubator/velox/pull/10806 Reviewed By: Yuhta Differential Revision: D61706390 Pulled By: xiaoxmeng fbshipit-source-id: 510b8f2821c1f012a4b9714b9ac9d92ba624b2ac --- velox/common/io/IoStatistics.cpp | 8 +++ velox/common/io/IoStatistics.h | 3 + velox/connectors/Connector.h | 1 + velox/connectors/hive/HiveDataSink.cpp | 3 + velox/dwio/common/FileSink.cpp | 15 +++-- velox/exec/PlanNodeStats.cpp | 7 +- velox/exec/TableWriter.cpp | 4 ++ velox/exec/tests/PrintPlanWithStatsTest.cpp | 73 +++++++++++++++++++++ velox/exec/tests/TableWriteTest.cpp | 5 ++ 9 files changed, 112 insertions(+), 7 deletions(-) diff --git a/velox/common/io/IoStatistics.cpp b/velox/common/io/IoStatistics.cpp index 7b7883c73dcf9..7dfddc6dc4831 100644 --- a/velox/common/io/IoStatistics.cpp +++ b/velox/common/io/IoStatistics.cpp @@ -46,6 +46,10 @@ uint64_t IoStatistics::totalScanTime() const { return totalScanTime_.load(std::memory_order_relaxed); } +uint64_t IoStatistics::writeIOTimeUs() const { + return writeIOTimeUs_.load(std::memory_order_relaxed); +} + uint64_t IoStatistics::incRawBytesRead(int64_t v) { return rawBytesRead_.fetch_add(v, std::memory_order_relaxed); } @@ -70,6 +74,10 @@ uint64_t IoStatistics::incTotalScanTime(int64_t v) { return totalScanTime_.fetch_add(v, std::memory_order_relaxed); } +uint64_t IoStatistics::incWriteIOTimeUs(int64_t v) { + return writeIOTimeUs_.fetch_add(v, std::memory_order_relaxed); +} + void IoStatistics::incOperationCounters( const std::string& operation, const uint64_t resourceThrottleCount, diff --git a/velox/common/io/IoStatistics.h b/velox/common/io/IoStatistics.h index 7e5bcb0192e3e..2111a8877b475 100644 --- a/velox/common/io/IoStatistics.h +++ b/velox/common/io/IoStatistics.h @@ -97,6 +97,7 @@ class IoStatistics { uint64_t inputBatchSize() const; uint64_t outputBatchSize() const; uint64_t totalScanTime() const; + uint64_t writeIOTimeUs() const; uint64_t incRawBytesRead(int64_t); uint64_t incRawOverreadBytes(int64_t); @@ -104,6 +105,7 @@ class IoStatistics { uint64_t incInputBatchSize(int64_t); uint64_t incOutputBatchSize(int64_t); uint64_t incTotalScanTime(int64_t); + uint64_t incWriteIOTimeUs(int64_t); IoCounter& prefetch() { return prefetch_; @@ -150,6 +152,7 @@ class IoStatistics { std::atomic outputBatchSize_{0}; std::atomic rawOverreadBytes_{0}; std::atomic totalScanTime_{0}; + std::atomic writeIOTimeUs_{0}; // Planned read from storage or SSD. IoCounter prefetch_; diff --git a/velox/connectors/Connector.h b/velox/connectors/Connector.h index c16124ff66e36..1dc77823e4366 100644 --- a/velox/connectors/Connector.h +++ b/velox/connectors/Connector.h @@ -151,6 +151,7 @@ class DataSink { struct Stats { uint64_t numWrittenBytes{0}; uint32_t numWrittenFiles{0}; + uint64_t writeIOTimeUs{0}; common::SpillStats spillStats; bool empty() const; diff --git a/velox/connectors/hive/HiveDataSink.cpp b/velox/connectors/hive/HiveDataSink.cpp index a185ca20e9d41..c0d5cb46b9fa5 100644 --- a/velox/connectors/hive/HiveDataSink.cpp +++ b/velox/connectors/hive/HiveDataSink.cpp @@ -511,10 +511,13 @@ DataSink::Stats HiveDataSink::stats() const { } int64_t numWrittenBytes{0}; + int64_t writeIOTimeUs{0}; for (const auto& ioStats : ioStats_) { numWrittenBytes += ioStats->rawBytesWritten(); + writeIOTimeUs += ioStats->writeIOTimeUs(); } stats.numWrittenBytes = numWrittenBytes; + stats.writeIOTimeUs = writeIOTimeUs; if (state_ != State::kClosed) { return stats; diff --git a/velox/dwio/common/FileSink.cpp b/velox/dwio/common/FileSink.cpp index 0f69ee1cc510d..cebfe54c3bbf2 100644 --- a/velox/dwio/common/FileSink.cpp +++ b/velox/dwio/common/FileSink.cpp @@ -67,14 +67,19 @@ void FileSink::writeImpl( const std::function&)>& callback) { DWIO_ENSURE(!isClosed(), "Cannot write to closed sink."); const uint64_t oldSize = size_; - for (auto& buf : buffers) { - // NOTE: we need to update 'size_' after each 'callback' invocation as some - // file sink implementation like MemorySink depends on the updated 'size_' - // for new write. - size_ += callback(buf); + uint64_t writeTimeUs{0}; + { + MicrosecondTimer timer(&writeTimeUs); + for (auto& buf : buffers) { + // NOTE: we need to update 'size_' after each 'callback' invocation as + // some file sink implementation like MemorySink depends on the updated + // 'size_' for new write. + size_ += callback(buf); + } } if (stats_ != nullptr) { stats_->incRawBytesWritten(size_ - oldSize); + stats_->incWriteIOTimeUs(writeTimeUs); } // Writing buffer is treated as transferring ownership. So clearing the // buffers after all buffers are written. diff --git a/velox/exec/PlanNodeStats.cpp b/velox/exec/PlanNodeStats.cpp index caab8f7648728..940d5c6d49618 100644 --- a/velox/exec/PlanNodeStats.cpp +++ b/velox/exec/PlanNodeStats.cpp @@ -95,8 +95,11 @@ std::string PlanNodeStats::toString(bool includeInputStats) const { } } out << "Output: " << outputRows << " rows (" << succinctBytes(outputBytes) - << ", " << outputVectors << " batches)" - << ", Cpu time: " << succinctNanos(cpuWallTiming.cpuNanos) + << ", " << outputVectors << " batches)"; + if (physicalWrittenBytes > 0) { + out << ", Physical written output: " << succinctBytes(physicalWrittenBytes); + } + out << ", Cpu time: " << succinctNanos(cpuWallTiming.cpuNanos) << ", Blocked wall time: " << succinctNanos(blockedWallNanos) << ", Peak memory: " << succinctBytes(peakMemoryBytes) << ", Memory allocations: " << numMemoryAllocations; diff --git a/velox/exec/TableWriter.cpp b/velox/exec/TableWriter.cpp index b8e0fca88e498..9bd199cfe9ffc 100644 --- a/velox/exec/TableWriter.cpp +++ b/velox/exec/TableWriter.cpp @@ -253,6 +253,10 @@ void TableWriter::updateStats(const connector::DataSink::Stats& stats) { } lockedStats->addRuntimeStat( "numWrittenFiles", RuntimeCounter(stats.numWrittenFiles)); + lockedStats->addRuntimeStat( + "writeIOTime", + RuntimeCounter( + stats.writeIOTimeUs * 1000, RuntimeCounter::Unit::kNanos)); } if (!stats.spillStats.empty()) { *spillStats_.wlock() += stats.spillStats; diff --git a/velox/exec/tests/PrintPlanWithStatsTest.cpp b/velox/exec/tests/PrintPlanWithStatsTest.cpp index f426f66dc85c0..9b52725c3feea 100644 --- a/velox/exec/tests/PrintPlanWithStatsTest.cpp +++ b/velox/exec/tests/PrintPlanWithStatsTest.cpp @@ -18,6 +18,7 @@ #include "velox/exec/tests/utils/AssertQueryBuilder.h" #include "velox/exec/tests/utils/HiveConnectorTestBase.h" #include "velox/exec/tests/utils/PlanBuilder.h" +#include "velox/exec/tests/utils/TempDirectoryPath.h" #include #include @@ -309,3 +310,75 @@ TEST_F(PrintPlanWithStatsTest, partialAggregateWithTableScan) { {" totalScanTime [ ]* sum: .+, count: .+, min: .+, max: .+"}}); } } + +TEST_F(PrintPlanWithStatsTest, tableWriterWithTableScan) { + RowTypePtr rowType{ + ROW({"c0", "c1", "c2", "c3", "c4", "c5"}, + {BIGINT(), INTEGER(), SMALLINT(), REAL(), DOUBLE(), VARCHAR()})}; + auto vectors = makeVectors(rowType, 10, 10); + + const auto filePath = TempFilePath::create(); + writeToFile(filePath->getPath(), vectors); + const auto writeDir = TempDirectoryPath::create(); + + auto writePlan = PlanBuilder() + .tableScan(rowType) + .tableWrite(writeDir->getPath()) + .planNode(); + + std::shared_ptr task; + AssertQueryBuilder(writePlan) + .splits(makeHiveConnectorSplits({filePath})) + .copyResults(pool(), task); + ensureTaskCompletion(task.get()); + compareOutputs( + ::testing::UnitTest::GetInstance()->current_test_info()->name(), + printPlanWithStats(*writePlan, task->taskStats()), + {{R"(-- TableWrite\[1\]\[.+InsertTableHandle .+)"}, + {" Output: .+, Physical written output: .+, Cpu time: .+, Blocked wall time: .+, Peak memory: .+, Memory allocations: .+, Threads: 1"}, + {R"( -- TableScan\[0\]\[table: hive_table\] -> c0:BIGINT, c1:INTEGER, c2:SMALLINT, c3:REAL, c4:DOUBLE, c5:VARCHAR)"}, + {R"( Input: 100 rows \(.+\), Output: 100 rows \(.+\), Cpu time: .+, Blocked wall time: .+, Peak memory: .+, Memory allocations: .+, Threads: 1, Splits: 1)"}}); + + compareOutputs( + ::testing::UnitTest::GetInstance()->current_test_info()->name(), + printPlanWithStats(*writePlan, task->taskStats(), true), + {{R"(-- TableWrite\[1\]\[.+InsertTableHandle .+)"}, + {" Output: .+, Physical written output: .+, Cpu time: .+, Blocked wall time: .+, Peak memory: .+, Memory allocations: .+, Threads: 1"}, + {" dataSourceLazyCpuNanos\\s+sum: .+, count: .+, min: .+, max: .+"}, + {" dataSourceLazyWallNanos\\s+sum: .+, count: .+, min: .+, max: .+"}, + {" numWrittenFiles\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" runningAddInputWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" runningFinishWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" runningGetOutputWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" stripeSize\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" writeIOTime\\s+sum: .+, count: 1, min: .+, max: .+"}, + {R"( -- TableScan\[0\]\[table: hive_table\] -> c0:BIGINT, c1:INTEGER, c2:SMALLINT, c3:REAL, c4:DOUBLE, c5:VARCHAR)"}, + {R"( Input: 100 rows \(.+\), Output: 100 rows \(.+\), Cpu time: .+, Blocked wall time: .+, Peak memory: .+, Memory allocations: .+, Threads: 1, Splits: 1)"}, + {" dataSourceAddSplitWallNanos[ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" dataSourceReadWallNanos[ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" flattenStringDictionaryValues [ ]* sum: 0, count: 1, min: 0, max: 0"}, + {" ioWaitWallNanos [ ]* sum: .+, count: .+ min: .+, max: .+"}, + {" localReadBytes [ ]* sum: 0B, count: 1, min: 0B, max: 0B"}, + {" maxSingleIoWaitWallNanos[ ]*sum: .+, count: 1, min: .+, max: .+"}, + {" numLocalRead [ ]* sum: 0, count: 1, min: 0, max: 0"}, + {" numPrefetch [ ]* sum: .+, count: .+, min: .+, max: .+"}, + {" numRamRead [ ]* sum: 7, count: 1, min: 7, max: 7"}, + {" numStorageRead [ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" overreadBytes[ ]* sum: 0B, count: 1, min: 0B, max: 0B"}, + + {" prefetchBytes [ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" preloadedSplits[ ]+sum: .+, count: .+, min: .+, max: .+", + true}, + {" ramReadBytes [ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" readyPreloadedSplits[ ]+sum: .+, count: .+, min: .+, max: .+", + true}, + {" runningAddInputWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" runningFinishWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" runningGetOutputWallNanos\\s+sum: .+, count: 1, min: .+, max: .+"}, + {" skippedSplitBytes[ ]* sum: 0B, count: 1, min: 0B, max: 0B"}, + {" skippedSplits [ ]* sum: 0, count: 1, min: 0, max: 0"}, + {" skippedStrides [ ]* sum: 0, count: 1, min: 0, max: 0"}, + {" storageReadBytes [ ]* sum: .+, count: 1, min: .+, max: .+"}, + {" totalRemainingFilterTime\\s+sum: .+, count: .+, min: .+, max: .+"}, + {" totalScanTime [ ]* sum: .+, count: .+, min: .+, max: .+"}}); +} diff --git a/velox/exec/tests/TableWriteTest.cpp b/velox/exec/tests/TableWriteTest.cpp index f249fc612c277..c54ebd3e443be 100644 --- a/velox/exec/tests/TableWriteTest.cpp +++ b/velox/exec/tests/TableWriteTest.cpp @@ -2360,6 +2360,8 @@ TEST_P(UnpartitionedTableWriterTest, runtimeStatsCheck) { stats[1].runtimeStats["stripeSize"].count, testData.expectedNumStripes); ASSERT_EQ(stats[1].runtimeStats["numWrittenFiles"].sum, 1); ASSERT_EQ(stats[1].runtimeStats["numWrittenFiles"].count, 1); + ASSERT_GE(stats[1].runtimeStats["writeIOTime"].sum, 0); + ASSERT_EQ(stats[1].runtimeStats["writeIOTime"].count, 1); } } @@ -3132,6 +3134,9 @@ TEST_P(AllTableWriterTest, tableWriterStats) { ->customStats.at("numWrittenFiles") .sum, numWrittenFiles); + ASSERT_GE( + stats.operatorStats.at("TableWrite")->customStats.at("writeIOTime").sum, + 0); } DEBUG_ONLY_TEST_P( From 52c1daaa10a896d2e96f322b710ed5860dbd4e53 Mon Sep 17 00:00:00 2001 From: snadukudy <51390531+snadukudy@users.noreply.github.com> Date: Fri, 23 Aug 2024 10:42:47 -0700 Subject: [PATCH 17/24] Add CI migration blog post (#10532) Summary: Add blog post on CircleCI migration to GitHub Actions Pull Request resolved: https://github.com/facebookincubator/velox/pull/10532 Reviewed By: amitkdutta Differential Revision: D61669570 Pulled By: kgpai fbshipit-source-id: 6c951e630f4ebc9930342809819d5046e4913024 --- website/blog/2024-08-23-ci-migration.mdx | 78 ++++++++++++++++++ .../static/img/abstract-white-background.jpg | Bin 0 -> 132559 bytes website/static/img/velox-build-metrics.png | Bin 0 -> 103291 bytes 3 files changed, 78 insertions(+) create mode 100644 website/blog/2024-08-23-ci-migration.mdx create mode 100644 website/static/img/abstract-white-background.jpg create mode 100644 website/static/img/velox-build-metrics.png diff --git a/website/blog/2024-08-23-ci-migration.mdx b/website/blog/2024-08-23-ci-migration.mdx new file mode 100644 index 0000000000000..398c17279b607 --- /dev/null +++ b/website/blog/2024-08-23-ci-migration.mdx @@ -0,0 +1,78 @@ +--- +slug: ci-migration +title: "Optimizing and Migrating Velox CI Workloads to Github Actions" +authors: [jwujciak, kgpai] +tags: [tech-blog, packaging] +--- + +
+ +
+ +## TL;DR + +In late 2023, the Meta OSS (Open Source Software) Team requested all Meta teams to move the CI deployments from CircleCI to Github Actions. [Voltron Data](http://voltrondata.com) and Meta in collaboration migrated all the deployed Velox CI jobs. For the year 2024, Velox CI spend was on track to overshoot the allocated resources by a considerable amount of money. As part of this migration effort, the CI workloads were consolidated and optimized by Q2 2024, bringing down the projected 2024 CI spend by 51%. + +## Velox’s Continuous Integration Workload + +Continuous Integration (CI) is crucial for Velox’s success as an open source project as it helps protect from bugs and errors, reduces likelihood of conflicts and leads to increased community trust in the project. This is to ensure the Velox builds works well on a myriad of system architectures, operating systems and compilers - along with the ones used internally at Meta. The OSS build version of Velox also supports additional features that aren't used internally in Meta (for example, support for Cloud blob stores, etc.). + +When a pull request is submitted to Velox, the following jobs are executed: + +1. Linting and Formatting workflows: + 1. Header checks + 2. License checks + 3. Basic Linters +2. Ensure Velox builds on various platforms + 1. MacOS (Intel, M1) + 2. Linux (Ubuntu/Centos) +3. Ensure Velox builds under its various configurations + 1. Debug / Release builds + 2. Build default Velox build + 3. Build Velox with support for Parquet, Arrow and External Adapters (S3/HDFS/GCS etc.) + 4. PyVelox builds +4. Run prerequisite tests + 1. Unit Tests + 2. Benchmarking Tests + 1. [Conbench](https://velox-conbench.voltrondata.run/runs/5bd139fffa9b4e0eb020da4d63211121/) is used to store and compare results, and also alert users on regressions + 3. Various Fuzzer Tests (Expression / Aggregation/ Exchange / Join etc) + 4. Signature Check and Biased Fuzzer Tests ( Expression / Aggregation) + 5. Fuzzer Tests using Presto as source of truth +5. Docker Image build jobs + 1. If an underlying dependency is changed, a new Docker CI image is built for + 1. Ubuntu Linux + 2. Centos + 3. Presto Linux image +6. Documentation build and publish Job + 1. If underlying documentation is changed, Velox documentation pages are rebuilt and published + 2. Netlify is used for publishing Velox web pages + +## Velox CI Optimization + +Previous implementation of CI in CircleCI grew organically and was unoptimized, resulting in long build times, and also significantly costlier. This opportunity to migrate to Github Actions helped to take a holistic view of CI deployments and actively optimized to reduce build times and CI spend. Note however, that there has been continued investment in reducing test times to further improve Velox reliability, stability and developer experience. Some of the optimizations completed are: + +1. **Persisting build artifacts across builds**: During every build, the object files and binaries produced are cached. In addition to this, artifacts such as scalar function signatures and aggregate function signatures are produced. These signatures are used to compare with the baseline version, by comparing against the changes in the current PR to determine if the current changes are backwards incompatible or bias the newly added changes. Using a stash to persist these artifacts helps save one build cycle. + +2. **Optimizing our Instances**: Building Velox is Memory and CPU intensive job. Some beefy instances (16-core machines) are used to build Velox. After the build, the build artifacts are copied to smaller instances (4 core machines) to run fuzzer tests and other jobs. Since these fuzzers often run for an hour and are less intensive than the build process, it resulted in significant CI savings while increasing the test coverage. + +## Instrumenting Velox CI Builds + +Velox CI builds were instrumented in Conbench so that it can capture various metrics about the builds: +1. Build times at translation unit / library/ project level. +2. Binary sizes produced at TLU/ .a,.so / executable level. +3. Memory pressure +4. Measure across time how our changes affect binary sizes + +A nightly job is run to capture these build metrics and it is uploaded to Conbench. Velox build metrics report is available here: [Velox Build Metrics Report](https://facebookincubator.github.io/velox/bm-report/) + + + +## Acknowledgements + +A large part of the credit goes to Jacob Wujciak and the team at Voltron Data. We would also like to thank other collaborators in the Open Source Community and at Meta, including but not limited to: + +**Meta**: Sridhar Anumandla, Pedro Eugenio Rocha Pedreira, Deepak Majeti, Meta OSS Team, and others + +**Voltron Data**: Jacob Wujciak, Austin Dickey, Marcus Hanwell, Sri Nadukudy, and others + + diff --git a/website/static/img/abstract-white-background.jpg b/website/static/img/abstract-white-background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c6b60317fa19cdfdd569c101508ad47e6dbc94b3 GIT binary patch literal 132559 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 literal 0 HcmV?d00001 diff --git a/website/static/img/velox-build-metrics.png b/website/static/img/velox-build-metrics.png new file mode 100644 index 0000000000000000000000000000000000000000..f3ee4694448ee38eab2840a06d4d3c2c471d5596 GIT binary patch literal 103291 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 Y{ZgMGU74R|M;L&>)78&qol`;+04|-&TL1t6 literal 0 HcmV?d00001 From 8d45b3c7899703b5d6a2de8fba897d7b1f7ff5a2 Mon Sep 17 00:00:00 2001 From: Deepak Majeti Date: Fri, 23 Aug 2024 10:48:04 -0700 Subject: [PATCH 18/24] Set CRC32 checksum algorithm for S3 multipart upload (#10801) Summary: The default algorithm used is MD5. However, MD5 is not supported with fips and can cause a SIGSEGV. Set CRC32 instead which is a standard for checksum computation and is not restricted by fips. crc32 is also faster than md5. Internally at IBM, we hit the following SIGSEGV ``` 0x0000000000000000 in ?? () Missing separate debuginfos, use: dnf debuginfo-install openssl-fips-provider-3.0.7-2.el9.x86_64 xz-libs-5.2.5-8.el9_0.x86_64 (gdb) bt #0 0x0000000000000000 in ?? () https://github.com/facebookincubator/velox/issues/1 0x0000000004e5f89b in Aws::Utils::Crypto::MD5OpenSSLImpl::Calculate(std::istream&) () https://github.com/facebookincubator/velox/issues/2 0x0000000004efd298 in Aws::Utils::Crypto::MD5::Calculate(std::istream&) () https://github.com/facebookincubator/velox/issues/3 0x0000000004ef71b9 in Aws::Utils::HashingUtils::CalculateMD5(std::iostream&) () https://github.com/facebookincubator/velox/issues/4 0x0000000004e8ebe8 in Aws::Client::AWSClient::AddChecksumToRequest(std::shared_ptr const&, Aws::AmazonWebServiceRequest const&) const () https://github.com/facebookincubator/velox/issues/5 0x0000000004e8ed15 in Aws::Client::AWSClient::BuildHttpRequest(Aws::AmazonWebServiceRequest const&, std::shared_ptr const&) const () https://github.com/facebookincubator/velox/issues/6 0x0000000004e977f9 in Aws::Client::AWSClient::AttemptOneRequest(std::shared_ptr const&, Aws::AmazonWebServiceRequest const&, char const*, char const*, char const*) const () https://github.com/facebookincubator/velox/issues/7 0x0000000004e9e1c0 in Aws::Client::AWSClient::AttemptExhaustively(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const () https://github.com/facebookincubator/velox/issues/8 0x0000000004ea15e8 in Aws::Client::AWSXMLClient::MakeRequest(Aws::Http::URI const&, Aws::AmazonWebServiceRequest const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const () https://github.com/facebookincubator/velox/issues/9 0x0000000004ea1f70 in Aws::Client::AWSXMLClient::MakeRequest(Aws::AmazonWebServiceRequest const&, Aws::Endpoint::AWSEndpoint const&, Aws::Http::HttpMethod, char const*, char const*, char const*) const () https://github.com/facebookincubator/velox/issues/10 0x0000000004de0933 in Aws::S3::S3Client::UploadPart(Aws::S3::Model::UploadPartRequest const&) const::{lambda()https://github.com/facebookincubator/velox/issues/1}::operator()() const () https://github.com/facebookincubator/velox/issues/11 0x0000000004de0b8c in std::_Function_handler (), Aws::S3::S3Client::UploadPart(Aws::S3::Model::UploadPartRequest const&) const::{lambda()https://github.com/facebookincubator/velox/issues/1}>::_M_invoke(std::_Any_data const&) () https://github.com/facebookincubator/velox/issues/12 0x0000000004e19317 in Aws::Utils::Outcome smithy::components::tracing::TracingUtils::MakeCallWithTiming >(std::function ()>, std::__cxx11::basic_string, std::allocator > const&, smithy::components::tracing::Meter const&, std::map, std::allocator >, std::__cxx11::basic_string, std::allocator >, std::less, std::allocator > >, std::allocator, std::allocator > const, std::__cxx11::basic_string, std::allocator > > > >&&, std::__cxx11::basic_string, std::allocator > const&) () https://github.com/facebookincubator/velox/issues/13 0x0000000004d7cdcf in Aws::S3::S3Client::UploadPart(Aws::S3::Model::UploadPartRequest const&) const () https://github.com/facebookincubator/velox/issues/14 0x0000000004ca4aa6 in facebook::velox::filesystems::S3WriteFile::Impl::uploadPart (this=0x7fffec2f09a0, part=..., isLast=true) at /root/velox/velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp:380 ``` Pull Request resolved: https://github.com/facebookincubator/velox/pull/10801 Reviewed By: amitkdutta Differential Revision: D61671574 Pulled By: kgpai fbshipit-source-id: 34c7b777b3fde0659ef74c4fbfd93740fdfa3f7c --- velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp b/velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp index 08a4261703f97..77e59f0097ee3 100644 --- a/velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp +++ b/velox/connectors/hive/storage_adapters/s3fs/S3FileSystem.cpp @@ -377,6 +377,10 @@ class S3WriteFile::Impl { request.SetContentLength(part.size()); request.SetBody( std::make_shared(part.data(), part.size())); + // The default algorithm used is MD5. However, MD5 is not supported with + // fips and can cause a SIGSEGV. Set CRC32 instead which is a standard for + // checksum computation and is not restricted by fips. + request.SetChecksumAlgorithm(Aws::S3::Model::ChecksumAlgorithm::CRC32); auto outcome = client_->UploadPart(request); VELOX_CHECK_AWS_OUTCOME(outcome, "Failed to upload", bucket_, key_); // Append ETag and part number for this uploaded part. From 3d10ccfc1706c45305d776b545a8f4ad0656d5a8 Mon Sep 17 00:00:00 2001 From: Ke Date: Fri, 23 Aug 2024 21:27:22 -0700 Subject: [PATCH 19/24] Make processConfigs function pure virtual (#10827) Summary: to enforce every data format implement its own processConfigs functions Pull Request resolved: https://github.com/facebookincubator/velox/pull/10827 Reviewed By: xiaoxmeng Differential Revision: D61743118 Pulled By: kewang1024 fbshipit-source-id: aa1d0b2ea12deaf5b2c2f2ee16827bd6c5a73f38 --- velox/dwio/common/Options.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/velox/dwio/common/Options.h b/velox/dwio/common/Options.h index a695e30b45b97..31a6fe0ccc91b 100644 --- a/velox/dwio/common/Options.h +++ b/velox/dwio/common/Options.h @@ -622,7 +622,7 @@ struct WriterOptions { // process format-specific session and connector configs. virtual void processConfigs( const config::ConfigBase& connectorConfig, - const config::ConfigBase& session) {}; + const config::ConfigBase& session) = 0; virtual ~WriterOptions() = default; }; From d33cdb2512052b1bc131908be0a7d26ad240d89d Mon Sep 17 00:00:00 2001 From: Jia Ke Date: Fri, 23 Aug 2024 22:40:39 -0700 Subject: [PATCH 20/24] Add RowsStreamingWindowBuild to avoid OOM in Window operator (#9025) Summary: Unlike `StreamingWindowBuild`, `RowLevelStreamingWindowBuild ` in this PR is capable of processing window functions as rows arrive within a single partition, without the need to wait for the entire partition to be ready. This approach can significantly reduce memory usage, especially when a single partition contains a large amount of data. It is particularly suited for optimizing `rank `and `row_number `functions, as well as aggregate window functions with a default frame. The detailed discussions is [here](https://github.com/facebookincubator/velox/discussions/8975). The design doc is [here](https://docs.google.com/document/d/17ONSJHK8XP5Lixm8XBl01RMNl4ntpixiVFe693ahw6k/edit?usp=sharing). Pull Request resolved: https://github.com/facebookincubator/velox/pull/9025 Test Plan: Run through 10hrs fuzzer testing Reviewed By: kagamiori Differential Revision: D61473798 Pulled By: xiaoxmeng fbshipit-source-id: 569a752770395330c48a3521bd5421eb89f5623d --- velox/exec/AggregateWindow.cpp | 1 + velox/exec/CMakeLists.txt | 3 +- ....cpp => PartitionStreamingWindowBuild.cpp} | 17 +- ...uild.h => PartitionStreamingWindowBuild.h} | 14 +- velox/exec/RowsStreamingWindowBuild.cpp | 93 +++++++ velox/exec/RowsStreamingWindowBuild.h | 82 ++++++ velox/exec/SortWindowBuild.cpp | 6 +- velox/exec/SortWindowBuild.h | 2 +- velox/exec/Window.cpp | 74 ++++- velox/exec/Window.h | 7 +- velox/exec/WindowBuild.h | 9 +- velox/exec/WindowFunction.cpp | 13 +- velox/exec/WindowFunction.h | 27 ++ velox/exec/WindowPartition.cpp | 135 +++++++-- velox/exec/WindowPartition.h | 90 +++++- velox/exec/tests/PlanBuilderTest.cpp | 6 +- velox/exec/tests/PlanNodeToStringTest.cpp | 6 +- .../exec/tests/WindowFunctionRegistryTest.cpp | 6 +- velox/exec/tests/WindowTest.cpp | 258 ++++++++++++++++++ velox/functions/lib/window/NthValue.cpp | 1 + velox/functions/lib/window/Ntile.cpp | 1 + velox/functions/lib/window/Rank.cpp | 24 +- velox/functions/lib/window/RowNumber.cpp | 1 + velox/functions/prestosql/window/CumeDist.cpp | 1 + .../prestosql/window/FirstLastValue.cpp | 1 + velox/functions/prestosql/window/LeadLag.cpp | 2 + 26 files changed, 815 insertions(+), 65 deletions(-) rename velox/exec/{StreamingWindowBuild.cpp => PartitionStreamingWindowBuild.cpp} (86%) rename velox/exec/{StreamingWindowBuild.h => PartitionStreamingWindowBuild.h} (84%) create mode 100644 velox/exec/RowsStreamingWindowBuild.cpp create mode 100644 velox/exec/RowsStreamingWindowBuild.h diff --git a/velox/exec/AggregateWindow.cpp b/velox/exec/AggregateWindow.cpp index cb32bd0779c3c..2bdad5342c6e6 100644 --- a/velox/exec/AggregateWindow.cpp +++ b/velox/exec/AggregateWindow.cpp @@ -410,6 +410,7 @@ void registerAggregateWindowFunction(const std::string& name) { exec::registerWindowFunction( name, std::move(signatures), + {exec::WindowFunction::ProcessMode::kRows, true}, [name]( const std::vector& args, const TypePtr& resultType, diff --git a/velox/exec/CMakeLists.txt b/velox/exec/CMakeLists.txt index c68a9af02ea59..48b63d0872df8 100644 --- a/velox/exec/CMakeLists.txt +++ b/velox/exec/CMakeLists.txt @@ -59,9 +59,11 @@ velox_add_library( OutputBufferManager.cpp PartitionedOutput.cpp PartitionFunction.cpp + PartitionStreamingWindowBuild.cpp PlanNodeStats.cpp PrefixSort.cpp ProbeOperatorState.cpp + RowsStreamingWindowBuild.cpp RowContainer.cpp RowNumber.cpp SortBuffer.cpp @@ -71,7 +73,6 @@ velox_add_library( SpillFile.cpp Spiller.cpp StreamingAggregation.cpp - StreamingWindowBuild.cpp Strings.cpp TableScan.cpp TableWriteMerge.cpp diff --git a/velox/exec/StreamingWindowBuild.cpp b/velox/exec/PartitionStreamingWindowBuild.cpp similarity index 86% rename from velox/exec/StreamingWindowBuild.cpp rename to velox/exec/PartitionStreamingWindowBuild.cpp index 2d855867ebd0e..f8deecf991946 100644 --- a/velox/exec/StreamingWindowBuild.cpp +++ b/velox/exec/PartitionStreamingWindowBuild.cpp @@ -14,24 +14,24 @@ * limitations under the License. */ -#include "velox/exec/StreamingWindowBuild.h" +#include "velox/exec/PartitionStreamingWindowBuild.h" namespace facebook::velox::exec { -StreamingWindowBuild::StreamingWindowBuild( +PartitionStreamingWindowBuild::PartitionStreamingWindowBuild( const std::shared_ptr& windowNode, velox::memory::MemoryPool* pool, const common::SpillConfig* spillConfig, tsan_atomic* nonReclaimableSection) : WindowBuild(windowNode, pool, spillConfig, nonReclaimableSection) {} -void StreamingWindowBuild::buildNextPartition() { +void PartitionStreamingWindowBuild::buildNextPartition() { partitionStartRows_.push_back(sortedRows_.size()); sortedRows_.insert(sortedRows_.end(), inputRows_.begin(), inputRows_.end()); inputRows_.clear(); } -void StreamingWindowBuild::addInput(RowVectorPtr input) { +void PartitionStreamingWindowBuild::addInput(RowVectorPtr input) { for (auto i = 0; i < inputChannels_.size(); ++i) { decodedInputVectors_[i].decode(*input->childAt(inputChannels_[i])); } @@ -53,14 +53,15 @@ void StreamingWindowBuild::addInput(RowVectorPtr input) { } } -void StreamingWindowBuild::noMoreInput() { +void PartitionStreamingWindowBuild::noMoreInput() { buildNextPartition(); // Help for last partition related calculations. partitionStartRows_.push_back(sortedRows_.size()); } -std::unique_ptr StreamingWindowBuild::nextPartition() { +std::shared_ptr +PartitionStreamingWindowBuild::nextPartition() { VELOX_CHECK_GT( partitionStartRows_.size(), 0, "No window partitions available") @@ -91,11 +92,11 @@ std::unique_ptr StreamingWindowBuild::nextPartition() { sortedRows_.data() + partitionStartRows_[currentPartition_], partitionSize); - return std::make_unique( + return std::make_shared( data_.get(), partition, inversedInputChannels_, sortKeyInfo_); } -bool StreamingWindowBuild::hasNextPartition() { +bool PartitionStreamingWindowBuild::hasNextPartition() { return partitionStartRows_.size() > 0 && currentPartition_ < int(partitionStartRows_.size() - 2); } diff --git a/velox/exec/StreamingWindowBuild.h b/velox/exec/PartitionStreamingWindowBuild.h similarity index 84% rename from velox/exec/StreamingWindowBuild.h rename to velox/exec/PartitionStreamingWindowBuild.h index a9c2e2abf4733..bb5cb352d24fc 100644 --- a/velox/exec/StreamingWindowBuild.h +++ b/velox/exec/PartitionStreamingWindowBuild.h @@ -20,13 +20,13 @@ namespace facebook::velox::exec { -/// The StreamingWindowBuild is used when the input data is already sorted by -/// {partition keys + order by keys}. The logic identifies partition changes -/// when receiving input rows and splits out WindowPartitions for the Window -/// operator to process. -class StreamingWindowBuild : public WindowBuild { +/// The PartitionStreamingWindowBuild is used when the input data is already +/// sorted by {partition keys + order by keys}. The logic identifies partition +/// changes when receiving input rows and splits out WindowPartitions for the +/// Window operator to process. +class PartitionStreamingWindowBuild : public WindowBuild { public: - StreamingWindowBuild( + PartitionStreamingWindowBuild( const std::shared_ptr& windowNode, velox::memory::MemoryPool* pool, const common::SpillConfig* spillConfig, @@ -46,7 +46,7 @@ class StreamingWindowBuild : public WindowBuild { bool hasNextPartition() override; - std::unique_ptr nextPartition() override; + std::shared_ptr nextPartition() override; bool needsInput() override { // No partitions are available or the currentPartition is the last available diff --git a/velox/exec/RowsStreamingWindowBuild.cpp b/velox/exec/RowsStreamingWindowBuild.cpp new file mode 100644 index 0000000000000..81d4a4f8d00ae --- /dev/null +++ b/velox/exec/RowsStreamingWindowBuild.cpp @@ -0,0 +1,93 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/RowsStreamingWindowBuild.h" +#include "velox/common/testutil/TestValue.h" + +namespace facebook::velox::exec { + +RowsStreamingWindowBuild::RowsStreamingWindowBuild( + const std::shared_ptr& windowNode, + velox::memory::MemoryPool* pool, + const common::SpillConfig* spillConfig, + tsan_atomic* nonReclaimableSection) + : WindowBuild(windowNode, pool, spillConfig, nonReclaimableSection) { + velox::common::testutil::TestValue::adjust( + "facebook::velox::exec::RowsStreamingWindowBuild::RowsStreamingWindowBuild", + this); +} + +void RowsStreamingWindowBuild::addPartitionInputs(bool finished) { + if (inputRows_.empty()) { + return; + } + + if (windowPartitions_.size() <= inputPartition_) { + windowPartitions_.push_back(std::make_shared( + data_.get(), inversedInputChannels_, sortKeyInfo_)); + } + + windowPartitions_[inputPartition_]->addRows(inputRows_); + + if (finished) { + windowPartitions_[inputPartition_]->setComplete(); + ++inputPartition_; + } + + inputRows_.clear(); +} + +void RowsStreamingWindowBuild::addInput(RowVectorPtr input) { + for (auto i = 0; i < inputChannels_.size(); ++i) { + decodedInputVectors_[i].decode(*input->childAt(inputChannels_[i])); + } + + for (auto row = 0; row < input->size(); ++row) { + char* newRow = data_->newRow(); + + for (auto col = 0; col < input->childrenSize(); ++col) { + data_->store(decodedInputVectors_[col], row, newRow, col); + } + + if (previousRow_ != nullptr && + compareRowsWithKeys(previousRow_, newRow, partitionKeyInfo_)) { + addPartitionInputs(true); + } + + if (previousRow_ != nullptr && inputRows_.size() >= numRowsPerOutput_) { + addPartitionInputs(false); + } + + inputRows_.push_back(newRow); + previousRow_ = newRow; + } +} + +void RowsStreamingWindowBuild::noMoreInput() { + addPartitionInputs(true); +} + +std::shared_ptr RowsStreamingWindowBuild::nextPartition() { + VELOX_CHECK(hasNextPartition()); + return windowPartitions_[++outputPartition_]; +} + +bool RowsStreamingWindowBuild::hasNextPartition() { + return !windowPartitions_.empty() && + outputPartition_ + 2 <= windowPartitions_.size(); +} + +} // namespace facebook::velox::exec diff --git a/velox/exec/RowsStreamingWindowBuild.h b/velox/exec/RowsStreamingWindowBuild.h new file mode 100644 index 0000000000000..c003f1e6a3f9b --- /dev/null +++ b/velox/exec/RowsStreamingWindowBuild.h @@ -0,0 +1,82 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "velox/exec/WindowBuild.h" + +namespace facebook::velox::exec { + +/// Unlike PartitionStreamingWindowBuild, RowsStreamingWindowBuild is capable of +/// processing window functions as rows arrive within a single partition, +/// without the need to wait for the entirewindow partition to be ready. This +/// approach can significantly reduce memory usage, especially when a single +/// partition contains a large amount of data. It is particularly suited for +/// optimizing rank, dense_rank and row_number functions, as well as aggregate +/// window functions with a default frame. +class RowsStreamingWindowBuild : public WindowBuild { + public: + RowsStreamingWindowBuild( + const std::shared_ptr& windowNode, + velox::memory::MemoryPool* pool, + const common::SpillConfig* spillConfig, + tsan_atomic* nonReclaimableSection); + + void addInput(RowVectorPtr input) override; + + void spill() override { + VELOX_UNREACHABLE(); + } + + std::optional spilledStats() const override { + return std::nullopt; + } + + void noMoreInput() override; + + bool hasNextPartition() override; + + std::shared_ptr nextPartition() override; + + bool needsInput() override { + // No partitions are available or the currentPartition is the last available + // one, so can consume input rows. + return windowPartitions_.empty() || + outputPartition_ == windowPartitions_.size() - 1; + } + + private: + // Adds input rows to the current partition, or creates a new partition if it + // does not exist. + void addPartitionInputs(bool finished); + + // Points to the input rows in the current partition. + std::vector inputRows_; + + // Used to compare rows based on partitionKeys. + char* previousRow_ = nullptr; + + // Point to the current output partition if not -1. + vector_size_t outputPartition_ = -1; + + // Current input partition that receives inputs. + vector_size_t inputPartition_ = 0; + + // Holds all the built window partitions. + std::vector> windowPartitions_; +}; + +} // namespace facebook::velox::exec diff --git a/velox/exec/SortWindowBuild.cpp b/velox/exec/SortWindowBuild.cpp index c65009012fdd4..400b1edbe636f 100644 --- a/velox/exec/SortWindowBuild.cpp +++ b/velox/exec/SortWindowBuild.cpp @@ -294,11 +294,11 @@ void SortWindowBuild::loadNextPartitionFromSpill() { } } -std::unique_ptr SortWindowBuild::nextPartition() { +std::shared_ptr SortWindowBuild::nextPartition() { if (merge_ != nullptr) { VELOX_CHECK(!sortedRows_.empty(), "No window partitions available") auto partition = folly::Range(sortedRows_.data(), sortedRows_.size()); - return std::make_unique( + return std::make_shared( data_.get(), partition, inversedInputChannels_, sortKeyInfo_); } @@ -316,7 +316,7 @@ std::unique_ptr SortWindowBuild::nextPartition() { auto partition = folly::Range( sortedRows_.data() + partitionStartRows_[currentPartition_], partitionSize); - return std::make_unique( + return std::make_shared( data_.get(), partition, inversedInputChannels_, sortKeyInfo_); } diff --git a/velox/exec/SortWindowBuild.h b/velox/exec/SortWindowBuild.h index 645949ddb7e02..0caecfe6a5c37 100644 --- a/velox/exec/SortWindowBuild.h +++ b/velox/exec/SortWindowBuild.h @@ -53,7 +53,7 @@ class SortWindowBuild : public WindowBuild { bool hasNextPartition() override; - std::unique_ptr nextPartition() override; + std::shared_ptr nextPartition() override; private: void ensureInputFits(const RowVectorPtr& input); diff --git a/velox/exec/Window.cpp b/velox/exec/Window.cpp index 4577388006fa2..e4d36d994591f 100644 --- a/velox/exec/Window.cpp +++ b/velox/exec/Window.cpp @@ -15,8 +15,9 @@ */ #include "velox/exec/Window.h" #include "velox/exec/OperatorUtils.h" +#include "velox/exec/PartitionStreamingWindowBuild.h" +#include "velox/exec/RowsStreamingWindowBuild.h" #include "velox/exec/SortWindowBuild.h" -#include "velox/exec/StreamingWindowBuild.h" #include "velox/exec/Task.h" namespace facebook::velox::exec { @@ -41,8 +42,13 @@ Window::Window( auto* spillConfig = spillConfig_.has_value() ? &spillConfig_.value() : nullptr; if (windowNode->inputsSorted()) { - windowBuild_ = std::make_unique( - windowNode, pool(), spillConfig, &nonReclaimableSection_); + if (supportRowsStreaming()) { + windowBuild_ = std::make_unique( + windowNode_, pool(), spillConfig, &nonReclaimableSection_); + } else { + windowBuild_ = std::make_unique( + windowNode, pool(), spillConfig, &nonReclaimableSection_); + } } else { windowBuild_ = std::make_unique( windowNode, pool(), spillConfig, &nonReclaimableSection_, &spillStats_); @@ -54,6 +60,7 @@ void Window::initialize() { VELOX_CHECK_NOT_NULL(windowNode_); createWindowFunctions(); createPeerAndFrameBuffers(); + windowBuild_->setNumRowsPerOutput(numRowsPerOutput_); windowNode_.reset(); } @@ -188,6 +195,31 @@ void Window::createWindowFunctions() { } } +bool Window::supportRowsStreaming() { + for (const auto& windowFunction : windowNode_->windowFunctions()) { + const auto& functionName = windowFunction.functionCall->name(); + const auto windowFunctionMetadata = + exec::getWindowFunctionMetadata(functionName); + + if (windowFunctionMetadata.processMode != + exec::WindowFunction::ProcessMode::kRows) { + return false; + } + + const auto& frame = windowFunction.frame; + // The default frame spans from the start of the partition to current row. + const bool isDefaultFrame = + (frame.startType == core::WindowNode::BoundType::kUnboundedPreceding && + frame.endType == core::WindowNode::BoundType::kCurrentRow); + + if (windowFunctionMetadata.isAggregate && !isDefaultFrame) { + return false; + } + } + + return true; +} + void Window::addInput(RowVectorPtr input) { windowBuild_->addInput(input); numRows_ += input->size(); @@ -542,9 +574,13 @@ void Window::callApplyForPartitionRows( vector_size_t endRow, vector_size_t resultOffset, const RowVectorPtr& result) { - getInputColumns(startRow, endRow, resultOffset, result); - + // NOTE: for a partial window partition, the last row of the previously + // processed rows (used for peer group comparison) will be deleted by + // computePeerAndFrameBuffers after peer group comparison. Hence we need to + // call getInputColumns after computePeerAndFrameBuffers. computePeerAndFrameBuffers(startRow, endRow); + + getInputColumns(startRow, endRow, resultOffset, result); vector_size_t numFuncs = windowFunctions_.size(); for (auto i = 0; i < numFuncs; ++i) { windowFunctions_[i]->apply( @@ -560,6 +596,10 @@ void Window::callApplyForPartitionRows( const vector_size_t numRows = endRow - startRow; numProcessedRows_ += numRows; partitionOffset_ += numRows; + + if (currentPartition_->partial()) { + currentPartition_->removeProcessedRows(numRows); + } } vector_size_t Window::callApplyLoop( @@ -573,18 +613,25 @@ vector_size_t Window::callApplyLoop( // This function requires that the currentPartition_ is available for output. VELOX_DCHECK_NOT_NULL(currentPartition_); while (numOutputRowsLeft > 0) { - const auto rowsForCurrentPartition = - currentPartition_->numRows() - partitionOffset_; - if (rowsForCurrentPartition <= numOutputRowsLeft) { + const auto numPartitionRows = + currentPartition_->numRowsForProcessing(partitionOffset_); + if (numPartitionRows <= numOutputRowsLeft) { // Current partition can fit completely in the output buffer. // So output all its rows. callApplyForPartitionRows( partitionOffset_, - partitionOffset_ + rowsForCurrentPartition, + partitionOffset_ + numPartitionRows, resultIndex, result); - resultIndex += rowsForCurrentPartition; - numOutputRowsLeft -= rowsForCurrentPartition; + resultIndex += numPartitionRows; + numOutputRowsLeft -= numPartitionRows; + + if (!currentPartition_->complete()) { + // There are more data need to process for a partial partition. + VELOX_CHECK(currentPartition_->partial()); + break; + } + callResetPartition(); if (currentPartition_ == nullptr) { // The WindowBuild doesn't have any more partitions to process right @@ -627,6 +674,11 @@ RowVectorPtr Window::getOutput() { } } + if (!currentPartition_->complete() && + (currentPartition_->numRowsForProcessing(partitionOffset_) == 0)) { + return nullptr; + } + const auto numOutputRows = std::min(numRowsPerOutput_, numRowsLeft); auto result = BaseVector::create( outputType_, numOutputRows, operatorCtx_->pool()); diff --git a/velox/exec/Window.h b/velox/exec/Window.h index 393bcc364accb..eb3389369a230 100644 --- a/velox/exec/Window.h +++ b/velox/exec/Window.h @@ -88,6 +88,11 @@ class Window : public Operator { const std::optional end; }; + // Returns if a window operator support rows-wise streaming processing or not. + // Currently we supports 'rank', 'dense_rank' and 'row_number' functions with + // any frame type. Also supports the agg window function with default frame. + bool supportRowsStreaming(); + // Creates WindowFunction and frame objects for this operator. void createWindowFunctions(); @@ -165,7 +170,7 @@ class Window : public Operator { // Used to access window partition rows and columns by the window // operator and functions. This structure is owned by the WindowBuild. - std::unique_ptr currentPartition_; + std::shared_ptr currentPartition_; // HashStringAllocator required by functions that allocate out of line // buffers. diff --git a/velox/exec/WindowBuild.h b/velox/exec/WindowBuild.h index 1f9207c4fbd55..01c470803ed70 100644 --- a/velox/exec/WindowBuild.h +++ b/velox/exec/WindowBuild.h @@ -66,7 +66,7 @@ class WindowBuild { /// access the underlying columns of Window partition data. Check /// hasNextPartition() before invoking this function. This function fails if /// called when no partition is available. - virtual std::unique_ptr nextPartition() = 0; + virtual std::shared_ptr nextPartition() = 0; /// Returns the average size of input rows in bytes stored in the data /// container of the WindowBuild. @@ -74,6 +74,10 @@ class WindowBuild { return data_->estimateRowSize(); } + void setNumRowsPerOutput(vector_size_t numRowsPerOutput) { + numRowsPerOutput_ = numRowsPerOutput; + } + protected: bool compareRowsWithKeys( const char* lhs, @@ -111,6 +115,9 @@ class WindowBuild { /// Number of input rows. vector_size_t numRows_ = 0; + + // The maximum number of rows that can fit into an output block. + vector_size_t numRowsPerOutput_; }; } // namespace facebook::velox::exec diff --git a/velox/exec/WindowFunction.cpp b/velox/exec/WindowFunction.cpp index b093024a1dbec..6eea3160b21ea 100644 --- a/velox/exec/WindowFunction.cpp +++ b/velox/exec/WindowFunction.cpp @@ -41,13 +41,24 @@ std::optional getWindowFunctionEntry( bool registerWindowFunction( const std::string& name, std::vector signatures, + WindowFunction::Metadata metadata, WindowFunctionFactory factory) { auto sanitizedName = sanitizeName(name); windowFunctions()[sanitizedName] = { - std::move(signatures), std::move(factory)}; + std::move(signatures), std::move(factory), std::move(metadata)}; return true; } +WindowFunction::Metadata getWindowFunctionMetadata(const std::string& name) { + const auto sanitizedName = sanitizeName(name); + if (auto func = getWindowFunctionEntry(sanitizedName)) { + return func.value()->metadata; + } else { + VELOX_USER_FAIL( + "Window function metadata not found for function: {}", name); + } +} + std::optional> getWindowFunctionSignatures( const std::string& name) { auto sanitizedName = sanitizeName(name); diff --git a/velox/exec/WindowFunction.h b/velox/exec/WindowFunction.h index ee0ef26869c1b..e9bea92ee2c23 100644 --- a/velox/exec/WindowFunction.h +++ b/velox/exec/WindowFunction.h @@ -33,6 +33,28 @@ struct WindowFunctionArg { class WindowFunction { public: + /// The data process mode for calculating the window function. + enum class ProcessMode { + /// Process can only start after all the rows from a partition become + /// available. + kPartition, + /// Process can start as soon as rows are available within a partition, + /// without waiting for all the rows in the partition to be ready. + kRows, + }; + + /// Indicates whether this is an aggregate window function and its process + /// unit. + struct Metadata { + ProcessMode processMode; + bool isAggregate; + + static Metadata defaultMetadata() { + static Metadata defaultValue{ProcessMode::kPartition, false}; + return defaultValue; + } + }; + explicit WindowFunction( TypePtr resultType, memory::MemoryPool* pool, @@ -149,6 +171,7 @@ using WindowFunctionFactory = std::function( bool registerWindowFunction( const std::string& name, std::vector signatures, + WindowFunction::Metadata metadata, WindowFunctionFactory factory); /// Returns signatures of the window function with the specified name. @@ -159,8 +182,12 @@ std::optional> getWindowFunctionSignatures( struct WindowFunctionEntry { std::vector signatures; WindowFunctionFactory factory; + WindowFunction::Metadata metadata; }; +/// Returns window function metadata. +WindowFunction::Metadata getWindowFunctionMetadata(const std::string& name); + using WindowFunctionMap = std::unordered_map; /// Returns a map of all window function names to their registrations. diff --git a/velox/exec/WindowPartition.cpp b/velox/exec/WindowPartition.cpp index 2783b6b7b2f3b..f57077364c348 100644 --- a/velox/exec/WindowPartition.cpp +++ b/velox/exec/WindowPartition.cpp @@ -21,13 +21,70 @@ WindowPartition::WindowPartition( RowContainer* data, const folly::Range& rows, const std::vector& inputMapping, - const std::vector>& sortKeyInfo) - : data_(data), + const std::vector>& sortKeyInfo, + bool partial, + bool complete) + : partial_(partial), + data_(data), partition_(rows), + complete_(complete), inputMapping_(inputMapping), sortKeyInfo_(sortKeyInfo) { - for (int i = 0; i < inputMapping_.size(); i++) { - columns_.emplace_back(data_->columnAt(inputMapping_[i])); + VELOX_CHECK_NE(partial_, complete_); + VELOX_CHECK_NE(complete_, partition_.empty()); + + for (auto index : inputMapping_) { + columns_.emplace_back(data_->columnAt(index)); + } +} + +WindowPartition::WindowPartition( + RowContainer* data, + const folly::Range& rows, + const std::vector& inputMapping, + const std::vector>& sortKeyInfo) + : WindowPartition(data, rows, inputMapping, sortKeyInfo, false, true) {} + +WindowPartition::WindowPartition( + RowContainer* data, + const std::vector& inputMapping, + const std::vector>& sortKeyInfo) + : WindowPartition(data, {}, inputMapping, sortKeyInfo, true, false) {} + +void WindowPartition::addRows(const std::vector& rows) { + checkPartial(); + rows_.insert(rows_.end(), rows.begin(), rows.end()); + partition_ = folly::Range(rows_.data(), rows_.size()); +} + +void WindowPartition::eraseRows(vector_size_t numRows) { + checkPartial(); + VELOX_CHECK_GE(data_->numRows(), numRows); + data_->eraseRows(folly::Range(rows_.data(), numRows)); +} + +void WindowPartition::removeProcessedRows(vector_size_t numRows) { + checkPartial(); + + VELOX_CHECK_NULL(previousRow_); + if (complete_ && rows_.size() == numRows) { + eraseRows(numRows); + } else { + eraseRows(numRows - 1); + previousRow_ = rows_[numRows - 1]; + } + + rows_.erase(rows_.begin(), rows_.begin() + numRows); + partition_ = folly::Range(rows_.data(), rows_.size()); + startRow_ += numRows; +} + +vector_size_t WindowPartition::numRowsForProcessing( + vector_size_t partitionOffset) const { + if (partial_) { + return partition_.size(); + } else { + return partition_.size() - partitionOffset; } } @@ -50,8 +107,9 @@ void WindowPartition::extractColumn( vector_size_t numRows, vector_size_t resultOffset, const VectorPtr& result) const { + VELOX_CHECK_GE(partitionOffset, startRow_); RowContainer::extractColumn( - partition_.data() + partitionOffset, + partition_.data() + partitionOffset - startRow_, numRows, columns_[columnIndex], resultOffset, @@ -130,23 +188,65 @@ bool WindowPartition::compareRowsWithSortKeys(const char* lhs, const char* rhs) return false; } +vector_size_t WindowPartition::findPeerRowEndIndex( + vector_size_t startRow, + vector_size_t lastRow, + const std::function& peerCompare) { + auto peerEnd = startRow; + while (peerEnd <= lastRow) { + if (peerCompare( + partition_[startRow - startRow_], + partition_[peerEnd - startRow_])) { + break; + } + ++peerEnd; + } + return peerEnd; +} + +void WindowPartition::removePreviousRow() { + VELOX_CHECK_NOT_NULL(previousRow_); + data_->eraseRows(folly::Range(&previousRow_, 1)); + previousRow_ = nullptr; +} + std::pair WindowPartition::computePeerBuffers( vector_size_t start, vector_size_t end, vector_size_t prevPeerStart, vector_size_t prevPeerEnd, vector_size_t* rawPeerStarts, - vector_size_t* rawPeerEnds) const { + vector_size_t* rawPeerEnds) { const auto peerCompare = [&](const char* lhs, const char* rhs) -> bool { return compareRowsWithSortKeys(lhs, rhs); }; - VELOX_CHECK_LE(end, numRows()); + VELOX_CHECK_LE(end, numRows() + startRow_); - const auto lastPartitionRow = numRows() - 1; + auto lastPartitionRow = numRows() + startRow_ - 1; auto peerStart = prevPeerStart; auto peerEnd = prevPeerEnd; - for (auto i = start, j = 0; i < end; ++i, ++j) { + + size_t next = start; + size_t index{0}; + if (partial_ && start > 0) { + const auto peerGroup = peerCompare(previousRow_, partition_[0]); + + // The first row is the last row in previous batch so delete it after used + // for the first peer group detection. + removePreviousRow(); + + if (!peerGroup) { + peerEnd = findPeerRowEndIndex(start, lastPartitionRow, peerCompare); + + for (; next < std::min(end, peerEnd); ++next, ++index) { + rawPeerStarts[index] = peerStart; + rawPeerEnds[index] = peerEnd - 1; + } + } + } + + for (; next < end; ++next, ++index) { // When traversing input partition rows, the peers are the rows with the // same values for the ORDER BY clause. These rows are equal in some ways // and affect the results of ranking functions. This logic exploits the fact @@ -155,22 +255,17 @@ std::pair WindowPartition::computePeerBuffers( // across the rows in that peer interval. Note: peerStart and peerEnd can be // maintained across getOutput calls. Hence, they are returned to the // caller. - if (i == 0 || i >= peerEnd) { + if (next == 0 || next >= peerEnd) { // Compute peerStart and peerEnd rows for the first row of the partition // or when past the previous peerGroup. - peerStart = i; - peerEnd = i; - while (peerEnd <= lastPartitionRow) { - if (peerCompare(partition_[peerStart], partition_[peerEnd])) { - break; - } - ++peerEnd; - } + peerStart = next; + peerEnd = findPeerRowEndIndex(peerStart, lastPartitionRow, peerCompare); } - rawPeerStarts[j] = peerStart; - rawPeerEnds[j] = peerEnd - 1; + rawPeerStarts[index] = peerStart; + rawPeerEnds[index] = peerEnd - 1; } + VELOX_CHECK_EQ(index, end - start); return {peerStart, peerEnd}; } diff --git a/velox/exec/WindowPartition.h b/velox/exec/WindowPartition.h index 7073af3a42383..7948611a98295 100644 --- a/velox/exec/WindowPartition.h +++ b/velox/exec/WindowPartition.h @@ -20,9 +20,14 @@ /// Simple WindowPartition that builds over the RowContainer used for storing /// the input rows in the Window Operator. This works completely in-memory. +/// WindowPartition supports partial window partitioning to facilitate +/// RowsStreamingWindowBuild which can start data processing with a portion set +/// of rows without having to wait until an entire partition of rows are ready. + /// TODO: This implementation will be revised for Spill to disk semantics. namespace facebook::velox::exec { + class WindowPartition { public: /// The WindowPartition is used by the Window operator and WindowFunction @@ -42,11 +47,46 @@ class WindowPartition { const std::vector>& sortKeyInfo); + /// The WindowPartition is used for RowStreamingWindowBuild which allows to + /// start data processing with a subset of partition rows. 'partial_' flag is + /// set for the constructed window partition. + WindowPartition( + RowContainer* data, + const std::vector& inputMapping, + const std::vector>& + sortKeyInfo); + + /// Adds remaining input 'rows' for a partial window partition. + void addRows(const std::vector& rows); + + /// Removes the first 'numRows' in 'rows_' from a partial window partition + /// after been processed. + void removeProcessedRows(vector_size_t numRows); + /// Returns the number of rows in the current WindowPartition. vector_size_t numRows() const { return partition_.size(); } + /// Returns the number of rows in a window partition remaining for data + /// processing. + vector_size_t numRowsForProcessing(vector_size_t partitionOffset) const; + + bool complete() const { + return complete_; + } + + bool partial() const { + return partial_; + } + + void setComplete() { + VELOX_CHECK(!complete_); + checkPartial(); + + complete_ = true; + } + /// Copies the values at 'columnIndex' into 'result' (starting at /// 'resultOffset') for the rows at positions in the 'rowNumbers' /// array from the partition input data. @@ -107,7 +147,7 @@ class WindowPartition { vector_size_t prevPeerStart, vector_size_t prevPeerEnd, vector_size_t* rawPeerStarts, - vector_size_t* rawPeerEnds) const; + vector_size_t* rawPeerEnds); /// Sets in 'rawFrameBounds' the frame boundary for the k range /// preceding/following frame. @@ -128,8 +168,33 @@ class WindowPartition { vector_size_t* rawFrameBounds) const; private: + WindowPartition( + RowContainer* data, + const folly::Range& rows, + const std::vector& inputMapping, + const std::vector>& + sortKeyInfo, + bool partial, + bool complete); + bool compareRowsWithSortKeys(const char* lhs, const char* rhs) const; + // Finds the index of the last peer row in range of ['startRow', 'lastRow']. + vector_size_t findPeerRowEndIndex( + vector_size_t startRow, + vector_size_t lastRow, + const std::function& peerCompare); + + // Removes 'numRows' from 'data_' and 'rows_'. + void eraseRows(vector_size_t numRows); + + void checkPartial() const { + VELOX_CHECK(partial_, "WindowPartition should be partial"); + } + + // Removes the previous row from 'data_'. + void removePreviousRow(); + // Searches for 'currentRow[frameColumn]' in 'orderByColumn' of rows between // 'start' and 'end' in the partition. 'firstMatch' specifies if first or last // row is matched. @@ -162,9 +227,16 @@ class WindowPartition { const vector_size_t* rawPeerBounds, vector_size_t* rawFrameBounds) const; + // Indicates if this is a partial partition for RowStreamWindowBuild + // processing. + const bool partial_; + // The RowContainer associated with the partition. // It is owned by the WindowBuild that creates the partition. - RowContainer* data_; + RowContainer* const data_; + + // Points to the input rows for partial partition. + std::vector rows_; // folly::Range is for the partition rows iterator provided by the // Window operator. The pointers are to rows from a RowContainer owned @@ -172,6 +244,10 @@ class WindowPartition { // of WindowPartition. folly::Range partition_; + // Indicates if a partial partition has received all the input rows. For a + // non-partial partition, this is always true. + bool complete_ = true; + // Mapping from window input column -> index in data_. This is required // because the WindowBuild reorders data_ to place partition and sort keys // before other columns in data_. But the Window Operator and Function code @@ -189,5 +265,15 @@ class WindowPartition { // corresponding indexes of their input arguments into this vector. // They will request for column vector values at the respective index. std::vector columns_; + + // The partition offset of the first row in 'rows_'. It is updated for + // partial partition during the data processing but always zero for + // non-partial partition. + vector_size_t startRow_{0}; + + // Points to the last row from the previous processed peer group if not null. + // This is only set for a partial window partition and always null for a + // non-partial one. + char* previousRow_{nullptr}; }; } // namespace facebook::velox::exec diff --git a/velox/exec/tests/PlanBuilderTest.cpp b/velox/exec/tests/PlanBuilderTest.cpp index 5204e92744035..c4b22f3678b81 100644 --- a/velox/exec/tests/PlanBuilderTest.cpp +++ b/velox/exec/tests/PlanBuilderTest.cpp @@ -98,7 +98,11 @@ void registerWindowFunction() { .returnType("BIGINT") .build(), }; - exec::registerWindowFunction("window1", std::move(signatures), nullptr); + exec::registerWindowFunction( + "window1", + std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), + nullptr); } } // namespace diff --git a/velox/exec/tests/PlanNodeToStringTest.cpp b/velox/exec/tests/PlanNodeToStringTest.cpp index b40463e4d55ca..59485b89478f2 100644 --- a/velox/exec/tests/PlanNodeToStringTest.cpp +++ b/velox/exec/tests/PlanNodeToStringTest.cpp @@ -729,7 +729,11 @@ TEST_F(PlanNodeToStringTest, window) { .returnType("BIGINT") .build(), }; - exec::registerWindowFunction("window1", std::move(signatures), nullptr); + exec::registerWindowFunction( + "window1", + std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), + nullptr); auto plan = PlanBuilder() diff --git a/velox/exec/tests/WindowFunctionRegistryTest.cpp b/velox/exec/tests/WindowFunctionRegistryTest.cpp index b0aee416caf73..f78b18a050248 100644 --- a/velox/exec/tests/WindowFunctionRegistryTest.cpp +++ b/velox/exec/tests/WindowFunctionRegistryTest.cpp @@ -37,7 +37,11 @@ void registerWindowFunction(const std::string& name) { .build(), exec::FunctionSignatureBuilder().returnType("date").build(), }; - exec::registerWindowFunction(name, std::move(signatures), nullptr); + exec::registerWindowFunction( + name, + std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), + nullptr); } } // namespace diff --git a/velox/exec/tests/WindowTest.cpp b/velox/exec/tests/WindowTest.cpp index febdcd743d30a..a55b6f4598ab2 100644 --- a/velox/exec/tests/WindowTest.cpp +++ b/velox/exec/tests/WindowTest.cpp @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +#include "velox/common/base/Exceptions.h" #include "velox/common/base/tests/GTestUtils.h" #include "velox/common/file/FileSystems.h" #include "velox/exec/PlanNodeStats.h" +#include "velox/exec/RowsStreamingWindowBuild.h" #include "velox/exec/tests/utils/AssertQueryBuilder.h" #include "velox/exec/tests/utils/OperatorTestBase.h" #include "velox/exec/tests/utils/PlanBuilder.h" @@ -79,6 +81,262 @@ TEST_F(WindowTest, spill) { ASSERT_GT(stats.spilledPartitions, 0); } +TEST_F(WindowTest, rowBasedStreamingWindowOOM) { + const vector_size_t size = 1'000'000; + auto data = makeRowVector( + {"d", "p", "s"}, + { + // Payload. + makeFlatVector(size, [](auto row) { return row; }), + // Partition key. + makeFlatVector(size, [](auto row) { return row; }), + // Sorting key. + makeFlatVector(size, [](auto row) { return row; }), + }); + + createDuckDbTable({data}); + + // Abstract the common values vector split. + auto valuesSplit = split(data, 10); + + auto planNodeIdGenerator = std::make_shared(); + CursorParameters params; + auto queryCtx = core::QueryCtx::create(executor_.get()); + queryCtx->testingOverrideMemoryPool(memory::memoryManager()->addRootPool( + queryCtx->queryId(), + 8'388'608 /* 8MB */, + exec::MemoryReclaimer::create())); + + params.queryCtx = queryCtx; + + auto testWindowBuild = [&](bool useStreamingWindow) { + if (useStreamingWindow) { + params.planNode = + PlanBuilder(planNodeIdGenerator) + .values(valuesSplit) + .streamingWindow( + {"row_number() over (partition by p order by s)"}) + .project({"d"}) + .singleAggregation({}, {"sum(d)"}) + .planNode(); + + readCursor(params, [](Task*) {}); + } else { + params.planNode = + PlanBuilder(planNodeIdGenerator) + .values(valuesSplit) + .window({"row_number() over (partition by p order by s)"}) + .project({"d"}) + .singleAggregation({}, {"sum(d)"}) + .planNode(); + + VELOX_ASSERT_THROW( + readCursor(params, [](Task*) {}), + "Exceeded memory pool capacity after attempt to grow capacity through arbitration."); + } + }; + // RowStreamingWindow will not OOM. + testWindowBuild(true); + // SortBasedWindow will OOM. + testWindowBuild(false); +} + +TEST_F(WindowTest, rowBasedStreamingWindowMemoryUsage) { + auto memoryUsage = [&](bool useStreamingWindow, vector_size_t size) { + auto data = makeRowVector( + {"d", "p", "s"}, + { + // Payload. + makeFlatVector(size, [](auto row) { return row; }), + // Partition key. + makeFlatVector(size, [](auto row) { return row % 11; }), + // Sorting key. + makeFlatVector(size, [](auto row) { return row; }), + }); + + createDuckDbTable({data}); + + // Abstract the common values vector split. + auto valuesSplit = split(data, 10); + core::PlanNodeId windowId; + auto builder = PlanBuilder().values(valuesSplit); + if (useStreamingWindow) { + builder.orderBy({"p", "s"}, false) + .streamingWindow({"row_number() over (partition by p order by s)"}); + } else { + builder.window({"row_number() over (partition by p order by s)"}); + } + auto plan = builder.capturePlanNodeId(windowId).planNode(); + auto task = + AssertQueryBuilder(plan, duckDbQueryRunner_) + .config(core::QueryConfig::kPreferredOutputBatchBytes, "1024") + .assertResults( + "SELECT *, row_number() over (partition by p order by s) FROM tmp"); + + return exec::toPlanStats(task->taskStats()).at(windowId).peakMemoryBytes; + }; + + const vector_size_t smallSize = 100'000; + const vector_size_t largeSize = 1'000'000; + // As the volume of data increases, the peak memory usage of the sort-based + // window will increase (2418624 vs 17098688). Since the peak memory usage of + // the RowBased Window represents the one batch data in a single partition, + // the peak memory usage will not increase as the volume of data grows. + auto sortWindowSmallUsage = memoryUsage(false, smallSize); + auto sortWindowLargeUsage = memoryUsage(false, largeSize); + ASSERT_GT(sortWindowLargeUsage, sortWindowSmallUsage); + + auto rowWindowSmallUsage = memoryUsage(true, smallSize); + auto rowWindowLargeUsage = memoryUsage(true, largeSize); + ASSERT_EQ(rowWindowSmallUsage, rowWindowLargeUsage); +} + +DEBUG_ONLY_TEST_F(WindowTest, rankRowStreamingWindowBuild) { + auto data = makeRowVector( + {"c1"}, + {makeFlatVector(std::vector{1, 1, 1, 1, 1, 2, 2})}); + + createDuckDbTable({data}); + + const std::vector kClauses = { + "rank() over (order by c1 rows unbounded preceding)"}; + + auto plan = PlanBuilder() + .values({data}) + .orderBy({"c1"}, false) + .streamingWindow(kClauses) + .planNode(); + + std::atomic_bool isStreamCreated{false}; + SCOPED_TESTVALUE_SET( + "facebook::velox::exec::RowsStreamingWindowBuild::RowsStreamingWindowBuild", + std::function( + [&](RowsStreamingWindowBuild* windowBuild) { + isStreamCreated.store(true); + })); + + AssertQueryBuilder(plan, duckDbQueryRunner_) + .config(core::QueryConfig::kPreferredOutputBatchBytes, "1024") + .config(core::QueryConfig::kPreferredOutputBatchRows, "2") + .config(core::QueryConfig::kMaxOutputBatchRows, "2") + .assertResults( + "SELECT *, rank() over (order by c1 rows unbounded preceding) FROM tmp"); + + ASSERT_TRUE(isStreamCreated.load()); +} + +DEBUG_ONLY_TEST_F(WindowTest, valuesRowsStreamingWindowBuild) { + const vector_size_t size = 1'00; + + auto data = makeRowVector( + {makeFlatVector(size, [](auto row) { return row % 5; }), + makeFlatVector(size, [](auto row) { return row % 50; }), + makeFlatVector( + size, [](auto row) { return row % 3 + 1; }, nullEvery(5)), + makeFlatVector(size, [](auto row) { return row % 40; }), + makeFlatVector(size, [](auto row) { return row; })}); + + createDuckDbTable({data}); + + const std::vector kClauses = { + "rank() over (partition by c0, c2 order by c1, c3)", + "dense_rank() over (partition by c0, c2 order by c1, c3)", + "row_number() over (partition by c0, c2 order by c1, c3)", + "sum(c4) over (partition by c0, c2 order by c1, c3)"}; + + auto plan = PlanBuilder() + .values({split(data, 10)}) + .orderBy({"c0", "c2", "c1", "c3"}, false) + .streamingWindow(kClauses) + .planNode(); + + std::atomic_bool isStreamCreated{false}; + SCOPED_TESTVALUE_SET( + "facebook::velox::exec::RowsStreamingWindowBuild::RowsStreamingWindowBuild", + std::function( + [&](RowsStreamingWindowBuild* windowBuild) { + isStreamCreated.store(true); + })); + + AssertQueryBuilder(plan, duckDbQueryRunner_) + .config(core::QueryConfig::kPreferredOutputBatchBytes, "1024") + .assertResults( + "SELECT *, rank() over (partition by c0, c2 order by c1, c3), dense_rank() over (partition by c0, c2 order by c1, c3), row_number() over (partition by c0, c2 order by c1, c3), sum(c4) over (partition by c0, c2 order by c1, c3) FROM tmp"); + ASSERT_TRUE(isStreamCreated.load()); +} + +DEBUG_ONLY_TEST_F(WindowTest, aggregationWithNonDefaultFrame) { + const vector_size_t size = 1'00; + + auto data = makeRowVector( + {makeFlatVector(size, [](auto row) { return row % 5; }), + makeFlatVector(size, [](auto row) { return row % 50; }), + makeFlatVector( + size, [](auto row) { return row % 3 + 1; }, nullEvery(5)), + makeFlatVector(size, [](auto row) { return row % 40; }), + makeFlatVector(size, [](auto row) { return row; })}); + + createDuckDbTable({data}); + + const std::vector kClauses = { + "sum(c4) over (partition by c0, c2 order by c1, c3 range between unbounded preceding and unbounded following)"}; + + auto plan = PlanBuilder() + .values({split(data, 10)}) + .orderBy({"c0", "c2", "c1", "c3"}, false) + .streamingWindow(kClauses) + .planNode(); + + std::atomic_bool isStreamCreated{false}; + SCOPED_TESTVALUE_SET( + "facebook::velox::exec::RowsStreamingWindowBuild::RowsStreamingWindowBuild", + std::function( + [&](RowsStreamingWindowBuild* windowBuild) { + isStreamCreated.store(true); + })); + + AssertQueryBuilder(plan, duckDbQueryRunner_) + .config(core::QueryConfig::kPreferredOutputBatchBytes, "1024") + .assertResults( + "SELECT *, sum(c4) over (partition by c0, c2 order by c1, c3 range between unbounded preceding and unbounded following) FROM tmp"); + + ASSERT_FALSE(isStreamCreated.load()); +} + +DEBUG_ONLY_TEST_F(WindowTest, nonRowsStreamingWindow) { + auto data = makeRowVector( + {"c1"}, + {makeFlatVector(std::vector{1, 1, 1, 1, 1, 2, 2})}); + + createDuckDbTable({data}); + + const std::vector kClauses = { + "first_value(c1) over (order by c1 rows unbounded preceding)", + "nth_value(c1, 1) over (order by c1 rows unbounded preceding)"}; + + auto plan = PlanBuilder() + .values({data}) + .orderBy({"c1"}, false) + .streamingWindow(kClauses) + .planNode(); + + std::atomic_bool isStreamCreated{false}; + SCOPED_TESTVALUE_SET( + "facebook::velox::exec::RowsStreamingWindowBuild::RowsStreamingWindowBuild", + std::function( + [&](RowsStreamingWindowBuild* windowBuild) { + isStreamCreated.store(true); + })); + + AssertQueryBuilder(plan, duckDbQueryRunner_) + .config(core::QueryConfig::kPreferredOutputBatchBytes, "1024") + .config(core::QueryConfig::kPreferredOutputBatchRows, "2") + .config(core::QueryConfig::kMaxOutputBatchRows, "2") + .assertResults( + "SELECT *, first_value(c1) over (order by c1 rows unbounded preceding), nth_value(c1, 1) over (order by c1 rows unbounded preceding) FROM tmp"); + ASSERT_FALSE(isStreamCreated.load()); +} + TEST_F(WindowTest, missingFunctionSignature) { auto input = {makeRowVector({ makeFlatVector({1, 2, 3}), diff --git a/velox/functions/lib/window/NthValue.cpp b/velox/functions/lib/window/NthValue.cpp index 8fd456c620cc4..5ef33fc303cd0 100644 --- a/velox/functions/lib/window/NthValue.cpp +++ b/velox/functions/lib/window/NthValue.cpp @@ -319,6 +319,7 @@ void registerNthValue(const std::string& name, TypeKind offsetTypeKind) { exec::registerWindowFunction( name, std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), [name]( const std::vector& args, const TypePtr& resultType, diff --git a/velox/functions/lib/window/Ntile.cpp b/velox/functions/lib/window/Ntile.cpp index eacd562d2744f..8b4460b90b42c 100644 --- a/velox/functions/lib/window/Ntile.cpp +++ b/velox/functions/lib/window/Ntile.cpp @@ -241,6 +241,7 @@ void registerNtile(const std::string& name, const std::string& type) { exec::registerWindowFunction( name, std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), [name]( const std::vector& args, const TypePtr& resultType, diff --git a/velox/functions/lib/window/Rank.cpp b/velox/functions/lib/window/Rank.cpp index 646557a2e30ce..625d2bd3b977f 100644 --- a/velox/functions/lib/window/Rank.cpp +++ b/velox/functions/lib/window/Rank.cpp @@ -97,9 +97,7 @@ void registerRankInternal( exec::FunctionSignatureBuilder().returnType(returnType).build(), }; - exec::registerWindowFunction( - name, - std::move(signatures), + auto windowFunctionFactory = [name]( const std::vector& /*args*/, const TypePtr& resultType, @@ -107,9 +105,23 @@ void registerRankInternal( velox::memory::MemoryPool* /*pool*/, HashStringAllocator* /*stringAllocator*/, const core::QueryConfig& /*queryConfig*/) - -> std::unique_ptr { - return std::make_unique>(resultType); - }); + -> std::unique_ptr { + return std::make_unique>(resultType); + }; + + if constexpr (TRank == RankType::kRank || TRank == RankType::kDenseRank) { + exec::registerWindowFunction( + name, + std::move(signatures), + {exec::WindowFunction::ProcessMode::kRows, false}, + std::move(windowFunctionFactory)); + } else { + exec::registerWindowFunction( + name, + std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), + std::move(windowFunctionFactory)); + } } void registerRankBigint(const std::string& name) { diff --git a/velox/functions/lib/window/RowNumber.cpp b/velox/functions/lib/window/RowNumber.cpp index 16b7feb0a5431..81fc7e5e0c6d4 100644 --- a/velox/functions/lib/window/RowNumber.cpp +++ b/velox/functions/lib/window/RowNumber.cpp @@ -75,6 +75,7 @@ void registerRowNumber(const std::string& name, TypeKind resultTypeKind) { exec::registerWindowFunction( name, std::move(signatures), + {exec::WindowFunction::ProcessMode::kRows, false}, [name]( const std::vector& /*args*/, const TypePtr& resultType, diff --git a/velox/functions/prestosql/window/CumeDist.cpp b/velox/functions/prestosql/window/CumeDist.cpp index 98b264ec9ca92..439693e7ccc71 100644 --- a/velox/functions/prestosql/window/CumeDist.cpp +++ b/velox/functions/prestosql/window/CumeDist.cpp @@ -74,6 +74,7 @@ void registerCumeDist(const std::string& name) { exec::registerWindowFunction( name, std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), [name]( const std::vector& /*args*/, const TypePtr& /*resultType*/, diff --git a/velox/functions/prestosql/window/FirstLastValue.cpp b/velox/functions/prestosql/window/FirstLastValue.cpp index eaec4db987037..8aff5e26d2a2a 100644 --- a/velox/functions/prestosql/window/FirstLastValue.cpp +++ b/velox/functions/prestosql/window/FirstLastValue.cpp @@ -175,6 +175,7 @@ void registerFirstLastInternal(const std::string& name) { exec::registerWindowFunction( name, std::move(signatures), + exec::WindowFunction::Metadata::defaultMetadata(), [](const std::vector& args, const TypePtr& resultType, bool ignoreNulls, diff --git a/velox/functions/prestosql/window/LeadLag.cpp b/velox/functions/prestosql/window/LeadLag.cpp index 8e4c598c4d637..9d65351c76d56 100644 --- a/velox/functions/prestosql/window/LeadLag.cpp +++ b/velox/functions/prestosql/window/LeadLag.cpp @@ -424,6 +424,7 @@ void registerLag(const std::string& name) { exec::registerWindowFunction( name, signatures(), + exec::WindowFunction::Metadata::defaultMetadata(), [name]( const std::vector& args, const TypePtr& resultType, @@ -441,6 +442,7 @@ void registerLead(const std::string& name) { exec::registerWindowFunction( name, signatures(), + exec::WindowFunction::Metadata::defaultMetadata(), [name]( const std::vector& args, const TypePtr& resultType, From 3bf1a74042e83981630d24c1eaee190d1f31aac2 Mon Sep 17 00:00:00 2001 From: Jimmy Lu Date: Mon, 26 Aug 2024 13:29:21 -0700 Subject: [PATCH 21/24] Fix LEFT and ANTI joins to preserve probe order (#10832) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10832 Currently for LEFT and ANTI joins, when there is a carryover missing row from previous batch, we keep it and add it at end of current batch if there is capacity, which breaks the order of probe side rows. To fix this we simplify the logic of `NoMatchDetector` to emit rows in order. As a result of that, the carryover row will be added as soon as we can decide it needs to be added to output; this means we need to make one row extra space on output buffers for the carryover case. In the same case we also need to use temporary buffers to avoid data being overwritten before we read them. Reviewed By: mbasmanova Differential Revision: D61795406 fbshipit-source-id: 7f972701718fb62d60c7122ee5432f47fec8e241 --- velox/core/PlanNode.h | 4 ++ velox/exec/HashProbe.cpp | 106 +++++++++++++++++++++++------- velox/exec/HashProbe.h | 91 +++++++------------------ velox/exec/tests/HashJoinTest.cpp | 42 ++++++++++++ 4 files changed, 153 insertions(+), 90 deletions(-) diff --git a/velox/core/PlanNode.h b/velox/core/PlanNode.h index ffe37eb8dbe9b..4013d7a2dee7a 100644 --- a/velox/core/PlanNode.h +++ b/velox/core/PlanNode.h @@ -1526,6 +1526,10 @@ class AbstractJoinNode : public PlanNode { return joinType_ == JoinType::kAnti; } + bool isPreservingProbeOrder() const { + return isInnerJoin() || isLeftJoin() || isAntiJoin(); + } + const std::vector& leftKeys() const { return leftKeys_; } diff --git a/velox/exec/HashProbe.cpp b/velox/exec/HashProbe.cpp index 95f060b789148..defbbcdd8e119 100644 --- a/velox/exec/HashProbe.cpp +++ b/velox/exec/HashProbe.cpp @@ -986,9 +986,18 @@ RowVectorPtr HashProbe::getOutputInternal(bool toSpillOutput) { auto outputBatchSize = (isLeftSemiOrAntiJoinNoFilter || emptyBuildSide) ? inputSize : outputBatchSize_; - auto mapping = - initializeRowNumberMapping(outputRowMapping_, outputBatchSize, pool()); - outputTableRows_.resize(outputBatchSize); + auto outputBatchCapacity = outputBatchSize; + if (filter_ && + (isLeftJoin(joinType_) || isFullJoin(joinType_) || + isAntiJoin(joinType_))) { + // If we need non-matching probe side row, there is a possibility that such + // row exists at end of an input batch and being carried over in the next + // output batch, so we need to make extra room of one row in output. + ++outputBatchCapacity; + } + auto mapping = initializeRowNumberMapping( + outputRowMapping_, outputBatchCapacity, pool()); + outputTableRows_.resize(outputBatchCapacity); for (;;) { int numOut = 0; @@ -996,8 +1005,11 @@ RowVectorPtr HashProbe::getOutputInternal(bool toSpillOutput) { if (emptyBuildSide) { // When build side is empty, anti and left joins return all probe side // rows, including ones with null join keys. - std::iota(mapping.begin(), mapping.end(), 0); - std::fill(outputTableRows_.begin(), outputTableRows_.end(), nullptr); + std::iota(mapping.begin(), mapping.begin() + inputSize, 0); + std::fill( + outputTableRows_.begin(), + outputTableRows_.begin() + inputSize, + nullptr); numOut = inputSize; } else if (isAntiJoin(joinType_) && !filter_) { if (nullAware_) { @@ -1024,8 +1036,8 @@ RowVectorPtr HashProbe::getOutputInternal(bool toSpillOutput) { numOut = table_->listJoinResults( *resultIter_, joinIncludesMissesFromLeft(joinType_), - mapping, - folly::Range(outputTableRows_.data(), outputTableRows_.size()), + folly::Range(mapping.data(), outputBatchSize), + folly::Range(outputTableRows_.data(), outputBatchSize), operatorCtx_->driverCtx()->queryConfig().preferredOutputBatchBytes()); } @@ -1036,7 +1048,7 @@ RowVectorPtr HashProbe::getOutputInternal(bool toSpillOutput) { input_ = nullptr; return nullptr; } - VELOX_CHECK_LE(numOut, outputTableRows_.size()); + VELOX_CHECK_LE(numOut, outputBatchSize); numOut = evalFilter(numOut); @@ -1302,6 +1314,19 @@ SelectivityVector HashProbe::evalFilterForNullAwareJoin( return filterPassedRows; } +namespace { + +template +T* initBuffer(BufferPtr& buffer, vector_size_t size, memory::MemoryPool* pool) { + VELOX_CHECK(!buffer || buffer->isMutable()); + if (!buffer || buffer->size() < size * sizeof(T)) { + buffer = AlignedBuffer::allocate(size, pool); + } + return buffer->asMutable(); +} + +} // namespace + int32_t HashProbe::evalFilter(int32_t numRows) { if (!filter_) { return numRows; @@ -1343,21 +1368,51 @@ int32_t HashProbe::evalFilter(int32_t numRows) { if (isLeftJoin(joinType_) || isFullJoin(joinType_)) { // Identify probe rows which got filtered out and add them back with nulls // for build side. - auto addMiss = [&](auto row) { - outputTableRows_[numPassed] = nullptr; - rawOutputProbeRowMapping[numPassed++] = row; - }; - for (auto i = 0; i < numRows; ++i) { - const bool passed = filterPassed(i); - noMatchDetector_.advance(rawOutputProbeRowMapping[i], passed, addMiss); - if (passed) { - outputTableRows_[numPassed] = outputTableRows_[i]; - rawOutputProbeRowMapping[numPassed++] = rawOutputProbeRowMapping[i]; + if (noMatchDetector_.hasLastMissedRow()) { + auto* tempOutputTableRows = initBuffer( + tempOutputTableRows_, outputTableRows_.size(), pool()); + auto* tempOutputRowMapping = initBuffer( + tempOutputRowMapping_, outputTableRows_.size(), pool()); + auto addMiss = [&](auto row) { + tempOutputTableRows[numPassed] = nullptr; + tempOutputRowMapping[numPassed++] = row; + }; + for (auto i = 0; i < numRows; ++i) { + const bool passed = filterPassed(i); + noMatchDetector_.advance(rawOutputProbeRowMapping[i], passed, addMiss); + if (passed) { + tempOutputTableRows[numPassed] = outputTableRows_[i]; + tempOutputRowMapping[numPassed++] = rawOutputProbeRowMapping[i]; + } + } + if (resultIter_->atEnd()) { + noMatchDetector_.finish(addMiss); + } + std::copy( + tempOutputTableRows, + tempOutputTableRows + numPassed, + outputTableRows_.data()); + std::copy( + tempOutputRowMapping, + tempOutputRowMapping + numPassed, + rawOutputProbeRowMapping); + } else { + auto addMiss = [&](auto row) { + outputTableRows_[numPassed] = nullptr; + rawOutputProbeRowMapping[numPassed++] = row; + }; + for (auto i = 0; i < numRows; ++i) { + const bool passed = filterPassed(i); + noMatchDetector_.advance(rawOutputProbeRowMapping[i], passed, addMiss); + if (passed) { + outputTableRows_[numPassed] = outputTableRows_[i]; + rawOutputProbeRowMapping[numPassed++] = rawOutputProbeRowMapping[i]; + } + } + if (resultIter_->atEnd()) { + noMatchDetector_.finish(addMiss); } } - - noMatchDetector_.finishIteration( - addMiss, resultIter_->atEnd(), outputTableRows_.size() - numPassed); } else if (isLeftSemiFilterJoin(joinType_)) { auto addLastMatch = [&](auto row) { outputTableRows_[numPassed] = nullptr; @@ -1442,9 +1497,9 @@ int32_t HashProbe::evalFilter(int32_t numRows) { noMatchDetector_.advance(probeRow, filterPassed(i), addMiss); } } - - noMatchDetector_.finishIteration( - addMiss, resultIter_->atEnd(), outputTableRows_.size() - numPassed); + if (resultIter_->atEnd()) { + noMatchDetector_.finish(addMiss); + } } else { for (auto i = 0; i < numRows; ++i) { if (filterPassed(i)) { @@ -1453,6 +1508,7 @@ int32_t HashProbe::evalFilter(int32_t numRows) { } } } + VELOX_CHECK_LE(numPassed, outputTableRows_.size()); return numPassed; } @@ -1938,6 +1994,8 @@ void HashProbe::close() { inputSpiller_.reset(); table_.reset(); outputRowMapping_.reset(); + tempOutputRowMapping_.reset(); + tempOutputTableRows_.reset(); output_.reset(); nonSpillInputIndicesBuffer_.reset(); spillInputIndicesBuffers_.clear(); diff --git a/velox/exec/HashProbe.h b/velox/exec/HashProbe.h index 79709e2917f0c..ddb310af6bba0 100644 --- a/velox/exec/HashProbe.h +++ b/velox/exec/HashProbe.h @@ -430,12 +430,20 @@ class HashProbe : public Operator { // Row number in 'input_' for each output row. BufferPtr outputRowMapping_; + // For left join with filter, we could overwrite the row which we have not + // checked if there is a carryover. Use a temporary buffer in this case. + BufferPtr tempOutputRowMapping_; + // maps from column index in 'table_' to channel in 'output_'. std::vector tableOutputProjections_; // Rows of table found by join probe, later filtered by 'filter_'. std::vector outputTableRows_; + // For left join with filter, we could overwrite the row which we have not + // checked if there is a carryover. Use a temporary buffer in this case. + BufferPtr tempOutputTableRows_; + // Indicates probe-side rows which should produce a NULL in left semi project // with filter. SelectivityVector leftSemiProjectIsNull_; @@ -447,89 +455,40 @@ class HashProbe : public Operator { // Called for each row that the filter was evaluated on. Expects that probe // side rows with multiple matches on the build side are next to each other. template - void advance(vector_size_t row, bool passed, TOnMiss onMiss) { - if (currentRow != row) { - // Check if 'currentRow' is the same input row as the last missed row - // from a previous output batch. If so finishIteration will call - // onMiss. - if (currentRow != -1 && !currentRowPassed && - (!lastMissedRow || currentRow != lastMissedRow)) { - onMiss(currentRow); + void advance(vector_size_t row, bool passed, TOnMiss&& onMiss) { + if (currentRow_ != row) { + if (hasLastMissedRow()) { + onMiss(currentRow_); } - currentRow = row; - currentRowPassed = false; + currentRow_ = row; + currentRowPassed_ = false; } - if (passed) { - // lastMissedRow can only be a row that has never passed the filter. If - // it passes there's no need to continue carrying it forward. - if (lastMissedRow && currentRow == lastMissedRow) { - lastMissedRow.reset(); - } - - currentRowPassed = true; + currentRowPassed_ = true; } } - // Invoked at the end of one output batch processing. 'end' is set to true - // at the end of processing an input batch. 'freeOutputRows' is the number - // of rows that can still be written to the output batch. + // Invoked at the end of all output batches. template - void - finishIteration(TOnMiss onMiss, bool endOfData, size_t freeOutputRows) { - if (endOfData) { - if (!currentRowPassed && currentRow != -1) { - // If we're at the end of the input batch and the current row hasn't - // passed the filter, it never will, process it as a miss. - // We're guaranteed to have space, at least the last row was never - // written out since it was a miss. - onMiss(currentRow); - freeOutputRows--; - } - - // We no longer need to carry the current row since we already called - // onMiss on it. - if (lastMissedRow && currentRow == lastMissedRow) { - lastMissedRow.reset(); - } - - currentRow = -1; - currentRowPassed = false; - } - - // If there's space left in the output batch, write out the last missed - // row. - if (lastMissedRow && currentRow != lastMissedRow && freeOutputRows > 0) { - onMiss(*lastMissedRow); - lastMissedRow.reset(); - } - - // If the current row hasn't passed the filter, we need to carry it - // forward in case it never passes the filter. - if (!currentRowPassed && currentRow != -1) { - lastMissedRow = currentRow; + void finish(TOnMiss&& onMiss) { + if (hasLastMissedRow()) { + onMiss(currentRow_); } + currentRow_ = -1; } // Returns if we're carrying forward a missed input row. Notably, if this is // true, we're not yet done processing the input batch. - bool hasLastMissedRow() { - return lastMissedRow.has_value(); + bool hasLastMissedRow() const { + return currentRow_ != -1 && !currentRowPassed_; } private: // Row number being processed. - vector_size_t currentRow{-1}; + vector_size_t currentRow_{-1}; - // True if currentRow has a match. - bool currentRowPassed{false}; - - // If set, it points to the last missed (input) row carried over from - // previous output batch processing. The last missed row is either written - // as a passed row if the same input row has a hit in the next output batch - // processed or written to the first output batch which has space at - // the end if it never has a hit. - std::optional lastMissedRow; + // True if currentRow_ has a match. + bool currentRowPassed_{false}; }; // For left semi join filter with extra filter, de-duplicates probe side rows diff --git a/velox/exec/tests/HashJoinTest.cpp b/velox/exec/tests/HashJoinTest.cpp index 37601f0f913c3..c07d462e5fd0c 100644 --- a/velox/exec/tests/HashJoinTest.cpp +++ b/velox/exec/tests/HashJoinTest.cpp @@ -6627,6 +6627,48 @@ TEST_F(HashJoinTest, leftJoinWithMissAtEndOfBatchMultipleBuildMatches) { test("t_k2 != 4 and t_k2 != 8"); } +TEST_F(HashJoinTest, leftJoinPreserveProbeOrder) { + const std::vector probeVectors = { + makeRowVector( + {"k1", "v1"}, + { + makeConstant(0, 2), + makeFlatVector({1, 0}), + }), + }; + const std::vector buildVectors = { + makeRowVector( + {"k2", "v2"}, + { + makeConstant(0, 2), + makeConstant(0, 2), + }), + }; + auto planNodeIdGenerator = std::make_shared(); + auto plan = + PlanBuilder(planNodeIdGenerator) + .values(probeVectors) + .hashJoin( + {"k1"}, + {"k2"}, + PlanBuilder(planNodeIdGenerator).values(buildVectors).planNode(), + "v1 % 2 = v2 % 2", + {"v1"}, + core::JoinType::kLeft) + .planNode(); + auto result = AssertQueryBuilder(plan) + .config(core::QueryConfig::kPreferredOutputBatchRows, "1") + .singleThreaded(true) + .copyResults(pool_.get()); + ASSERT_EQ(result->size(), 3); + auto* v1 = + result->childAt(0)->loadedVector()->asUnchecked>(); + ASSERT_FALSE(v1->mayHaveNulls()); + ASSERT_EQ(v1->valueAt(0), 1); + ASSERT_EQ(v1->valueAt(1), 0); + ASSERT_EQ(v1->valueAt(2), 0); +} + DEBUG_ONLY_TEST_F(HashJoinTest, minSpillableMemoryReservation) { constexpr int64_t kMaxBytes = 1LL << 30; // 1GB VectorFuzzer fuzzer({.vectorSize = 1000}, pool()); From c74b5e19e48f5889da84c746d77bafa0109146cb Mon Sep 17 00:00:00 2001 From: xiaoxmeng Date: Mon, 26 Aug 2024 18:17:53 -0700 Subject: [PATCH 22/24] Relax the reclaimed bytes check for hash build (#10833) Summary: Join fuzzer detects check failure on hash build operator reclaim as the operator memory usage gets increased after reclamation. This happens when the parallel join build is running at a background and the memory reclamation is skipped. The parallel join build can cause additional memory usage such as data structure used to store parallel join data partitioning as well as duplicate row vector. This PR fixes this by skipping the reclaimed bytes check for hash build which only use the spill memory pool for memory reclamation. Pull Request resolved: https://github.com/facebookincubator/velox/pull/10833 Test Plan: This PR has run through join fuzzer in opt mode for 5 hours. Reviewed By: Yuhta Differential Revision: D61800038 Pulled By: xiaoxmeng fbshipit-source-id: 3135e6d908082598b4afe7e52efbb85611b57f60 --- velox/exec/HashTable.cpp | 23 +++++++++++------------ velox/exec/HashTable.h | 2 +- velox/exec/Operator.cpp | 32 +++++++++++++++++++++++--------- velox/exec/RowContainer.cpp | 3 ++- 4 files changed, 37 insertions(+), 23 deletions(-) diff --git a/velox/exec/HashTable.cpp b/velox/exec/HashTable.cpp index 7c263dfdd073f..d5d9059461b2a 100644 --- a/velox/exec/HashTable.cpp +++ b/velox/exec/HashTable.cpp @@ -1052,7 +1052,7 @@ void HashTable::buildJoinPartition( buildPartitionBounds_[partition], buildPartitionBounds_[partition + 1], overflow}; - auto rowContainer = + auto* rowContainer = (partition == 0 ? this : otherTables_[partition - 1].get())->rows(); for (auto i = 0; i < numPartitions; ++i) { auto* table = i == 0 ? this : otherTables_[i - 1].get(); @@ -1138,16 +1138,16 @@ void HashTable::insertForGroupBy( template bool HashTable::arrayPushRow(char* row, int32_t index) { - auto existing = table_[index]; - if (existing) { - if (nextOffset_) { + auto* existingRow = table_[index]; + if (existingRow != nullptr) { + if (nextOffset_ > 0) { hasDuplicates_ = true; - rows_->appendNextRow(existing, row); + rows_->appendNextRow(existingRow, row); } return false; } table_[index] = row; - return !existing; + return existingRow == nullptr; } template @@ -1155,10 +1155,9 @@ void HashTable::pushNext( RowContainer* rows, char* row, char* next) { - if (nextOffset_ > 0) { - hasDuplicates_ = true; - rows->appendNextRow(row, next); - } + VELOX_CHECK_GT(nextOffset_, 0); + hasDuplicates_ = true; + rows->appendNextRow(row, next); } template @@ -1187,7 +1186,7 @@ FOLLY_ALWAYS_INLINE void HashTable::buildFullProbe( [&](char* group, int32_t /*row*/) { if (RowContainer::normalizedKey(group) == RowContainer::normalizedKey(inserted)) { - if (nextOffset_) { + if (nextOffset_ > 0) { pushNext(rows, group, inserted); } return true; @@ -1809,7 +1808,7 @@ int32_t HashTable::listJoinResults( (joinProjectedVarColumnsSize(iter.varSizeListColumns, hit) + iter.fixedSizeListColumnsSizeSum); } else { - auto numRows = rows->size(); + const auto numRows = rows->size(); auto num = std::min(numRows - iter.lastDuplicateRowIndex, maxOut - numOut); std::fill_n(inputRows.begin() + numOut, num, row); diff --git a/velox/exec/HashTable.h b/velox/exec/HashTable.h index b545658ac6190..4d6293b82b6c0 100644 --- a/velox/exec/HashTable.h +++ b/velox/exec/HashTable.h @@ -1002,7 +1002,7 @@ class HashTable : public BaseHashTable { std::atomic hasDuplicates_{false}; // Offset of next row link for join build side set from 'rows_'. - int32_t nextOffset_; + int32_t nextOffset_{0}; char** table_ = nullptr; memory::ContiguousAllocation tableAllocation_; diff --git a/velox/exec/Operator.cpp b/velox/exec/Operator.cpp index 0eacee7ca4a89..5abd3e8acda8a 100644 --- a/velox/exec/Operator.cpp +++ b/velox/exec/Operator.cpp @@ -304,25 +304,29 @@ void Operator::recordSpillStats() { lockedStats->addRuntimeStat( kSpillFillTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillFillTimeNanos)}); + static_cast(lockedSpillStats->spillFillTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillSortTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillSortTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillSortTimeNanos)}); + static_cast(lockedSpillStats->spillSortTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillSerializationTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillSerializationTime, - RuntimeCounter{static_cast( - lockedSpillStats->spillSerializationTimeNanos)}); + RuntimeCounter{ + static_cast(lockedSpillStats->spillSerializationTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillFlushTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillFlushTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillFlushTimeNanos)}); + static_cast(lockedSpillStats->spillFlushTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillWrites != 0) { lockedStats->addRuntimeStat( @@ -333,7 +337,8 @@ void Operator::recordSpillStats() { lockedStats->addRuntimeStat( kSpillWriteTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillWriteTimeNanos)}); + static_cast(lockedSpillStats->spillWriteTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillRuns != 0) { lockedStats->addRuntimeStat( @@ -369,14 +374,17 @@ void Operator::recordSpillStats() { lockedStats->addRuntimeStat( kSpillReadTime, RuntimeCounter{ - static_cast(lockedSpillStats->spillReadTimeNanos)}); + static_cast(lockedSpillStats->spillReadTimeNanos), + RuntimeCounter::Unit::kNanos}); } if (lockedSpillStats->spillDeserializationTimeNanos != 0) { lockedStats->addRuntimeStat( kSpillDeserializationTime, - RuntimeCounter{static_cast( - lockedSpillStats->spillDeserializationTimeNanos)}); + RuntimeCounter{ + static_cast( + lockedSpillStats->spillDeserializationTimeNanos), + RuntimeCounter::Unit::kNanos}); } lockedSpillStats->reset(); } @@ -648,6 +656,12 @@ uint64_t Operator::MemoryReclaimer::reclaim( memory::ScopedReclaimedBytesRecorder recoder(pool, &reclaimedBytes); op_->reclaim(targetBytes, stats); } + // NOTE: the parallel hash build is running at the background thread + // pool which won't stop during memory reclamation so the operator's + // memory usage might increase in such case. memory usage. + if (op_->operatorType() == "HashBuild") { + reclaimedBytes = std::max(0, reclaimedBytes); + } return reclaimedBytes; }, stats); diff --git a/velox/exec/RowContainer.cpp b/velox/exec/RowContainer.cpp index 7b8a36ed46ac4..29de71ba6180c 100644 --- a/velox/exec/RowContainer.cpp +++ b/velox/exec/RowContainer.cpp @@ -391,8 +391,9 @@ int32_t RowContainer::findRows(folly::Range rows, char** result) { } void RowContainer::appendNextRow(char* current, char* nextRow) { + VELOX_CHECK(getNextRowVector(nextRow) == nullptr); NextRowVector*& nextRowArrayPtr = getNextRowVector(current); - if (!nextRowArrayPtr) { + if (nextRowArrayPtr == nullptr) { nextRowArrayPtr = new (stringAllocator_->allocate(kNextRowVectorSize)->begin()) NextRowVector(StlAllocator(stringAllocator_.get())); From 72c4921d2a7ef00355530ec59dfbf4ec601aff62 Mon Sep 17 00:00:00 2001 From: Serge Druzkin Date: Mon, 26 Aug 2024 20:37:54 -0700 Subject: [PATCH 23/24] Fix minor doc typos in SelectiveColumnReader.h (#10835) Summary: Pull Request resolved: https://github.com/facebookincubator/velox/pull/10835 Reviewed By: xiaoxmeng, Yuhta Differential Revision: D61815649 fbshipit-source-id: 20e11fd6dc1a19c249b52884dd26cdef156d4b21 --- velox/dwio/common/SelectiveColumnReader.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/velox/dwio/common/SelectiveColumnReader.h b/velox/dwio/common/SelectiveColumnReader.h index e1190f19f1f38..821498a409f4f 100644 --- a/velox/dwio/common/SelectiveColumnReader.h +++ b/velox/dwio/common/SelectiveColumnReader.h @@ -105,14 +105,14 @@ struct ScanState { DictionaryValues dictionary; // If the format, like ORC/DWRF has a base dictionary completed by - // local delta dictionaries over the furst one, this represents the + // local delta dictionaries over the first one, this represents the // local values, e.g. row group dictionary in ORC. TBD: If there is // a pattern of dictionaries completed by more dictionaries in other // formats, this will be modeled as an vector of n DictionaryValues. DictionaryValues dictionary2; // Bits selecting between dictionary and dictionary2 or dictionary and - // literal. OR/DWRFC only. + // literal. ORC/DWRF only. BufferPtr inDictionary; // Copy of Visitor::rows_ adjusted to start at the current encoding From 763c19c0c767035ce03573ca2dc852b1303eeb6f Mon Sep 17 00:00:00 2001 From: duanmeng Date: Tue, 27 Aug 2024 02:47:41 -0700 Subject: [PATCH 24/24] Trace metadata during task creation (#10815) Summary: Create a directory named `$QueryTraceBaseDir/$taskId` when a task is initiated, if query tracing is enabled. This directory will store metadata related to the task, including the query plan node tree, query configurations, and connector properties. Part of https://github.com/facebookincubator/velox/issues/9668 Pull Request resolved: https://github.com/facebookincubator/velox/pull/10815 Reviewed By: Yuhta Differential Revision: D61808438 Pulled By: xiaoxmeng fbshipit-source-id: 57eff8f4b70405ba5c60fcd8315b025b22c2317b --- velox/common/memory/Memory.cpp | 5 + velox/common/memory/Memory.h | 9 + .../common/memory/tests/MemoryManagerTest.cpp | 63 +++---- velox/core/QueryConfig.h | 25 +++ velox/docs/configs.rst | 25 +++ velox/exec/Task.cpp | 42 ++++- velox/exec/Task.h | 10 ++ velox/exec/trace/CMakeLists.txt | 16 +- velox/exec/trace/QueryDataReader.cpp | 3 - velox/exec/trace/QueryDataWriter.cpp | 2 - velox/exec/trace/QueryMetadataReader.cpp | 4 - velox/exec/trace/QueryMetadataWriter.cpp | 3 - velox/exec/trace/QueryTraceConfig.cpp | 5 + velox/exec/trace/QueryTraceConfig.h | 6 +- velox/exec/trace/QueryTraceUtil.cpp | 38 ++++ velox/exec/trace/QueryTraceUtil.h | 26 +++ velox/exec/trace/test/CMakeLists.txt | 9 +- velox/exec/trace/test/QueryTraceTest.cpp | 166 ++++++++++++++++++ 18 files changed, 405 insertions(+), 52 deletions(-) create mode 100644 velox/exec/trace/QueryTraceUtil.cpp create mode 100644 velox/exec/trace/QueryTraceUtil.h diff --git a/velox/common/memory/Memory.cpp b/velox/common/memory/Memory.cpp index 15213843b7a2a..c42b3912b9806 100644 --- a/velox/common/memory/Memory.cpp +++ b/velox/common/memory/Memory.cpp @@ -157,6 +157,7 @@ MemoryManager::MemoryManager(const MemoryManagerOptions& options) .coreOnAllocationFailureEnabled = options.coreOnAllocationFailureEnabled})}, spillPool_{addLeafPool("__sys_spilling__")}, + tracePool_{addLeafPool("__sys_tracing__")}, sharedLeafPools_(createSharedLeafMemoryPools(*sysRoot_)) { VELOX_CHECK_NOT_NULL(allocator_); VELOX_CHECK_NOT_NULL(arbitrator_); @@ -427,4 +428,8 @@ memory::MemoryPool* spillMemoryPool() { bool isSpillMemoryPool(memory::MemoryPool* pool) { return pool == spillMemoryPool(); } + +memory::MemoryPool* traceMemoryPool() { + return memory::MemoryManager::getInstance()->tracePool(); +} } // namespace facebook::velox::memory diff --git a/velox/common/memory/Memory.h b/velox/common/memory/Memory.h index ac1085f473bfd..ac176fb2f5bde 100644 --- a/velox/common/memory/Memory.h +++ b/velox/common/memory/Memory.h @@ -342,6 +342,11 @@ class MemoryManager { return spillPool_.get(); } + /// Returns the process wide leaf memory pool used for query tracing. + MemoryPool* tracePool() const { + return tracePool_.get(); + } + const std::vector>& testingSharedLeafPools() { return sharedLeafPools_; } @@ -374,6 +379,7 @@ class MemoryManager { const std::shared_ptr sysRoot_; const std::shared_ptr spillPool_; + const std::shared_ptr tracePool_; const std::vector> sharedLeafPools_; mutable folly::SharedMutex mutex_; @@ -420,6 +426,9 @@ memory::MemoryPool* spillMemoryPool(); /// Returns true if the provided 'pool' is the spilling memory pool. bool isSpillMemoryPool(memory::MemoryPool* pool); +/// Returns the system-wide memory pool for tracing memory usage. +memory::MemoryPool* traceMemoryPool(); + FOLLY_ALWAYS_INLINE int32_t alignmentPadding(void* address, int32_t alignment) { auto extra = reinterpret_cast(address) % alignment; return extra == 0 ? 0 : alignment - extra; diff --git a/velox/common/memory/tests/MemoryManagerTest.cpp b/velox/common/memory/tests/MemoryManagerTest.cpp index 02ac9a11fb2ab..d3da086e9b86c 100644 --- a/velox/common/memory/tests/MemoryManagerTest.cpp +++ b/velox/common/memory/tests/MemoryManagerTest.cpp @@ -53,7 +53,7 @@ TEST_F(MemoryManagerTest, ctor) { const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools; { MemoryManager manager{}; - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); ASSERT_EQ(manager.capacity(), kMaxMemory); ASSERT_EQ(0, manager.getTotalBytes()); ASSERT_EQ(manager.alignment(), MemoryAllocator::kMaxAlignment); @@ -69,7 +69,7 @@ TEST_F(MemoryManagerTest, ctor) { .arbitratorCapacity = kCapacity, .arbitratorReservedCapacity = 0}}; ASSERT_EQ(kCapacity, manager.capacity()); - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); ASSERT_EQ(manager.testingDefaultRoot().alignment(), manager.alignment()); } { @@ -84,7 +84,7 @@ TEST_F(MemoryManagerTest, ctor) { ASSERT_EQ(manager.testingDefaultRoot().alignment(), manager.alignment()); // TODO: replace with root pool memory tracker quota check. ASSERT_EQ( - kSharedPoolCount + 1, manager.testingDefaultRoot().getChildCount()); + kSharedPoolCount + 2, manager.testingDefaultRoot().getChildCount()); ASSERT_EQ(kCapacity, manager.capacity()); ASSERT_EQ(0, manager.getTotalBytes()); } @@ -103,7 +103,7 @@ TEST_F(MemoryManagerTest, ctor) { ASSERT_EQ( manager.toString(), "Memory Manager[capacity 4.00GB alignment 64B usedBytes 0B number of " - "pools 1\nList of root pools:\n\t__sys_root__\n" + "pools 2\nList of root pools:\n\t__sys_root__\n" "Memory Allocator[MALLOC capacity 4.00GB allocated bytes 0 " "allocated pages 0 mapped pages 0]\n" "ARBITRATOR[SHARED CAPACITY[4.00GB] PENDING[0] " @@ -246,10 +246,10 @@ TEST_F(MemoryManagerTest, addPoolWithArbitrator) { TEST_F(MemoryManagerTest, defaultMemoryManager) { auto& managerA = toMemoryManager(deprecatedDefaultMemoryManager()); auto& managerB = toMemoryManager(deprecatedDefaultMemoryManager()); - const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 1; - ASSERT_EQ(managerA.numPools(), 1); + const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 2; + ASSERT_EQ(managerA.numPools(), 2); ASSERT_EQ(managerA.testingDefaultRoot().getChildCount(), kSharedPoolCount); - ASSERT_EQ(managerB.numPools(), 1); + ASSERT_EQ(managerB.numPools(), 2); ASSERT_EQ(managerB.testingDefaultRoot().getChildCount(), kSharedPoolCount); auto child1 = managerA.addLeafPool("child_1"); @@ -260,41 +260,44 @@ TEST_F(MemoryManagerTest, defaultMemoryManager) { kSharedPoolCount + 2, managerA.testingDefaultRoot().getChildCount()); EXPECT_EQ( kSharedPoolCount + 2, managerB.testingDefaultRoot().getChildCount()); - ASSERT_EQ(managerA.numPools(), 3); - ASSERT_EQ(managerB.numPools(), 3); - auto pool = managerB.addRootPool(); ASSERT_EQ(managerA.numPools(), 4); ASSERT_EQ(managerB.numPools(), 4); + auto pool = managerB.addRootPool(); + ASSERT_EQ(managerA.numPools(), 5); + ASSERT_EQ(managerB.numPools(), 5); ASSERT_EQ( managerA.toString(), - "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 4\nList of root pools:\n\t__sys_root__\n\tdefault_root_0\n\trefcount 2\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); + "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 5\nList of root pools:\n\t__sys_root__\n\tdefault_root_0\n\trefcount 2\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); ASSERT_EQ( managerB.toString(), - "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 4\nList of root pools:\n\t__sys_root__\n\tdefault_root_0\n\trefcount 2\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); + "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 5\nList of root pools:\n\t__sys_root__\n\tdefault_root_0\n\trefcount 2\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); child1.reset(); EXPECT_EQ( kSharedPoolCount + 1, managerA.testingDefaultRoot().getChildCount()); child2.reset(); EXPECT_EQ(kSharedPoolCount, managerB.testingDefaultRoot().getChildCount()); + ASSERT_EQ(managerA.numPools(), 3); + ASSERT_EQ(managerB.numPools(), 3); + pool.reset(); ASSERT_EQ(managerA.numPools(), 2); ASSERT_EQ(managerB.numPools(), 2); - pool.reset(); - ASSERT_EQ(managerA.numPools(), 1); - ASSERT_EQ(managerB.numPools(), 1); ASSERT_EQ( managerA.toString(), - "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 1\nList of root pools:\n\t__sys_root__\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); + "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 2\nList of root pools:\n\t__sys_root__\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); ASSERT_EQ( managerB.toString(), - "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 1\nList of root pools:\n\t__sys_root__\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); + "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 2\nList of root pools:\n\t__sys_root__\nMemory Allocator[MALLOC capacity UNLIMITED allocated bytes 0 allocated pages 0 mapped pages 0]\nARBIRTATOR[NOOP CAPACITY[UNLIMITED]]]"); const std::string detailedManagerStr = managerA.toString(true); ASSERT_THAT( detailedManagerStr, testing::HasSubstr( - "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 1\nList of root pools:\n__sys_root__ usage 0B reserved 0B peak 0B\n")); + "Memory Manager[capacity UNLIMITED alignment 64B usedBytes 0B number of pools 2\nList of root pools:\n__sys_root__ usage 0B reserved 0B peak 0B\n")); ASSERT_THAT( detailedManagerStr, testing::HasSubstr("__sys_spilling__ usage 0B reserved 0B peak 0B\n")); + ASSERT_THAT( + detailedManagerStr, + testing::HasSubstr("__sys_tracing__ usage 0B reserved 0B peak 0B\n")); for (int i = 0; i < 32; ++i) { ASSERT_THAT( managerA.toString(true), @@ -306,7 +309,7 @@ TEST_F(MemoryManagerTest, defaultMemoryManager) { // TODO: remove this test when remove deprecatedAddDefaultLeafMemoryPool. TEST(MemoryHeaderTest, addDefaultLeafMemoryPool) { auto& manager = toMemoryManager(deprecatedDefaultMemoryManager()); - const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 1; + const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 2; ASSERT_EQ(manager.testingDefaultRoot().getChildCount(), kSharedPoolCount); { auto poolA = deprecatedAddDefaultLeafMemoryPool(); @@ -361,7 +364,7 @@ TEST_F(MemoryManagerTest, memoryPoolManagement) { MemoryManagerOptions options; options.alignment = alignment; MemoryManager manager{options}; - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); const int numPools = 100; std::vector> userRootPools; std::vector> userLeafPools; @@ -386,14 +389,14 @@ TEST_F(MemoryManagerTest, memoryPoolManagement) { ASSERT_FALSE(rootUnamedPool->name().empty()); ASSERT_EQ(rootUnamedPool->kind(), MemoryPool::Kind::kAggregate); ASSERT_EQ(rootUnamedPool->parent(), nullptr); - ASSERT_EQ(manager.numPools(), 1 + numPools + 2); + ASSERT_EQ(manager.numPools(), 1 + numPools + 2 + 1); userLeafPools.clear(); leafUnamedPool.reset(); - ASSERT_EQ(manager.numPools(), 1 + numPools / 2 + 1); + ASSERT_EQ(manager.numPools(), 1 + numPools / 2 + 1 + 1); userRootPools.clear(); - ASSERT_EQ(manager.numPools(), 1 + 1); + ASSERT_EQ(manager.numPools(), 1 + 2); rootUnamedPool.reset(); - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); } // TODO: when run sequentially, e.g. `buck run dwio/memory/...`, this has side @@ -410,7 +413,7 @@ TEST_F(MemoryManagerTest, globalMemoryManager) { ASSERT_NE(manager, globalManager); ASSERT_EQ(manager, memoryManager()); auto* managerII = memoryManager(); - const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 1; + const auto kSharedPoolCount = FLAGS_velox_memory_num_shared_leaf_pools + 2; { auto& rootI = manager->testingDefaultRoot(); const std::string childIName("some_child"); @@ -444,9 +447,9 @@ TEST_F(MemoryManagerTest, globalMemoryManager) { ASSERT_EQ(userRootChild->kind(), MemoryPool::Kind::kAggregate); ASSERT_EQ(rootI.getChildCount(), kSharedPoolCount + 1); ASSERT_EQ(rootII.getChildCount(), kSharedPoolCount + 1); - ASSERT_EQ(manager->numPools(), 2 + 1); + ASSERT_EQ(manager->numPools(), 2 + 2); } - ASSERT_EQ(manager->numPools(), 1); + ASSERT_EQ(manager->numPools(), 2); } TEST_F(MemoryManagerTest, alignmentOptionCheck) { @@ -544,9 +547,9 @@ TEST_F(MemoryManagerTest, concurrentPoolAccess) { } stopCheck = true; checkThread.join(); - ASSERT_EQ(manager.numPools(), pools.size() + 1); + ASSERT_EQ(manager.numPools(), pools.size() + 2); pools.clear(); - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); } TEST_F(MemoryManagerTest, quotaEnforcement) { @@ -654,7 +657,7 @@ TEST_F(MemoryManagerTest, disableMemoryPoolTracking) { ASSERT_EQ(manager.capacity(), 64LL << 20); ASSERT_EQ(manager.shrinkPools(), 0); // Default 1 system pool with 1 leaf child - ASSERT_EQ(manager.numPools(), 1); + ASSERT_EQ(manager.numPools(), 2); VELOX_ASSERT_THROW( leaf0->allocate(38LL << 20), "Exceeded memory pool capacity"); diff --git a/velox/core/QueryConfig.h b/velox/core/QueryConfig.h index 78f51375fa45a..bc1a24976a712 100644 --- a/velox/core/QueryConfig.h +++ b/velox/core/QueryConfig.h @@ -346,6 +346,16 @@ class QueryConfig { /// derived using micro-benchmarking. static constexpr const char* kPrefixSortMinRows = "prefixsort_min_rows"; + /// Enable query tracing flag. + static constexpr const char* kQueryTraceEnabled = "query_trace_enabled"; + + /// Base dir of a query to store tracing data. + static constexpr const char* kQueryTraceDir = "query_trace_dir"; + + /// A comma-separated list of plan node ids whose input data will be traced. + /// Empty string if only want to trace the query metadata. + static constexpr const char* kQueryTraceNodeIds = "query_trace_node_ids"; + uint64_t queryMaxMemoryPerNode() const { return config::toCapacity( get(kQueryMaxMemoryPerNode, "0B"), @@ -611,6 +621,21 @@ class QueryConfig { return get(kSpillableReservationGrowthPct, kDefaultPct); } + /// Returns true if query tracing is enabled. + bool queryTraceEnabled() const { + return get(kQueryTraceEnabled, false); + } + + std::string queryTraceDir() const { + // The default query trace dir, empty by default. + return get(kQueryTraceDir, ""); + } + + std::string queryTraceNodeIds() const { + // The default query trace nodes, empty by default. + return get(kQueryTraceNodeIds, ""); + } + bool prestoArrayAggIgnoreNulls() const { return get(kPrestoArrayAggIgnoreNulls, false); } diff --git a/velox/docs/configs.rst b/velox/docs/configs.rst index cd782b2d763d2..0e4582e1b5c19 100644 --- a/velox/docs/configs.rst +++ b/velox/docs/configs.rst @@ -686,4 +686,29 @@ Spark-specific Configuration the value of this config can not exceed the default value. * - spark.partition_id - integer + - - The current task's Spark partition ID. It's set by the query engine (Spark) prior to task execution. + +Tracing +-------- +.. list-table:: + :widths: 30 10 10 70 + :header-rows: 1 + + * - Property Name + - Type + - Default Value + - Description + * - query_trace_enabled + - bool + - true + - If true, enable query tracing. + * - query_trace_dir + - string + - + - The root directory to store the tracing data and metadata for a query. + * - query_trace_node_ids + - string + - + - A comma-separated list of plan node ids whose input data will be trace. If it is empty, then we only trace the + query metadata which includes the query plan and configs etc. diff --git a/velox/exec/Task.cpp b/velox/exec/Task.cpp index 38006f9baf531..3ff2e7321f46d 100644 --- a/velox/exec/Task.cpp +++ b/velox/exec/Task.cpp @@ -27,11 +27,11 @@ #include "velox/exec/HashBuild.h" #include "velox/exec/LocalPlanner.h" #include "velox/exec/MemoryReclaimer.h" -#include "velox/exec/Merge.h" #include "velox/exec/NestedLoopJoinBuild.h" #include "velox/exec/OperatorUtils.h" #include "velox/exec/OutputBufferManager.h" #include "velox/exec/Task.h" +#include "velox/exec/trace/QueryTraceUtil.h" using facebook::velox::common::testutil::TestValue; @@ -293,6 +293,7 @@ Task::Task( planFragment_(std::move(planFragment)), destination_(destination), queryCtx_(std::move(queryCtx)), + traceConfig_(maybeMakeTraceConfig()), mode_(mode), consumerSupplier_(std::move(consumerSupplier)), onError_(std::move(onError)), @@ -304,6 +305,8 @@ Task::Task( VELOX_CHECK_NULL( dynamic_cast(queryCtx_->executor())); } + + maybeInitQueryTrace(); } Task::~Task() { @@ -2833,6 +2836,43 @@ std::shared_ptr Task::getExchangeClientLocked( return exchangeClients_[pipelineId]; } +std::optional Task::maybeMakeTraceConfig() const { + const auto& queryConfig = queryCtx_->queryConfig(); + if (!queryConfig.queryTraceEnabled()) { + return std::nullopt; + } + + VELOX_USER_CHECK( + !queryConfig.queryTraceDir().empty(), + "Query trace enabled but the trace dir is not set"); + + const auto queryTraceNodes = queryConfig.queryTraceNodeIds(); + if (queryTraceNodes.empty()) { + return trace::QueryTraceConfig(queryConfig.queryTraceDir()); + } + + std::vector nodes; + folly::split(',', queryTraceNodes, nodes); + std::unordered_set nodeSet(nodes.begin(), nodes.end()); + VELOX_CHECK_EQ(nodeSet.size(), nodes.size()); + LOG(INFO) << "Query trace plan node ids: " << queryTraceNodes; + return trace::QueryTraceConfig( + std::move(nodeSet), queryConfig.queryTraceDir()); +} + +void Task::maybeInitQueryTrace() { + if (!traceConfig_) { + return; + } + + const auto traceTaskDir = + fmt::format("{}/{}", traceConfig_->queryTraceDir, taskId_); + trace::createTraceDirectory(traceTaskDir); + const auto queryMetadatWriter = std::make_unique( + traceTaskDir, memory::traceMemoryPool()); + queryMetadatWriter->write(queryCtx_, planFragment_.planNode); +} + void Task::testingVisitDrivers(const std::function& callback) { std::lock_guard l(mutex_); for (int i = 0; i < drivers_.size(); ++i) { diff --git a/velox/exec/Task.h b/velox/exec/Task.h index 433df9a0451e5..6a18d3fbcef17 100644 --- a/velox/exec/Task.h +++ b/velox/exec/Task.h @@ -23,6 +23,8 @@ #include "velox/exec/Split.h" #include "velox/exec/TaskStats.h" #include "velox/exec/TaskStructs.h" +#include "velox/exec/trace/QueryMetadataWriter.h" +#include "velox/exec/trace/QueryTraceConfig.h" #include "velox/vector/ComplexVector.h" namespace facebook::velox::exec { @@ -969,6 +971,13 @@ class Task : public std::enable_shared_from_this { std::shared_ptr getExchangeClientLocked( int32_t pipelineId) const; + // Builds the query trace config. + std::optional maybeMakeTraceConfig() const; + + // Create a 'QueryMetadtaWriter' to trace the query metadata if the query + // trace enabled. + void maybeInitQueryTrace(); + // The helper class used to maintain 'numCreatedTasks_' and 'numDeletedTasks_' // on task construction and destruction. class TaskCounter { @@ -999,6 +1008,7 @@ class Task : public std::enable_shared_from_this { core::PlanFragment planFragment_; const int destination_; const std::shared_ptr queryCtx_; + const std::optional traceConfig_; // The execution mode of the task. It is enforced that a task can only be // executed in a single mode throughout its lifetime diff --git a/velox/exec/trace/CMakeLists.txt b/velox/exec/trace/CMakeLists.txt index 532f3ed27f328..f8cc53e08ae4f 100644 --- a/velox/exec/trace/CMakeLists.txt +++ b/velox/exec/trace/CMakeLists.txt @@ -12,10 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -add_library(velox_query_trace_exec QueryMetadataWriter.cpp QueryTraceConfig.cpp - QueryDataWriter.cpp) +velox_add_library( + velox_query_trace_exec + QueryMetadataWriter.cpp + QueryTraceConfig.cpp + QueryDataWriter.cpp + QueryTraceUtil.cpp) -target_link_libraries( +velox_link_libraries( velox_query_trace_exec PRIVATE velox_common_io @@ -26,10 +30,10 @@ target_link_libraries( velox_common_base velox_presto_serializer) -add_library(velox_query_trace_retrieve QueryDataReader.cpp - QueryMetadataReader.cpp) +velox_add_library(velox_query_trace_retrieve QueryDataReader.cpp + QueryMetadataReader.cpp) -target_link_libraries( +velox_link_libraries( velox_query_trace_retrieve velox_common_io velox_file diff --git a/velox/exec/trace/QueryDataReader.cpp b/velox/exec/trace/QueryDataReader.cpp index b234330dd0e53..f0175fe11b367 100644 --- a/velox/exec/trace/QueryDataReader.cpp +++ b/velox/exec/trace/QueryDataReader.cpp @@ -17,9 +17,6 @@ #include "velox/exec/trace/QueryDataReader.h" #include "velox/common/file/File.h" -#include "velox/connectors/hive/HiveDataSink.h" -#include "velox/connectors/hive/TableHandle.h" -#include "velox/exec/TableWriter.h" #include "velox/exec/trace/QueryTraceTraits.h" namespace facebook::velox::exec::trace { diff --git a/velox/exec/trace/QueryDataWriter.cpp b/velox/exec/trace/QueryDataWriter.cpp index 544e7a3d4580f..57dc4284cd10b 100644 --- a/velox/exec/trace/QueryDataWriter.cpp +++ b/velox/exec/trace/QueryDataWriter.cpp @@ -18,8 +18,6 @@ #include "velox/common/base/SpillStats.h" #include "velox/common/file/File.h" #include "velox/common/file/FileSystems.h" -#include "velox/exec/TreeOfLosers.h" -#include "velox/exec/UnorderedStreamReader.h" #include "velox/exec/trace/QueryTraceTraits.h" #include "velox/serializers/PrestoSerializer.h" diff --git a/velox/exec/trace/QueryMetadataReader.cpp b/velox/exec/trace/QueryMetadataReader.cpp index 8843aa49a2ab6..99fe7cc8c4353 100644 --- a/velox/exec/trace/QueryMetadataReader.cpp +++ b/velox/exec/trace/QueryMetadataReader.cpp @@ -18,11 +18,7 @@ #include "velox/common/file/File.h" #include "velox/common/file/FileSystems.h" -#include "velox/connectors/hive/HiveDataSink.h" -#include "velox/connectors/hive/TableHandle.h" #include "velox/core/PlanNode.h" -#include "velox/exec/PartitionFunction.h" -#include "velox/exec/TableWriter.h" #include "velox/exec/trace/QueryTraceTraits.h" namespace facebook::velox::exec::trace { diff --git a/velox/exec/trace/QueryMetadataWriter.cpp b/velox/exec/trace/QueryMetadataWriter.cpp index beba2e8097469..e14cdb197f446 100644 --- a/velox/exec/trace/QueryMetadataWriter.cpp +++ b/velox/exec/trace/QueryMetadataWriter.cpp @@ -17,11 +17,8 @@ #include "velox/exec/trace/QueryMetadataWriter.h" #include "velox/common/config/Config.h" #include "velox/common/file/File.h" -#include "velox/connectors/hive/HiveDataSink.h" -#include "velox/connectors/hive/TableHandle.h" #include "velox/core/PlanNode.h" #include "velox/core/QueryCtx.h" -#include "velox/exec/TableWriter.h" #include "velox/exec/trace/QueryTraceTraits.h" namespace facebook::velox::exec::trace { diff --git a/velox/exec/trace/QueryTraceConfig.cpp b/velox/exec/trace/QueryTraceConfig.cpp index 2437a632fb432..233226ebccf50 100644 --- a/velox/exec/trace/QueryTraceConfig.cpp +++ b/velox/exec/trace/QueryTraceConfig.cpp @@ -24,4 +24,9 @@ QueryTraceConfig::QueryTraceConfig( : queryNodes(std::move(_queryNodeIds)), queryTraceDir(std::move(_queryTraceDir)) {} +QueryTraceConfig::QueryTraceConfig(std::string _queryTraceDir) + : QueryTraceConfig( + std::unordered_set{}, + std::move(_queryTraceDir)) {} + } // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceConfig.h b/velox/exec/trace/QueryTraceConfig.h index 8afcbf22fb53d..0e9c22818c342 100644 --- a/velox/exec/trace/QueryTraceConfig.h +++ b/velox/exec/trace/QueryTraceConfig.h @@ -21,15 +21,17 @@ namespace facebook::velox::exec::trace { struct QueryTraceConfig { - /// Target query trace nodes + /// Target query trace nodes. std::unordered_set queryNodes; - /// Base dir of query trace, normmaly it is $prefix/$taskId. + /// Base dir of query trace. std::string queryTraceDir; QueryTraceConfig( std::unordered_set _queryNodeIds, std::string _queryTraceDir); + QueryTraceConfig(std::string _queryTraceDir); + QueryTraceConfig() = default; }; } // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceUtil.cpp b/velox/exec/trace/QueryTraceUtil.cpp new file mode 100644 index 0000000000000..437d3eee224e5 --- /dev/null +++ b/velox/exec/trace/QueryTraceUtil.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "velox/exec/trace/QueryTraceUtil.h" +#include "velox/common/base/Exceptions.h" +#include "velox/common/file/FileSystems.h" + +namespace facebook::velox::exec::trace { + +void createTraceDirectory(const std::string& traceDir) { + try { + const auto fs = filesystems::getFileSystem(traceDir, nullptr); + if (fs->exists(traceDir)) { + fs->rmdir(traceDir); + } + fs->mkdir(traceDir); + } catch (const std::exception& e) { + VELOX_FAIL( + "Failed to create trace directory '{}' with error: {}", + traceDir, + e.what()); + } +} + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/QueryTraceUtil.h b/velox/exec/trace/QueryTraceUtil.h new file mode 100644 index 0000000000000..826aa35283a28 --- /dev/null +++ b/velox/exec/trace/QueryTraceUtil.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +namespace facebook::velox::exec::trace { + +/// Creates a directory to store the query trace metdata and data. +void createTraceDirectory(const std::string& traceDir); + +} // namespace facebook::velox::exec::trace diff --git a/velox/exec/trace/test/CMakeLists.txt b/velox/exec/trace/test/CMakeLists.txt index 9c4ffe9dd3585..861b5ffb339ad 100644 --- a/velox/exec/trace/test/CMakeLists.txt +++ b/velox/exec/trace/test/CMakeLists.txt @@ -28,4 +28,11 @@ target_link_libraries( velox_memory velox_query_trace_exec velox_query_trace_retrieve - velox_vector_fuzzer) + velox_vector_fuzzer + GTest::gtest_main + GTest::gmock + Folly::folly + gflags::gflags + glog::glog + fmt::fmt + ${FILESYSTEM}) diff --git a/velox/exec/trace/test/QueryTraceTest.cpp b/velox/exec/trace/test/QueryTraceTest.cpp index 765eeccaaf532..8b969cc1b9b65 100644 --- a/velox/exec/trace/test/QueryTraceTest.cpp +++ b/velox/exec/trace/test/QueryTraceTest.cpp @@ -28,6 +28,7 @@ #include "velox/exec/trace/QueryDataWriter.h" #include "velox/exec/trace/QueryMetadataReader.h" #include "velox/exec/trace/QueryMetadataWriter.h" +#include "velox/exec/trace/QueryTraceUtil.h" #include "velox/serializers/PrestoSerializer.h" #include "velox/vector/tests/utils/VectorTestBase.h" @@ -202,4 +203,169 @@ TEST_F(QueryTracerTest, traceMetadata) { ASSERT_EQ(actualConnectorConfigs.at(key), expectedConnectorConfigs.at(key)); } } + +TEST_F(QueryTracerTest, task) { + const auto rowType = + ROW({"c0", "c1", "c2", "c3", "c4", "c5"}, + {BIGINT(), SMALLINT(), TINYINT(), VARCHAR(), VARCHAR(), VARCHAR()}); + std::vector rows; + constexpr auto numBatch = 1; + rows.reserve(numBatch); + for (auto i = 0; i < numBatch; ++i) { + rows.push_back(vectorFuzzer_.fuzzRow(rowType, 2)); + } + + auto planNodeIdGenerator = std::make_shared(); + const auto planNode = + PlanBuilder(planNodeIdGenerator) + .values(rows, false) + .project({"c0", "c1", "c2"}) + .hashJoin( + {"c0"}, + {"u0"}, + PlanBuilder(planNodeIdGenerator) + .values(rows, true) + .singleAggregation({"c0", "c1"}, {"min(c2)"}) + .project({"c0 AS u0", "c1 AS u1", "a0 AS u2"}) + .planNode(), + "c0 < 135", + {"c0", "c1", "c2"}, + core::JoinType::kInner) + .planNode(); + const auto expectedResult = + AssertQueryBuilder(planNode).maxDrivers(1).copyResults(pool()); + + for (const auto& queryTraceNodeIds : {"1,2", ""}) { + const auto outputDir = TempDirectoryPath::create(); + const auto expectedQueryConfigs = + std::unordered_map{ + {core::QueryConfig::kSpillEnabled, "true"}, + {core::QueryConfig::kSpillNumPartitionBits, "17"}, + {core::QueryConfig::kQueryTraceEnabled, "true"}, + {core::QueryConfig::kQueryTraceDir, outputDir->getPath()}, + {core::QueryConfig::kQueryTraceEnabled, queryTraceNodeIds}, + {"key1", "value1"}, + }; + const auto expectedConnectorProperties = + std::unordered_map>{ + {"test_trace", + std::make_shared( + std::unordered_map{ + {"cKey1", "cVal1"}})}}; + const auto queryCtx = core::QueryCtx::create( + executor_.get(), + core::QueryConfig(expectedQueryConfigs), + expectedConnectorProperties); + + std::shared_ptr task; + const auto result = AssertQueryBuilder(planNode) + .queryCtx(queryCtx) + .maxDrivers(1) + .copyResults(pool(), task); + assertEqualResults({result}, {expectedResult}); + + const auto expectedDir = + fmt::format("{}/{}", outputDir->getPath(), task->taskId()); + const auto fs = filesystems::getFileSystem(expectedDir, nullptr); + const auto actaulDirs = fs->list(outputDir->getPath()); + ASSERT_EQ(actaulDirs.size(), 1); + ASSERT_EQ(actaulDirs.at(0), expectedDir); + + std::unordered_map acutalQueryConfigs; + std:: + unordered_map> + actualConnectorProperties; + core::PlanNodePtr actualQueryPlan; + auto reader = trace::QueryMetadataReader(expectedDir, pool()); + reader.read(acutalQueryConfigs, actualConnectorProperties, actualQueryPlan); + + ASSERT_TRUE(isSamePlan(actualQueryPlan, planNode)); + ASSERT_EQ(acutalQueryConfigs.size(), expectedQueryConfigs.size()); + for (const auto& [key, value] : acutalQueryConfigs) { + ASSERT_EQ(acutalQueryConfigs.at(key), expectedQueryConfigs.at(key)); + } + + ASSERT_EQ( + actualConnectorProperties.size(), expectedConnectorProperties.size()); + ASSERT_EQ(actualConnectorProperties.count("test_trace"), 1); + const auto expectedConnectorConfigs = + expectedConnectorProperties.at("test_trace")->rawConfigsCopy(); + const auto actualConnectorConfigs = + actualConnectorProperties.at("test_trace"); + for (const auto& [key, value] : actualConnectorConfigs) { + ASSERT_EQ( + actualConnectorConfigs.at(key), expectedConnectorConfigs.at(key)); + } + } +} + +TEST_F(QueryTracerTest, error) { + const auto planNode = PlanBuilder().values({}).planNode(); + const auto expectedQueryConfigs = + std::unordered_map{ + {core::QueryConfig::kSpillEnabled, "true"}, + {core::QueryConfig::kSpillNumPartitionBits, "17"}, + {core::QueryConfig::kQueryTraceEnabled, "true"}, + }; + const auto queryCtx = core::QueryCtx::create( + executor_.get(), core::QueryConfig(expectedQueryConfigs)); + VELOX_ASSERT_USER_THROW( + AssertQueryBuilder(planNode).queryCtx(queryCtx).maxDrivers(1).copyResults( + pool()), + "Query trace enabled but the trace dir is not set"); +} + +TEST_F(QueryTracerTest, traceDir) { + const auto outputDir = TempDirectoryPath::create(); + const auto rootDir = outputDir->getPath(); + const auto fs = filesystems::getFileSystem(rootDir, nullptr); + auto dir1 = fmt::format("{}/{}", outputDir->getPath(), "t1"); + trace::createTraceDirectory(dir1); + ASSERT_TRUE(fs->exists(dir1)); + + auto dir2 = fmt::format("{}/{}", dir1, "t1_1"); + trace::createTraceDirectory(dir2); + ASSERT_TRUE(fs->exists(dir2)); + + // It will remove the old dir1 along with its subdir when created the dir1 + // again. + trace::createTraceDirectory(dir1); + ASSERT_TRUE(fs->exists(dir1)); + ASSERT_FALSE(fs->exists(dir2)); + + const auto parentDir = fmt::format("{}/{}", outputDir->getPath(), "p"); + fs->mkdir(parentDir); + + constexpr auto numThreads = 5; + std::vector queryThreads; + queryThreads.reserve(numThreads); + std::set expectedDirs; + for (int i = 0; i < numThreads; ++i) { + queryThreads.emplace_back([&, i]() { + const auto dir = fmt::format("{}/s{}", parentDir, i); + trace::createTraceDirectory(dir); + expectedDirs.insert(dir); + }); + } + + for (auto& queryThread : queryThreads) { + queryThread.join(); + } + + const auto actualDirs = fs->list(parentDir); + ASSERT_EQ(actualDirs.size(), numThreads); + ASSERT_EQ(actualDirs.size(), expectedDirs.size()); + for (const auto& dir : actualDirs) { + ASSERT_EQ(expectedDirs.count(dir), 1); + } +} } // namespace facebook::velox::exec::test + +// This main is needed for some tests on linux. +int main(int argc, char** argv) { + testing::InitGoogleTest(&argc, argv); + // Signal handler required for ThreadDebugInfoTest + facebook::velox::process::addDefaultFatalSignalHandler(); + folly::Init init(&argc, &argv, false); + return RUN_ALL_TESTS(); +}