Skip to content

perf: send a native plan's output schema to the JVM only when it changes - #7405

Open
andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:cache-native-output-schema
Open

andygrove wants to merge 2 commits into
apache:mainfrom
andygrove:cache-native-output-schema

Conversation

@andygrove

@andygrove andygrove commented Nov 2, 2026 •

Copy link
Copy Markdown
Member

Which issue does this PR close?

No issue is open for this. #1115 raised the cost of sending the schema with every batch for batches going from the JVM to native, and #4572 removed it there by moving that direction to the Arrow C Stream Interface. This PR removes it for batches going from native to the JVM.

Rationale for this change

executePlan hands each output batch to the JVM as one ArrowArray and one ArrowSchema per column. A plan's output schema rarely changes between batches, but native exported it with every batch, building each column's format string, name, metadata and, for a nested type, its children. The JVM then allocated a struct for it, parsed it back into a Field, and made a JNI call to release it. Measured with the benchmark in this PR, that costs about 0.5 µs of CPU per flat column and 1.2 to 1.4 µs per struct<bigint,string> column in every batch, on top of exporting and importing the arrays themselves.

Exporting the output as an Arrow C Stream, the way #4572 handles input, would also send the schema once. But Arrow Java's ArrowArrayStreamReader loads every batch through Data.importIntoVectorSchemaRoot, which builds a temporary StructVector and then loads its buffers into a reused root. In a standalone measurement of both paths (arrow-rs 59.3, Arrow Java 18.3), that made the stream slower than the current path for single-column and nested outputs. A stream would also turn native errors into IOException text, the problem #6243 worked around for input, and it would reuse one root for every batch, which changes who owns the batches CometExecIterator returns. Keeping executePlan and dropping only the repeated schema leaves errors, metrics and batch ownership as they are.

What changes are included in this PR?

  • Native (jni_api.rs): an OutputSchemaCache held by the ExecutionContext records each output column's data type and field metadata as executePlan last exported them. These two are what move_to_spark builds the FFI_ArrowSchema from. While a batch matches them, prepare_output writes only the arrays, with the new move_array_to_spark, which still applies zero_offsets, and doesn't read the schema addresses. Otherwise it writes the schemas too, records the new ones, and sets OUTPUT_SCHEMA_EXPORTED (bit 32) in the row count it returns. A batch with a dictionary anywhere in its types always carries its schema and clears the record, because the JVM gives each dictionary it imports a new id and keeps the values under that id.
  • JVM: CometExecIterator fetches batches through NativeUtil.getNextPlanBatch, which allocates the ArrowSchema structs once and passes the same, still empty, structs with every batch. When native sets the flag, it imports the batch as getNextBatch does, which consumes the structs, and keeps the batch's fields unless one of them has a dictionary. Otherwise it imports the arrays with those fields through the new ArrowImporter.importVector(array, field, provider), which the schema overload now delegates to. Both imports share one loop and its cleanup. close() frees the schema structs that are left. Decoded shuffle blocks still carry a schema each, through getNextBatch.
  • The internal config spark.comet.exec.cacheOutputSchema.enabled, on by default, makes native send the schema with every batch when it is off. Only native reads it; the JVM follows the flag. It is there for the benchmark and as a fallback.
  • ffi.md and memory_management.md describe the protocol.
  • The second commit adds CometPlanOutputBenchmark, which compares the two settings. Each case reads one column of a Parquet table and projects it 1, 10 or 100 times, so the native plan does almost nothing per output column beyond exporting it, and the JVM only counts the rows of each batch. One more case reads 100 distinct columns. Before timing, the benchmark checks that both settings return every row, and that the plan sent its schema with every batch with the cache off and once per task with it on. Wall time leaves planning out. The benchmark also reports each setting's process CPU time per batch: for a plan with no JVM input the native plan runs on a Tokio worker while the task thread imports the batches, so wall time hides the saving until the JVM side is the slower one, but a busy executor has no idle core to hide it on.

How are these changes tested?

New NativeUtilSuite tests check that:

  • batches that arrive without a schema are imported with the fields sent last, and are passed the same schema structs;
  • the structs native filled before failing are released;
  • the schema structs left at the end of the output are freed by close();
  • a batch without a schema before any schema was sent fails with an error naming the problem;
  • no fields are kept from a batch with a dictionary, so a batch without a schema after it fails;
  • a native plan whose output takes at least 10 batches sends its schema once per task with the cache on, and with every batch with it off, and still matches Spark.

New Rust tests cover the cache's sequence of exports (the first batch writes the schema, a matching one doesn't, a new type writes it again, a dictionary writes it every time, and no addresses write nothing), a disabled cache writing the schema every time, move_array_to_spark read back through a schema exported for an earlier array, the column comparison (type and metadata count, name and nullability don't), and has_dictionary on nested types.

Locally, NativeUtilSuite, CometExecIteratorLifecycleSuite, CometNativeSuite, SparkErrorConverterSuite, CometArrowStreamSuite, CometNativeShuffleSuite and CometExecSuite pass (367 tests), and so does cargo clippy --all-targets --workspace -- -D warnings.

CometPlanOutputBenchmark on macOS (Apple M3 Max, JDK 17.0.10, local[1], an optimized native build without LTO). Wall time is the best of each case's runs, with the cache off, on, and off again to show drift. CPU is the median process CPU time per batch over seven alternating samples. Each case has 1025 batches of 8192 rows, except the last, which has 129.

Output columns Off On Off (repeat) CPU per batch, off CPU per batch, on
100 × bigint, 1 column read 218 ms 172 ms 215 ms 287 µs 239 µs
100 × string, 1 column read 257 ms 196 ms 248 ms 385 µs 337 µs
100 × struct<bigint,string>, 1 column read 623 ms 494 ms 621 ms 808 µs 667 µs
100 distinct bigint, 100 columns read 474 ms 475 ms 473 ms 4165 µs 4137 µs

Earlier runs gave 1.20x to 1.28x for the first three rows. With 1 or 10 output columns the two settings are within run-to-run noise on this machine: the expected saving is 0.5 to 14 µs on batches that cost 55 to 245 µs of CPU. In the last row the saving, 29 µs per batch, is under 1% of decoding 100 columns.

To run it:

SPARK_GENERATE_BENCHMARK_FILES=1 make benchmark-org.apache.spark.sql.benchmark.CometPlanOutputBenchmark

It runs on local[1] unless COMET_BENCHMARK_MASTER says otherwise.

Full benchmark output
1 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
1 bigint output columns, 1 column read:   Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                46             48           2        180.5           5.5       1.0X
cacheOutputSchema ON                                 47             49           2        179.1           5.6       1.0X
cacheOutputSchema OFF (repeat)                       46             48           2        181.9           5.5       1.0X
1 bigint output columns, 1 column read: process CPU per batch 68.0 us with the schema in every batch, 56.9 us with it cached, 11.1 us (16.4%) less
10 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
10 bigint output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                45             48           2        185.6           5.4       1.0X
cacheOutputSchema ON                                 45             48           3        184.9           5.4       1.0X
cacheOutputSchema OFF (repeat)                       49             52           2        170.8           5.9       0.9X
10 bigint output columns, 1 column read: process CPU per batch 76.1 us with the schema in every batch, 70.9 us with it cached, 5.2 us (6.8%) less
100 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
100 bigint output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               218            233           8         38.4          26.0       1.0X
cacheOutputSchema ON                                172            180           7         48.7          20.5       1.3X
cacheOutputSchema OFF (repeat)                      215            229           9         39.0          25.7       1.0X
100 bigint output columns, 1 column read: process CPU per batch 287.4 us with the schema in every batch, 239.3 us with it cached, 48.1 us (16.7%) less
1 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
1 string output columns, 1 column read:   Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               110            117           5         76.1          13.1       1.0X
cacheOutputSchema ON                                111            129          12         75.6          13.2       1.0X
cacheOutputSchema OFF (repeat)                      113            134          14         74.2          13.5       1.0X
1 string output columns, 1 column read: process CPU per batch 139.2 us with the schema in every batch, 145.4 us with it cached, -6.2 us (-4.4%) less
10 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
10 string output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               111            131          12         75.7          13.2       1.0X
cacheOutputSchema ON                                110            130          13         76.3          13.1       1.0X
cacheOutputSchema OFF (repeat)                      116            135          10         72.3          13.8       1.0X
10 string output columns, 1 column read: process CPU per batch 156.0 us with the schema in every batch, 176.7 us with it cached, -20.7 us (-13.3%) less
100 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
100 string output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               257            266           8         32.7          30.6       1.0X
cacheOutputSchema ON                                196            218          13         42.9          23.3       1.3X
cacheOutputSchema OFF (repeat)                      248            267           9         33.8          29.6       1.0X
100 string output columns, 1 column read: process CPU per batch 385.0 us with the schema in every batch, 336.5 us with it cached, 48.5 us (12.6%) less
1 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
1 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
-------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                            153            178          15         54.8          18.3       1.0X
cacheOutputSchema ON                                             159            184          13         52.8          19.0       1.0X
cacheOutputSchema OFF (repeat)                                   146            163          11         57.6          17.4       1.1X
1 struct<bigint,string> output columns, 1 column read: process CPU per batch 199.6 us with the schema in every batch, 210.8 us with it cached, -11.3 us (-5.6%) less
10 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
10 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
--------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                             152            181          17         55.1          18.1       1.0X
cacheOutputSchema ON                                              154            179          18         54.5          18.4       1.0X
cacheOutputSchema OFF (repeat)                                    151            175          16         55.6          18.0       1.0X
10 struct<bigint,string> output columns, 1 column read: process CPU per batch 235.1 us with the schema in every batch, 242.7 us with it cached, -7.7 us (-3.3%) less
100 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
100 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
---------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                              623            635          10         13.5          74.2       1.0X
cacheOutputSchema ON                                               494            500           6         17.0          58.9       1.3X
cacheOutputSchema OFF (repeat)                                     621            633          10         13.5          74.0       1.0X
100 struct<bigint,string> output columns, 1 column read: process CPU per batch 807.9 us with the schema in every batch, 666.6 us with it cached, 141.3 us (17.5%) less
100 bigint output columns, 100 columns read, cacheOutputSchema=false: 129 batches in 3 tasks, the schema sent with 129 of them
100 bigint output columns, 100 columns read, cacheOutputSchema=true: 129 batches in 3 tasks, the schema sent with 3 of them
OpenJDK 64-Bit Server VM 17.0.10+7-LTS on Mac OS X 26.6.2
Apple M3 Max
100 bigint output columns, 100 columns read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
---------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                  474            485           8          2.2         452.3       1.0X
cacheOutputSchema ON                                   475            485          11          2.2         452.9       1.0X
cacheOutputSchema OFF (repeat)                         473            491          11          2.2         450.7       1.0X
100 bigint output columns, 100 columns read: process CPU per batch 4165.4 us with the schema in every batch, 4136.6 us with it cached, 28.8 us (0.7%) less

@andygrove andygrove added enhancement New feature or request performance area:ffi Arrow FFI / JNI boundary labels Nov 2, 2026
executePlan exported an ArrowSchema for every column of every output batch,
and the JVM imported each one, although a plan's output schema rarely changes
between batches. Native now records the data type and field metadata of each
output column it last exported, and while a batch matches them it fills only
the ArrowArray structs. It sets bit 32 of the row count it returns when it
filled the schemas too. The JVM passes the same empty ArrowSchema structs with
every batch, and imports a batch that arrives without a schema with the fields
of the last one. A batch with a dictionary-encoded column always carries its
schema, because the JVM gives each dictionary it imports a new id.

The internal config spark.comet.exec.cacheOutputSchema.enabled, on by default,
makes native send the schema with every batch when it is off.
CometPlanOutputBenchmark compares spark.comet.exec.cacheOutputSchema.enabled
on and off for native plans with 1, 10 and 100 output columns of bigint,
string and struct<bigint,string>, and for one that reads 100 distinct columns.
Besides wall time, which leaves planning out, it reports each arm's process
CPU time per batch, since the JVM's import overlaps the native plan when that
runs on a Tokio worker.
@andygrove
andygrove force-pushed the cache-native-output-schema branch from 5fcdd4c to 566fc87 Compare November 2, 2026 18:30
@andygrove

Copy link
Copy Markdown
Member Author

CometPlanOutputBenchmark results on Linux x86 at 566fc87 (AMD Ryzen 9 7950X3D, Ubuntu 22.04, OpenJDK 17.0.20, local[1], make release build, pinned with taskset -c 0-7,16-23 to the V-Cache CCD). Wall time is the best of each case's runs. CPU is process CPU time per batch.

Output columns Off On Off (repeat) CPU per batch, off CPU per batch, on
1 × bigint 53 ms 51 ms 52 ms 68.3 µs 65.0 µs
10 × bigint 52 ms 52 ms 52 ms 78.0 µs 84.6 µs
100 × bigint 163 ms 120 ms 162 ms 227.6 µs 188.6 µs
1 × string 95 ms 95 ms 96 ms 104.1 µs 104.1 µs
10 × string 97 ms 95 ms 94 ms 117.1 µs 110.6 µs
100 × string 191 ms 151 ms 190 ms 289.4 µs 253.7 µs
1 × struct<bigint,string> 139 ms 139 ms 140 ms 149.6 µs 149.6 µs
10 × struct<bigint,string> 142 ms 143 ms 143 ms 201.6 µs 188.6 µs
100 × struct<bigint,string> 548 ms 422 ms 549 ms 708.9 µs 578.9 µs
100 distinct bigint, 100 columns read 690 ms 680 ms 682 ms 5710.6 µs 5658.9 µs

This matches the M3 Max results in the description. With 100 projected columns, wall time is 1.36x faster for bigint, 1.26x for string and 1.30x for struct<bigint,string>, and CPU per batch is 12 to 18% lower. The gains are a little larger than on macOS. With 1 or 10 output columns the settings are within noise (the 10 × bigint CPU sample went the other way by 6.5 µs), and the 100-distinct-column case differs by under 1%. The schema-count checks passed in every case: 1025 schemas with the cache off and 1 with it on, or 129 and 3 for the 3-task case.

Full results file
1 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
1 bigint output columns, 1 column read:   Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                53             55           1        158.1           6.3       1.0X
cacheOutputSchema ON                                 51             53           1        163.8           6.1       1.0X
cacheOutputSchema OFF (repeat)                       52             54           2        161.4           6.2       1.0X

1 bigint output columns, 1 column read: process CPU per batch 68.3 us with the schema in every batch, 65.0 us with it cached, 3.3 us (4.8%) less
10 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
10 bigint output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                52             53           1        160.6           6.2       1.0X
cacheOutputSchema ON                                 52             55           2        161.0           6.2       1.0X
cacheOutputSchema OFF (repeat)                       52             54           1        162.3           6.2       1.0X

10 bigint output columns, 1 column read: process CPU per batch 78.0 us with the schema in every batch, 84.6 us with it cached, -6.5 us (-8.3%) less
100 bigint output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 bigint output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
100 bigint output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               163            165           2         51.6          19.4       1.0X
cacheOutputSchema ON                                120            123           1         69.9          14.3       1.4X
cacheOutputSchema OFF (repeat)                      162            165           2         51.7          19.3       1.0X

100 bigint output columns, 1 column read: process CPU per batch 227.6 us with the schema in every batch, 188.6 us with it cached, 39.0 us (17.1%) less
1 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
1 string output columns, 1 column read:   Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                95             96           2         88.6          11.3       1.0X
cacheOutputSchema ON                                 95             98           1         88.1          11.4       1.0X
cacheOutputSchema OFF (repeat)                       96             98           1         87.5          11.4       1.0X

1 string output columns, 1 column read: process CPU per batch 104.1 us with the schema in every batch, 104.1 us with it cached, 0.0 us (0.0%) less
10 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
10 string output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                97             99           1         86.1          11.6       1.0X
cacheOutputSchema ON                                 95             99           1         88.1          11.4       1.0X
cacheOutputSchema OFF (repeat)                       94             96           1         89.4          11.2       1.0X

10 string output columns, 1 column read: process CPU per batch 117.1 us with the schema in every batch, 110.6 us with it cached, 6.5 us (5.6%) less
100 string output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 string output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
100 string output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                               191            194           3         43.9          22.8       1.0X
cacheOutputSchema ON                                151            153           2         55.4          18.1       1.3X
cacheOutputSchema OFF (repeat)                      190            193           4         44.2          22.6       1.0X

100 string output columns, 1 column read: process CPU per batch 289.4 us with the schema in every batch, 253.7 us with it cached, 35.8 us (12.4%) less
1 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
1 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
1 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
-------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                            139            142           1         60.2          16.6       1.0X
cacheOutputSchema ON                                             139            143           3         60.3          16.6       1.0X
cacheOutputSchema OFF (repeat)                                   140            142           1         59.8          16.7       1.0X

1 struct<bigint,string> output columns, 1 column read: process CPU per batch 149.6 us with the schema in every batch, 149.6 us with it cached, 0.0 us (0.0%) less
10 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
10 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
10 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
--------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                             142            145           1         59.0          17.0       1.0X
cacheOutputSchema ON                                              143            145           2         58.6          17.1       1.0X
cacheOutputSchema OFF (repeat)                                    143            146           2         58.6          17.1       1.0X

10 struct<bigint,string> output columns, 1 column read: process CPU per batch 201.6 us with the schema in every batch, 188.6 us with it cached, 13.0 us (6.5%) less
100 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=false: 1025 batches in 1 tasks, the schema sent with 1025 of them
100 struct<bigint,string> output columns, 1 column read, cacheOutputSchema=true: 1025 batches in 1 tasks, the schema sent with 1 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
100 struct<bigint,string> output columns, 1 column read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
---------------------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                              548            551           2         15.3          65.3       1.0X
cacheOutputSchema ON                                               422            425           1         19.9          50.3       1.3X
cacheOutputSchema OFF (repeat)                                     549            553           3         15.3          65.4       1.0X

100 struct<bigint,string> output columns, 1 column read: process CPU per batch 708.9 us with the schema in every batch, 578.9 us with it cached, 130.1 us (18.3%) less
100 bigint output columns, 100 columns read, cacheOutputSchema=false: 129 batches in 3 tasks, the schema sent with 129 of them
100 bigint output columns, 100 columns read, cacheOutputSchema=true: 129 batches in 3 tasks, the schema sent with 3 of them
OpenJDK 64-Bit Server VM 17.0.20.1+1-1-22.04-Ubuntu on Linux 6.8.0-138-generic
AMD Ryzen 9 7950X3D 16-Core Processor
100 bigint output columns, 100 columns read:  Best Time(ms)   Avg Time(ms)   Stdev(ms)    Rate(M/s)   Per Row(ns)   Relative
---------------------------------------------------------------------------------------------------------------------------
cacheOutputSchema OFF                                  690            692           3          1.5         657.9       1.0X
cacheOutputSchema ON                                   680            684           7          1.5         648.3       1.0X
cacheOutputSchema OFF (repeat)                         682            689           8          1.5         650.6       1.0X

100 bigint output columns, 100 columns read: process CPU per batch 5710.6 us with the schema in every batch, 5658.9 us with it cached, 51.7 us (0.9%) less

@andygrove
andygrove marked this pull request as ready for review November 2, 2026 18:58
@andygrove
andygrove requested a review from comphead November 2, 2026 18:59

@comphead comphead left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks, this reads cleanly. I checked it against arrow-rs 59.3 and Arrow Java 18.3. The cache compares exactly what move_to_spark builds the FFI_ArrowSchema from, and arrow-rs Field equality includes child names, nullability and metadata, so a nested change sends the schema again. Always sending it for dictionaries matches ArrayImporter.doImport, which clears the provider's dictionary vector on every import. I found no leak or double release on the error or end-of-output paths. I have not run the tests or the benchmark.

#6883 makes prepare_output pub(crate) and calls it from its new native/core/src/local.rs, and its LocalQueryIterator reads through getNextBatch. Whichever lands second has to pass None there, since getNextBatch does not handle OUTPUT_SCHEMA_EXPORTED.

Comment on lines +473 to +474
private def hasDictionary(field: Field): Boolean =
field.getDictionary != null || field.getChildren.asScala.exists(hasDictionary)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CometArrowPythonRunnerBase (under spark-4.x) has the same private hasDictionary(field: Field). Could this one be private[apache], or move to CometVectorUtils, so the Python runner reuses it instead of keeping a second copy?

Comment on lines +341 to +344
val batches = Seq(
nextBatch(Seq(1, 2), withSchema = true),
nextBatch(Seq(3), withSchema = false),
nextBatch(Seq(4, 5, 6), withSchema = false))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this sequence also send a batch of another type with its schema (a BigIntVector, for example) and then one more without a schema, and check that the last one reads back as longs? With the current tests, a getNextPlanBatch that kept only the first schema's fields would still pass: the schema arrives once here and in the dictionary test, and with the cache off the fields it keeps are never used. The native side of that sequence is covered by output_schema_cache_writes_the_schema_only_when_it_changes, but nothing on the JVM side reads a batch without a schema after a second schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ffi Arrow FFI / JNI boundary enhancement New feature or request performance

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants