blob: a090dd89d556d4c6c2605b7e09a3213099bd70a3 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//! Define JNI APIs which can be called from Java/Scala.
use super::{serde, utils::SparkArrowConvert};
use crate::{
errors::{try_unwrap_or_throw, CometError, CometResult},
execution::{
metrics::utils::update_comet_metric, planner::PhysicalPlanner, serde::to_arrow_datatype,
shuffle::spark_unsafe::row::process_sorted_row_partition, sort::RdxSort,
},
jvm_bridge::{jni_new_global_ref, JVMClasses},
};
use arrow::array::{Array, RecordBatch, UInt32Array};
use arrow::compute::{take, TakeOptions};
use arrow::datatypes::DataType as ArrowDataType;
use datafusion::common::{Result as DataFusionResult, ScalarValue};
use datafusion::execution::disk_manager::DiskManagerMode;
use datafusion::execution::memory_pool::MemoryPool;
use datafusion::execution::runtime_env::RuntimeEnvBuilder;
use datafusion::logical_expr::ScalarUDF;
use datafusion::{
execution::disk_manager::DiskManagerBuilder,
physical_plan::{display::DisplayableExecutionPlan, SendableRecordBatchStream},
prelude::{SessionConfig, SessionContext},
};
use datafusion_comet_proto::spark_operator::Operator;
use datafusion_spark::function::bitwise::bit_get::SparkBitGet;
use datafusion_spark::function::bitwise::bitwise_not::SparkBitwiseNot;
use datafusion_spark::function::datetime::date_add::SparkDateAdd;
use datafusion_spark::function::datetime::date_sub::SparkDateSub;
use datafusion_spark::function::datetime::last_day::SparkLastDay;
use datafusion_spark::function::datetime::next_day::SparkNextDay;
use datafusion_spark::function::hash::crc32::SparkCrc32;
use datafusion_spark::function::hash::sha1::SparkSha1;
use datafusion_spark::function::hash::sha2::SparkSha2;
use datafusion_spark::function::map::map_from_entries::MapFromEntries;
use datafusion_spark::function::math::expm1::SparkExpm1;
use datafusion_spark::function::math::hex::SparkHex;
use datafusion_spark::function::math::width_bucket::SparkWidthBucket;
use datafusion_spark::function::string::char::CharFunc;
use datafusion_spark::function::string::concat::SparkConcat;
use futures::poll;
use futures::stream::StreamExt;
use jni::objects::JByteBuffer;
use jni::sys::{jlongArray, JNI_FALSE};
use jni::{
errors::Result as JNIResult,
objects::{
GlobalRef, JByteArray, JClass, JIntArray, JLongArray, JObject, JObjectArray, JString,
ReleaseMode,
},
sys::{jboolean, jdouble, jint, jlong},
JNIEnv,
};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use std::{sync::Arc, task::Poll};
use tokio::runtime::Runtime;
use tokio::sync::mpsc;
use crate::execution::memory_pools::{
create_memory_pool, handle_task_shared_pool_release, parse_memory_pool_config, MemoryPoolConfig,
};
use crate::execution::operators::ScanExec;
use crate::execution::shuffle::{read_ipc_compressed, CompressionCodec};
use crate::execution::spark_plan::SparkPlan;
use crate::execution::tracing::{log_memory_usage, trace_begin, trace_end, with_trace};
use crate::execution::memory_pools::logging_pool::LoggingMemoryPool;
use crate::execution::spark_config::{
SparkConfig, COMET_DEBUG_ENABLED, COMET_DEBUG_MEMORY, COMET_EXPLAIN_NATIVE_ENABLED,
COMET_MAX_TEMP_DIRECTORY_SIZE, COMET_TRACING_ENABLED, SPARK_EXECUTOR_CORES,
};
use crate::parquet::encryption_support::{CometEncryptionFactory, ENCRYPTION_FACTORY_ID};
use datafusion_comet_proto::spark_operator::operator::OpStruct;
use log::info;
use std::sync::OnceLock;
#[cfg(feature = "jemalloc")]
use tikv_jemalloc_ctl::{epoch, stats};
static TOKIO_RUNTIME: OnceLock<Runtime> = OnceLock::new();
fn parse_usize_env_var(name: &str) -> Option<usize> {
std::env::var_os(name).and_then(|n| n.to_str().and_then(|s| s.parse::<usize>().ok()))
}
fn build_runtime(default_worker_threads: Option<usize>) -> Runtime {
let mut builder = tokio::runtime::Builder::new_multi_thread();
if let Some(n) = parse_usize_env_var("COMET_WORKER_THREADS") {
info!("Comet tokio runtime: using COMET_WORKER_THREADS={n}");
builder.worker_threads(n);
} else if let Some(n) = default_worker_threads {
info!("Comet tokio runtime: using spark.executor.cores={n} worker threads");
builder.worker_threads(n);
} else {
info!("Comet tokio runtime: using default thread count");
}
if let Some(n) = parse_usize_env_var("COMET_MAX_BLOCKING_THREADS") {
builder.max_blocking_threads(n);
}
builder
.enable_all()
.build()
.expect("Failed to create Tokio runtime")
}
/// Initialize the global Tokio runtime with the given default worker thread count.
/// If the runtime is already initialized, this is a no-op.
pub fn init_runtime(default_worker_threads: usize) {
TOKIO_RUNTIME.get_or_init(|| build_runtime(Some(default_worker_threads)));
}
/// Function to get a handle to the global Tokio runtime
pub fn get_runtime() -> &'static Runtime {
TOKIO_RUNTIME.get_or_init(|| build_runtime(None))
}
/// Comet native execution context. Kept alive across JNI calls.
struct ExecutionContext {
/// The id of the execution context.
pub id: i64,
/// Task attempt id
pub task_attempt_id: i64,
/// The deserialized Spark plan
pub spark_plan: Operator,
/// The number of partitions
pub partition_count: usize,
/// The DataFusion root operator converted from the `spark_plan`
pub root_op: Option<Arc<SparkPlan>>,
/// The input sources for the DataFusion plan
pub scans: Vec<ScanExec>,
/// The global reference of input sources for the DataFusion plan
pub input_sources: Vec<Arc<GlobalRef>>,
/// The record batch stream to pull results from
pub stream: Option<SendableRecordBatchStream>,
/// Receives batches from a spawned tokio task (async I/O path)
pub batch_receiver: Option<mpsc::Receiver<DataFusionResult<RecordBatch>>>,
/// Native metrics
pub metrics: Arc<GlobalRef>,
// The interval in milliseconds to update metrics
pub metrics_update_interval: Option<Duration>,
// The last update time of metrics
pub metrics_last_update_time: Instant,
/// Counter to avoid checking time on every poll iteration (reduces syscalls)
pub poll_count_since_metrics_check: u32,
/// The time it took to create the native plan and configure the context
pub plan_creation_time: Duration,
/// DataFusion SessionContext
pub session_ctx: Arc<SessionContext>,
/// Whether to enable additional debugging checks & messages
pub debug_native: bool,
/// Whether to write native plans with metrics to stdout
pub explain_native: bool,
/// Memory pool config
pub memory_pool_config: MemoryPoolConfig,
/// Whether to log memory usage on each call to execute_plan
pub tracing_enabled: bool,
}
/// Accept serialized query plan and return the address of the native query plan.
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_createPlan(
e: JNIEnv,
_class: JClass,
id: jlong,
iterators: JObjectArray,
serialized_query: JByteArray,
serialized_spark_configs: JByteArray,
partition_count: jint,
metrics_node: JObject,
metrics_update_interval: jlong,
comet_task_memory_manager_obj: JObject,
local_dirs: JObjectArray,
batch_size: jint,
off_heap_mode: jboolean,
memory_pool_type: JString,
memory_limit: jlong,
memory_limit_per_task: jlong,
task_attempt_id: jlong,
task_cpus: jlong,
key_unwrapper_obj: JObject,
) -> jlong {
try_unwrap_or_throw(&e, |mut env| {
// Deserialize Spark configs
let bytes = env.convert_byte_array(serialized_spark_configs)?;
let spark_configs = serde::deserialize_config(bytes.as_slice())?;
let spark_config: HashMap<String, String> = spark_configs.entries.into_iter().collect();
// Initialize the tokio runtime with spark.executor.cores as the default
// worker thread count, falling back to 1 if not set.
let executor_cores = spark_config.get_usize(SPARK_EXECUTOR_CORES, 1);
init_runtime(executor_cores);
// Access Comet configs
let debug_native = spark_config.get_bool(COMET_DEBUG_ENABLED);
let explain_native = spark_config.get_bool(COMET_EXPLAIN_NATIVE_ENABLED);
let tracing_enabled = spark_config.get_bool(COMET_TRACING_ENABLED);
let max_temp_directory_size =
spark_config.get_u64(COMET_MAX_TEMP_DIRECTORY_SIZE, 100 * 1024 * 1024 * 1024);
let logging_memory_pool = spark_config.get_bool(COMET_DEBUG_MEMORY);
with_trace("createPlan", tracing_enabled, || {
// Init JVM classes
JVMClasses::init(&mut env);
let start = Instant::now();
// Deserialize query plan
let bytes = env.convert_byte_array(serialized_query)?;
let spark_plan = serde::deserialize_op(bytes.as_slice())?;
let metrics = Arc::new(jni_new_global_ref!(env, metrics_node)?);
// Get the global references of input sources
let mut input_sources = vec![];
let num_inputs = env.get_array_length(&iterators)?;
for i in 0..num_inputs {
let input_source = env.get_object_array_element(&iterators, i)?;
let input_source = Arc::new(jni_new_global_ref!(env, input_source)?);
input_sources.push(input_source);
}
// Create DataFusion memory pool
let task_memory_manager =
Arc::new(jni_new_global_ref!(env, comet_task_memory_manager_obj)?);
let memory_pool_type = env.get_string(&memory_pool_type)?.into();
let memory_pool_config = parse_memory_pool_config(
off_heap_mode != JNI_FALSE,
memory_pool_type,
memory_limit,
memory_limit_per_task,
)?;
let memory_pool =
create_memory_pool(&memory_pool_config, task_memory_manager, task_attempt_id);
let memory_pool = if logging_memory_pool {
Arc::new(LoggingMemoryPool::new(task_attempt_id as u64, memory_pool))
} else {
memory_pool
};
// Get local directories for storing spill files
let num_local_dirs = env.get_array_length(&local_dirs)?;
let mut local_dirs_vec = vec![];
for i in 0..num_local_dirs {
let local_dir: JString = env.get_object_array_element(&local_dirs, i)?.into();
let local_dir = env.get_string(&local_dir)?;
local_dirs_vec.push(local_dir.into());
}
// We need to keep the session context alive. Some session state like temporary
// dictionaries are stored in session context. If it is dropped, the temporary
// dictionaries will be dropped as well.
let session = prepare_datafusion_session_context(
batch_size as usize,
memory_pool,
local_dirs_vec,
max_temp_directory_size,
task_cpus as usize,
&spark_config,
)?;
let plan_creation_time = start.elapsed();
let metrics_update_interval = if metrics_update_interval > 0 {
Some(Duration::from_millis(metrics_update_interval as u64))
} else {
None
};
// Handle key unwrapper for encrypted files
if !key_unwrapper_obj.is_null() {
let encryption_factory = CometEncryptionFactory {
key_unwrapper: jni_new_global_ref!(env, key_unwrapper_obj)?,
};
session.runtime_env().register_parquet_encryption_factory(
ENCRYPTION_FACTORY_ID,
Arc::new(encryption_factory),
);
}
let exec_context = Box::new(ExecutionContext {
id,
task_attempt_id,
spark_plan,
partition_count: partition_count as usize,
root_op: None,
scans: vec![],
input_sources,
stream: None,
batch_receiver: None,
metrics,
metrics_update_interval,
metrics_last_update_time: Instant::now(),
poll_count_since_metrics_check: 0,
plan_creation_time,
session_ctx: Arc::new(session),
debug_native,
explain_native,
memory_pool_config,
tracing_enabled,
});
Ok(Box::into_raw(exec_context) as i64)
})
})
}
/// Configure DataFusion session context.
fn prepare_datafusion_session_context(
batch_size: usize,
memory_pool: Arc<dyn MemoryPool>,
local_dirs: Vec<String>,
max_temp_directory_size: u64,
task_cpus: usize,
spark_config: &HashMap<String, String>,
) -> CometResult<SessionContext> {
let paths = local_dirs.into_iter().map(PathBuf::from).collect();
let disk_manager = DiskManagerBuilder::default()
.with_mode(DiskManagerMode::Directories(paths))
.with_max_temp_directory_size(max_temp_directory_size);
let mut rt_config = RuntimeEnvBuilder::new().with_disk_manager_builder(disk_manager);
rt_config = rt_config.with_memory_pool(memory_pool);
let mut session_config = SessionConfig::new()
.with_target_partitions(task_cpus)
// This DataFusion context is within the scope of an executing Spark Task. We want to set
// its internal parallelism to the number of CPUs allocated to Spark Tasks. This can be
// modified by changing spark.task.cpus in the Spark config.
.with_batch_size(batch_size)
// DataFusion partial aggregates can emit duplicate rows so we disable the
// skip partial aggregation feature because this is not compatible with Spark's
// use of partial aggregates.
.set(
"datafusion.execution.skip_partial_aggregation_probe_ratio_threshold",
// this is the threshold of number of groups / number of rows and the
// maximum value is 1.0, so we set the threshold a little higher just
// to be safe
&ScalarValue::Float64(Some(1.1)),
);
// Pass through DataFusion configs from Spark.
// e.g: spark-shell --conf spark.comet.datafusion.sql_parser.parse_float_as_decimal=true
// becomes datafusion.sql_parser.parse_float_as_decimal=true
const SPARK_COMET_DF_PREFIX: &str = "spark.comet.datafusion.";
for (key, value) in spark_config {
if let Some(df_key) = key.strip_prefix(SPARK_COMET_DF_PREFIX) {
let df_key = format!("datafusion.{df_key}");
session_config = session_config.set_str(&df_key, value);
}
}
let runtime = rt_config.build()?;
let mut session_ctx = SessionContext::new_with_config_rt(session_config, Arc::new(runtime));
datafusion::functions_nested::register_all(&mut session_ctx)?;
register_datafusion_spark_function(&session_ctx);
// Must be the last one to override existing functions with the same name
datafusion_comet_spark_expr::register_all_comet_functions(&mut session_ctx)?;
Ok(session_ctx)
}
// register UDFs from datafusion-spark crate
fn register_datafusion_spark_function(session_ctx: &SessionContext) {
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkExpm1::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkSha2::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(CharFunc::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkBitGet::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkDateAdd::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkDateSub::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkLastDay::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkNextDay::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkSha1::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkConcat::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkBitwiseNot::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkHex::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkWidthBucket::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(MapFromEntries::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkCrc32::default()));
session_ctx.register_udf(ScalarUDF::new_from_impl(SparkSpace::default()));
}
/// Prepares arrow arrays for output.
fn prepare_output(
env: &mut JNIEnv,
array_addrs: JLongArray,
schema_addrs: JLongArray,
output_batch: RecordBatch,
validate: bool,
) -> CometResult<jlong> {
let num_cols = env.get_array_length(&array_addrs)? as usize;
let array_addrs = unsafe { env.get_array_elements(&array_addrs, ReleaseMode::NoCopyBack)? };
let array_addrs = &*array_addrs;
let schema_addrs = unsafe { env.get_array_elements(&schema_addrs, ReleaseMode::NoCopyBack)? };
let schema_addrs = &*schema_addrs;
let results = output_batch.columns();
let num_rows = output_batch.num_rows();
// there are edge cases where num_cols can be zero due to Spark optimizations
// when the results of a query are not used
if num_cols > 0 {
if results.len() != num_cols {
return Err(CometError::Internal(format!(
"Output column count mismatch: expected {num_cols}, got {}",
results.len()
)));
}
if validate {
// Validate the output arrays.
for array in results.iter() {
let array_data = array.to_data();
array_data
.validate_full()
.expect("Invalid output array data");
}
}
let mut i = 0;
while i < results.len() {
let array_ref = results.get(i).ok_or(CometError::IndexOutOfBounds(i))?;
if array_ref.offset() != 0 {
// https://github.com/apache/datafusion-comet/issues/2051
// Bug with non-zero offset FFI, so take to a new array which will have an offset of 0.
// We expect this to be a cold code path, hence the check_bounds: true and assert_eq.
let indices = UInt32Array::from((0..num_rows as u32).collect::<Vec<u32>>());
let new_array = take(
array_ref,
&indices,
Some(TakeOptions { check_bounds: true }),
)?;
assert_eq!(new_array.offset(), 0);
new_array
.to_data()
.move_to_spark(array_addrs[i], schema_addrs[i])?;
} else {
array_ref
.to_data()
.move_to_spark(array_addrs[i], schema_addrs[i])?;
}
i += 1;
}
}
Ok(num_rows as jlong)
}
/// Pull the next input from JVM. Note that we cannot pull input batches in
/// `ScanStream.poll_next` when the execution stream is polled for output.
/// Because the input source could be another native execution stream, which
/// will be executed in another tokio blocking thread. It causes JNI throw
/// Java exception. So we pull input batches here and insert them into scan
/// operators before polling the stream,
#[inline]
fn pull_input_batches(exec_context: &mut ExecutionContext) -> Result<(), CometError> {
exec_context.scans.iter_mut().try_for_each(|scan| {
scan.get_next_batch()?;
Ok::<(), CometError>(())
})
}
/// Accept serialized query plan and the addresses of Arrow Arrays from Spark,
/// then execute the query. Return addresses of arrow vector.
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_executePlan(
e: JNIEnv,
_class: JClass,
stage_id: jint,
partition: jint,
exec_context: jlong,
array_addrs: JLongArray,
schema_addrs: JLongArray,
) -> jlong {
try_unwrap_or_throw(&e, |mut env| {
// Retrieve the query
let exec_context = get_execution_context(exec_context);
let tracing_event_name = match &exec_context.spark_plan.op_struct {
Some(OpStruct::ShuffleWriter(_)) => "executePlan(ShuffleWriter)",
_ => "executePlan",
};
if exec_context.tracing_enabled {
#[cfg(feature = "jemalloc")]
{
let e = epoch::mib().unwrap();
let allocated = stats::allocated::mib().unwrap();
e.advance().unwrap();
use crate::execution::tracing::log_memory_usage;
log_memory_usage("jemalloc_allocated", allocated.read().unwrap() as u64);
}
}
with_trace(tracing_event_name, exec_context.tracing_enabled, || {
let exec_context_id = exec_context.id;
// Initialize the execution stream.
// Because we don't know if input arrays are dictionary-encoded when we create
// query plan, we need to defer stream initialization to first time execution.
if exec_context.root_op.is_none() {
let start = Instant::now();
let planner =
PhysicalPlanner::new(Arc::clone(&exec_context.session_ctx), partition)
.with_exec_id(exec_context_id);
let (scans, root_op) = planner.create_plan(
&exec_context.spark_plan,
&mut exec_context.input_sources.clone(),
exec_context.partition_count,
)?;
let physical_plan_time = start.elapsed();
exec_context.plan_creation_time += physical_plan_time;
exec_context.root_op = Some(Arc::clone(&root_op));
exec_context.scans = scans;
if exec_context.explain_native {
let formatted_plan_str =
DisplayableExecutionPlan::new(root_op.native_plan.as_ref()).indent(true);
info!("Comet native query plan:\n{formatted_plan_str:}");
}
let task_ctx = exec_context.session_ctx.task_ctx();
// Each Comet native execution corresponds to a single Spark partition,
// so we should always execute partition 0.
let stream = root_op.native_plan.execute(0, task_ctx)?;
if exec_context.scans.is_empty() {
// No JVM data sources — spawn onto tokio so the executor
// thread parks in blocking_recv instead of busy-polling.
//
// Channel capacity of 2 allows the producer to work one batch
// ahead while the consumer processes the current one via JNI,
// without buffering excessive memory. Increasing this would
// trade memory for latency hiding if JNI/FFI overhead dominates;
// decreasing to 1 would serialize production and consumption.
let (tx, rx) = mpsc::channel(2);
let mut stream = stream;
get_runtime().spawn(async move {
while let Some(batch) = stream.next().await {
if tx.send(batch).await.is_err() {
break;
}
}
});
exec_context.batch_receiver = Some(rx);
} else {
exec_context.stream = Some(stream);
}
} else {
// Pull input batches
pull_input_batches(exec_context)?;
}
if let Some(rx) = &mut exec_context.batch_receiver {
match rx.blocking_recv() {
Some(Ok(batch)) => {
update_metrics(&mut env, exec_context)?;
return prepare_output(
&mut env,
array_addrs,
schema_addrs,
batch,
exec_context.debug_native,
);
}
Some(Err(e)) => {
return Err(e.into());
}
None => {
log_plan_metrics(exec_context, stage_id, partition);
return Ok(-1);
}
}
}
// ScanExec path: busy-poll to interleave JVM batch pulls with stream polling
get_runtime().block_on(async {
loop {
let next_item = exec_context.stream.as_mut().unwrap().next();
let poll_output = poll!(next_item);
// Only check time every 100 polls to reduce syscall overhead
if let Some(interval) = exec_context.metrics_update_interval {
exec_context.poll_count_since_metrics_check += 1;
if exec_context.poll_count_since_metrics_check >= 100 {
let now = Instant::now();
if now - exec_context.metrics_last_update_time >= interval {
update_metrics(&mut env, exec_context)?;
exec_context.metrics_last_update_time = now;
}
exec_context.poll_count_since_metrics_check = 0;
}
}
match poll_output {
Poll::Ready(Some(output)) => {
return prepare_output(
&mut env,
array_addrs,
schema_addrs,
output?,
exec_context.debug_native,
);
}
Poll::Ready(None) => {
log_plan_metrics(exec_context, stage_id, partition);
return Ok(-1);
}
Poll::Pending => {
// JNI call to pull batches from JVM into ScanExec operators.
// block_in_place lets tokio move other tasks off this worker
// while we wait for JVM data.
tokio::task::block_in_place(|| pull_input_batches(exec_context))?;
}
}
}
})
})
})
}
#[no_mangle]
/// Drop the native query plan object and context object.
pub extern "system" fn Java_org_apache_comet_Native_releasePlan(
e: JNIEnv,
_class: JClass,
exec_context: jlong,
) {
try_unwrap_or_throw(&e, |mut env| unsafe {
let execution_context = get_execution_context(exec_context);
// Update metrics
update_metrics(&mut env, execution_context)?;
handle_task_shared_pool_release(
execution_context.memory_pool_config.pool_type,
execution_context.task_attempt_id,
);
let _: Box<ExecutionContext> = Box::from_raw(execution_context);
Ok(())
})
}
/// Updates the metrics of the query plan.
fn update_metrics(env: &mut JNIEnv, exec_context: &mut ExecutionContext) -> CometResult<()> {
if let Some(native_query) = &exec_context.root_op {
let metrics = exec_context.metrics.as_obj();
update_comet_metric(env, metrics, native_query)
} else {
Ok(())
}
}
fn log_plan_metrics(exec_context: &ExecutionContext, stage_id: jint, partition: jint) {
if exec_context.explain_native {
if let Some(plan) = &exec_context.root_op {
let formatted_plan_str =
DisplayableExecutionPlan::with_metrics(plan.native_plan.as_ref()).indent(true);
info!(
"Comet native query plan with metrics (Plan #{} Stage {} Partition {}):\
\n plan creation took {:?}:\
\n{formatted_plan_str:}",
plan.plan_id, stage_id, partition, exec_context.plan_creation_time
);
}
}
}
fn convert_datatype_arrays(
env: &'_ mut JNIEnv<'_>,
serialized_datatypes: JObjectArray,
) -> JNIResult<Vec<ArrowDataType>> {
let array_len = env.get_array_length(&serialized_datatypes)?;
let mut res: Vec<ArrowDataType> = Vec::new();
for i in 0..array_len {
let inner_array = env.get_object_array_element(&serialized_datatypes, i)?;
let inner_array: JByteArray = inner_array.into();
let bytes = env.convert_byte_array(inner_array)?;
let data_type = serde::deserialize_data_type(bytes.as_slice()).unwrap();
let arrow_dt = to_arrow_datatype(&data_type);
res.push(arrow_dt);
}
Ok(res)
}
fn get_execution_context<'a>(id: i64) -> &'a mut ExecutionContext {
unsafe {
(id as *mut ExecutionContext)
.as_mut()
.expect("Comet execution context shouldn't be null!")
}
}
/// Used by Comet shuffle external sorter to write sorted records to disk.
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_writeSortedFileNative(
e: JNIEnv,
_class: JClass,
row_addresses: JLongArray,
row_sizes: JIntArray,
serialized_datatypes: JObjectArray,
file_path: JString,
prefer_dictionary_ratio: jdouble,
batch_size: jlong,
checksum_enabled: jboolean,
checksum_algo: jint,
current_checksum: jlong,
compression_codec: JString,
compression_level: jint,
tracing_enabled: jboolean,
) -> jlongArray {
try_unwrap_or_throw(&e, |mut env| unsafe {
with_trace(
"writeSortedFileNative",
tracing_enabled != JNI_FALSE,
|| {
let data_types = convert_datatype_arrays(&mut env, serialized_datatypes)?;
let row_num = env.get_array_length(&row_addresses)? as usize;
let row_addresses =
env.get_array_elements(&row_addresses, ReleaseMode::NoCopyBack)?;
let row_sizes = env.get_array_elements(&row_sizes, ReleaseMode::NoCopyBack)?;
let row_addresses_ptr = row_addresses.as_ptr();
let row_sizes_ptr = row_sizes.as_ptr();
let output_path: String = env.get_string(&file_path).unwrap().into();
let checksum_enabled = checksum_enabled == 1;
let current_checksum = if current_checksum == i64::MIN {
// Initial checksum is not available.
None
} else {
Some(current_checksum as u32)
};
let compression_codec: String = env.get_string(&compression_codec).unwrap().into();
let compression_codec = match compression_codec.as_str() {
"zstd" => CompressionCodec::Zstd(compression_level),
"lz4" => CompressionCodec::Lz4Frame,
"snappy" => CompressionCodec::Snappy,
_ => CompressionCodec::Lz4Frame,
};
let (written_bytes, checksum) = process_sorted_row_partition(
row_num,
batch_size as usize,
row_addresses_ptr,
row_sizes_ptr,
&data_types,
output_path,
prefer_dictionary_ratio,
checksum_enabled,
checksum_algo,
current_checksum,
&compression_codec,
)?;
let checksum = if let Some(checksum) = checksum {
checksum as i64
} else {
// Spark checksums (CRC32 or Adler32) are both u32, so we use i64::MIN to indicate
// checksum is not available.
i64::MIN
};
let long_array = env.new_long_array(2)?;
env.set_long_array_region(&long_array, 0, &[written_bytes, checksum])?;
Ok(long_array.into_raw())
},
)
})
}
#[no_mangle]
/// Used by Comet shuffle external sorter to sort in-memory row partition ids.
pub extern "system" fn Java_org_apache_comet_Native_sortRowPartitionsNative(
e: JNIEnv,
_class: JClass,
address: jlong,
size: jlong,
tracing_enabled: jboolean,
) {
try_unwrap_or_throw(&e, |_| {
with_trace(
"sortRowPartitionsNative",
tracing_enabled != JNI_FALSE,
|| {
// SAFETY: JVM unsafe memory allocation is aligned with long.
let array =
unsafe { std::slice::from_raw_parts_mut(address as *mut i64, size as usize) };
array.rdxsort();
Ok(())
},
)
})
}
#[no_mangle]
/// Used by Comet native shuffle reader
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
pub unsafe extern "system" fn Java_org_apache_comet_Native_decodeShuffleBlock(
e: JNIEnv,
_class: JClass,
byte_buffer: JByteBuffer,
length: jint,
array_addrs: JLongArray,
schema_addrs: JLongArray,
tracing_enabled: jboolean,
) -> jlong {
try_unwrap_or_throw(&e, |mut env| {
with_trace("decodeShuffleBlock", tracing_enabled != JNI_FALSE, || {
let raw_pointer = env.get_direct_buffer_address(&byte_buffer)?;
let length = length as usize;
let slice: &[u8] = unsafe { std::slice::from_raw_parts(raw_pointer, length) };
let batch = read_ipc_compressed(slice)?;
prepare_output(&mut env, array_addrs, schema_addrs, batch, false)
})
})
}
#[no_mangle]
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
pub unsafe extern "system" fn Java_org_apache_comet_Native_traceBegin(
e: JNIEnv,
_class: JClass,
event: JString,
) {
try_unwrap_or_throw(&e, |mut env| {
let name: String = env.get_string(&event).unwrap().into();
trace_begin(&name);
Ok(())
})
}
#[no_mangle]
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
pub unsafe extern "system" fn Java_org_apache_comet_Native_traceEnd(
e: JNIEnv,
_class: JClass,
event: JString,
) {
try_unwrap_or_throw(&e, |mut env| {
let name: String = env.get_string(&event).unwrap().into();
trace_end(&name);
Ok(())
})
}
#[no_mangle]
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
pub unsafe extern "system" fn Java_org_apache_comet_Native_logMemoryUsage(
e: JNIEnv,
_class: JClass,
name: JString,
value: jlong,
) {
try_unwrap_or_throw(&e, |mut env| {
let name: String = env.get_string(&name).unwrap().into();
log_memory_usage(&name, value as u64);
Ok(())
})
}
// ============================================================================
// Native Columnar to Row Conversion
// ============================================================================
use crate::execution::columnar_to_row::ColumnarToRowContext;
use arrow::ffi::{from_ffi, FFI_ArrowArray, FFI_ArrowSchema};
use datafusion_spark::function::string::space::SparkSpace;
/// Initialize a native columnar to row converter.
///
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_columnarToRowInit(
e: JNIEnv,
_class: JClass,
serialized_schema: JObjectArray,
batch_size: jint,
) -> jlong {
try_unwrap_or_throw(&e, |mut env| {
// Deserialize the schema
let schema = convert_datatype_arrays(&mut env, serialized_schema)?;
// Create the context
let ctx = Box::new(ColumnarToRowContext::new(schema, batch_size as usize));
Ok(Box::into_raw(ctx) as jlong)
})
}
/// Convert Arrow columnar data to Spark UnsafeRow format.
///
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_columnarToRowConvert(
e: JNIEnv,
_class: JClass,
c2r_handle: jlong,
array_addrs: JLongArray,
schema_addrs: JLongArray,
num_rows: jint,
) -> jni::sys::jobject {
try_unwrap_or_throw(&e, |mut env| {
// Get the context
let ctx = (c2r_handle as *mut ColumnarToRowContext)
.as_mut()
.ok_or_else(|| CometError::Internal("Null columnar to row context".to_string()))?;
let num_cols = env.get_array_length(&array_addrs)? as usize;
// Get array and schema addresses
let array_addrs_elements = env.get_array_elements(&array_addrs, ReleaseMode::NoCopyBack)?;
let schema_addrs_elements =
env.get_array_elements(&schema_addrs, ReleaseMode::NoCopyBack)?;
// Import Arrow arrays from FFI
let mut arrays = Vec::with_capacity(num_cols);
for i in 0..num_cols {
let array_ptr = array_addrs_elements[i] as *mut FFI_ArrowArray;
let schema_ptr = schema_addrs_elements[i] as *mut FFI_ArrowSchema;
// Take ownership of the FFI structures
let ffi_array = std::ptr::read(array_ptr);
let ffi_schema = std::ptr::read(schema_ptr);
// Convert to Arrow ArrayData
let array_data = from_ffi(ffi_array, &ffi_schema)
.map_err(|e| CometError::Internal(format!("Failed to import array: {}", e)))?;
arrays.push(arrow::array::make_array(array_data));
}
// Convert columnar to row
let (buffer_ptr, offsets, lengths) = ctx.convert(&arrays, num_rows as usize)?;
// Create Java int arrays for offsets and lengths
let offsets_array = env.new_int_array(offsets.len() as i32)?;
env.set_int_array_region(&offsets_array, 0, offsets)?;
let lengths_array = env.new_int_array(lengths.len() as i32)?;
env.set_int_array_region(&lengths_array, 0, lengths)?;
// Create the NativeColumnarToRowInfo object
let info_class = env.find_class("org/apache/comet/NativeColumnarToRowInfo")?;
let info_obj = env.new_object(
info_class,
"(J[I[I)V",
&[
jni::objects::JValue::Long(buffer_ptr as jlong),
jni::objects::JValue::Object(&offsets_array),
jni::objects::JValue::Object(&lengths_array),
],
)?;
Ok(info_obj.into_raw())
})
}
/// Close and release the native columnar to row converter.
///
/// # Safety
/// This function is inherently unsafe since it deals with raw pointers passed from JNI.
#[no_mangle]
pub unsafe extern "system" fn Java_org_apache_comet_Native_columnarToRowClose(
e: JNIEnv,
_class: JClass,
c2r_handle: jlong,
) {
try_unwrap_or_throw(&e, |_env| {
if c2r_handle != 0 {
let _ctx: Box<ColumnarToRowContext> =
Box::from_raw(c2r_handle as *mut ColumnarToRowContext);
// ctx is dropped here, freeing the buffer
}
Ok(())
})
}