sunchao commented on code in PR #5027:
URL: https://github.com/apache/datafusion-comet/pull/5027#discussion_r3868456475


##########
spark/src/main/java/org/apache/comet/udf/CometUdfBridge.java:
##########
@@ -254,4 +271,225 @@ private static void evaluateInternal(
       }
     }
   }
+
+  /** Visible to the focused allocator test in this package. */
+  static BufferAllocator taskAllocator(TaskContext taskContext) {
+    return taskState(taskContext).allocator();
+  }
+
+  /** Visible to the focused allocator test in this package. */
+  static int taskStateCount() {
+    return TASKS.size();
+  }
+
+  /** Visible to the focused allocator test in this package. */
+  static Runnable beginTaskEvaluation(TaskContext taskContext) {
+    TaskState state = taskState(taskContext);
+    state.beginEvaluation();
+    return state::finishEvaluation;
+  }
+
+  private static TaskState taskState(TaskContext taskContext) {
+    return TASKS.computeIfAbsent(
+        taskContext,
+        context -> {
+          TaskState state = new TaskState(context, 
CometTaskContextShim.taskMemoryManager(context));
+          context.addTaskCompletionListener(
+              (TaskCompletionListener) ignored -> state.taskCompleted());
+          return state;
+        });
+  }
+
+  /** Per-task Arrow listener and non-spillable Spark memory consumer. */
+  private static final class TaskState implements AllocationListener {
+    private final TaskContext taskContext;
+    private final long taskAttemptId;
+    private final TaskMemoryManager taskMemoryManager;
+    private final TaskMemoryConsumer consumer;
+    private final ConcurrentHashMap<String, CometUDF> instances = new 
ConcurrentHashMap<>();
+
+    private BufferAllocator allocator;
+    // Arrow updates allocator accounting after onPreAllocation returns.
+    private int evaluationsInFlight;
+    private boolean completed;
+    private boolean closed;
+
+    private TaskState(TaskContext taskContext, TaskMemoryManager 
taskMemoryManager) {
+      this.taskContext = taskContext;
+      this.taskAttemptId = taskContext.taskAttemptId();
+      this.taskMemoryManager = taskMemoryManager;
+      this.consumer =
+          taskMemoryManager.getTungstenMemoryMode() == MemoryMode.OFF_HEAP
+              ? new TaskMemoryConsumer(taskMemoryManager)
+              : null;
+    }
+
+    private synchronized BufferAllocator allocator() {
+      if (completed) {
+        throw new IllegalStateException(
+            "Cannot allocate JVM UDF memory after task " + taskAttemptId + " 
completed");
+      }
+      if (allocator == null) {
+        allocator =
+            ROOT_ALLOCATOR.newChildAllocator(
+                "comet-udf-task-" + taskAttemptId, this, 0L, Long.MAX_VALUE);
+      }
+      return allocator;
+    }
+
+    @Override
+    public void onPreAllocation(long size) {
+      // Spark's executor cleanup also synchronizes on TaskMemoryManager. Keep 
that cleanup from
+      // overtaking an admitted allocation, while leaving this TaskState 
monitor free for buffer
+      // releases that can satisfy a blocking acquire.
+      synchronized (taskMemoryManager) {
+        synchronized (this) {
+          if (completed) {
+            throw new OutOfMemoryException(
+                "Cannot allocate " + size + " JVM UDF bytes after task 
completion");
+          }
+        }
+
+        long acquired = consumer == null ? size : consumer.acquireMemory(size);

Review Comment:
   **[P2] Avoid charging exported UDF buffers twice**
   
   The new Spark charge remains until FFI release, but native hash joins and 
shuffles reserve the same buffers against Spark again. `JvmScalarUdfExpr` 
imports the output without copying, and Arrow's single-batch concatenation also 
shares the original buffers. A UDF feeding a single-batch hash-join build can 
therefore fail its native reservation even when the physical allocations fit 
the pool.
   
   A focused component probe using the exact JVM sources with Spark 4.1.3 and 
Arrow 18.3.0 confirmed this: a 32 MiB buffer in a 48 MiB pool receives the full 
32 MiB native reservation with the root-allocator control. With this change, 
the retained JVM charge leaves only about 16 MiB for that same reservation. The 
FFI buffer address is unchanged.
   
   Could we coordinate accounting ownership across the FFI boundary, or exclude 
already-charged buffers from native reservations? The native query path was 
source-traced; this reproduction was not an end-to-end native query.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to