This is an automated email from the ASF dual-hosted git repository.

Jefffrey pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/main by this push:
     new ab25025904 perf(arrow-cast): improve custom temporal formatting 
performance in ArrayFormatter (#10594)
ab25025904 is described below

commit ab250259045e94d90b557f8d81ff13b36b55c0d7
Author: linfeng <[email protected]>
AuthorDate: Mon Aug 10 08:04:23 2026 +0800

    perf(arrow-cast): improve custom temporal formatting performance in 
ArrayFormatter (#10594)
    
    # Which issue does this PR close?
    
    <!--
    We generally require a GitHub issue to be filed for all bug fixes and
    enhancements and this helps us generate change logs for our releases.
    You can link an issue to this PR using the GitHub syntax.
    -->
    
    - Closes #NNN.
    
    # Rationale for this change
    
    <!--
    Why are you proposing this change? If this is already explained clearly
    in the issue then this section is not needed.
    Explaining clearly why changes are proposed helps reviewers understand
    your changes and offer better suggestions for fixes.
    -->
    
    ArrayFormatter currently reparses custom Chrono format strings for every
    temporal value.
    
    As an ArrayFormatter is typically created once and reused for an array,
    the parsed format items can instead be prepared once when constructing
    the formatter and reused for every value.
    
    # What changes are included in this PR?
    
    <!--
    There is no need to duplicate the description in the issue here but it
    is sometimes worth providing a summary of the individual changes in this
    PR.
    -->
    
    - Precompile custom temporal format strings when constructing an
    ArrayFormatter
    - Reuse the compiled format items for date, time, and timestamp values
    - Add benchmarks covering default and custom temporal formats
    
    # Are these changes tested?
    
    <!--
    We typically require tests for all PRs in order to:
    1. Prevent the code from being accidentally broken by subsequent changes
    2. Serve as another way to document the expected behavior of the code
    
    If tests are not included in your PR, please explain why (for example,
    are they covered by existing tests)?
    
    If this PR claims a performance improvement, please include evidence
    such as benchmark results.
    -->
    
    Yes.
    
    # Are there any user-facing changes?
    
    <!--
    If there are user-facing changes then we may require documentation to be
    updated before approving the PR.
    
    If there are any breaking changes to public APIs, please call them out.
    -->
    
    No. There are no public API or behavior changes.
    
    # Benchmarks
    ```
    group                                       main                            
        optimized
    -----                                       ----                            
        ---------
    format_temporal/date64/custom_long          1.79  1559.4±47.26µs        ? 
?/sec     1.00   872.7±32.05µs        ? ?/sec
    format_temporal/date64/custom_short         1.68   797.6±39.91µs        ? 
?/sec     1.00   473.7±15.21µs        ? ?/sec
    format_temporal/date64/default              1.02   464.7±12.82µs        ? 
?/sec     1.00    454.9±8.67µs        ? ?/sec
    format_temporal/timestamp/custom_long       1.77  1635.8±175.75µs       ? 
?/sec     1.00   925.6±29.74µs        ? ?/sec
    format_temporal/timestamp_tz/custom_long    1.54  1939.6±42.35µs        ? 
?/sec     1.00  1259.3±41.78µs        ? ?/sec
    ```
---
 arrow-cast/Cargo.toml                 |  4 ++
 arrow-cast/benches/format_temporal.rs | 80 +++++++++++++++++++++++++++++++++
 arrow-cast/src/display.rs             | 85 +++++++++++++++++++++++++++++------
 3 files changed, 156 insertions(+), 13 deletions(-)

diff --git a/arrow-cast/Cargo.toml b/arrow-cast/Cargo.toml
index d3257f5894..32248f0ac3 100644
--- a/arrow-cast/Cargo.toml
+++ b/arrow-cast/Cargo.toml
@@ -77,5 +77,9 @@ harness = false
 name = "parse_decimal"
 harness = false
 
+[[bench]]
+name = "format_temporal"
+harness = false
+
 [lints]
 workspace = true
diff --git a/arrow-cast/benches/format_temporal.rs 
b/arrow-cast/benches/format_temporal.rs
new file mode 100644
index 0000000000..3b9d4b0367
--- /dev/null
+++ b/arrow-cast/benches/format_temporal.rs
@@ -0,0 +1,80 @@
+// 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.
+
+use std::hint::black_box;
+
+use arrow_array::{Array, Date64Array, TimestampNanosecondArray};
+use arrow_cast::display::{ArrayFormatter, FormatOptions};
+use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
+
+const ARRAY_LEN: usize = 8192;
+const SHORT_FORMAT: &str = "%Y-%m-%d";
+const LONG_FORMAT: &str = "%Y-%m-%d %H:%M:%S%.9f";
+
+fn format_array(c: &mut Criterion) {
+    let date64 = Date64Array::from_value(1_754_668_645_123, ARRAY_LEN);
+    let timestamp = 
TimestampNanosecondArray::from_value(1_754_668_645_123_456_789, ARRAY_LEN);
+    let timestamp_tz = timestamp.clone().with_timezone("+08:00");
+
+    let mut group = c.benchmark_group("format_temporal");
+
+    for (id, array, options) in [
+        (
+            BenchmarkId::new("date64", "default"),
+            &date64 as &dyn Array,
+            FormatOptions::new(),
+        ),
+        (
+            BenchmarkId::new("date64", "custom_short"),
+            &date64 as &dyn Array,
+            FormatOptions::new().with_datetime_format(Some(SHORT_FORMAT)),
+        ),
+        (
+            BenchmarkId::new("date64", "custom_long"),
+            &date64 as &dyn Array,
+            FormatOptions::new().with_datetime_format(Some(LONG_FORMAT)),
+        ),
+        (
+            BenchmarkId::new("timestamp", "custom_long"),
+            &timestamp as &dyn Array,
+            FormatOptions::new().with_timestamp_format(Some(LONG_FORMAT)),
+        ),
+        (
+            BenchmarkId::new("timestamp_tz", "custom_long"),
+            &timestamp_tz as &dyn Array,
+            FormatOptions::new().with_timestamp_tz_format(Some(LONG_FORMAT)),
+        ),
+    ] {
+        let formatter = ArrayFormatter::try_new(array, &options).unwrap();
+        let mut output = String::with_capacity(32);
+
+        group.bench_function(id, |b| {
+            b.iter(|| {
+                for idx in 0..array.len() {
+                    output.clear();
+                    formatter.value(idx).write(&mut output).unwrap();
+                }
+                black_box(&output);
+            })
+        });
+    }
+
+    group.finish();
+}
+
+criterion_group!(benches, format_array);
+criterion_main!(benches);
diff --git a/arrow-cast/src/display.rs b/arrow-cast/src/display.rs
index 5705a455c9..64ba6b7d81 100644
--- a/arrow-cast/src/display.rs
+++ b/arrow-cast/src/display.rs
@@ -34,11 +34,30 @@ use arrow_array::types::*;
 use arrow_array::*;
 use arrow_buffer::ArrowNativeType;
 use arrow_schema::*;
+use chrono::format::{Item, StrftimeItems};
 use chrono::{NaiveDate, NaiveDateTime, SecondsFormat, TimeZone, Utc};
 use lexical_core::FormattedSize;
 
 type TimeFormat<'a> = Option<&'a str>;
 
+struct CompiledItems<'a>(Vec<Item<'a>>);
+
+enum CompiledTimeFormat<'a> {
+    Default,
+    Custom(Box<CompiledItems<'a>>),
+}
+
+impl<'a> CompiledTimeFormat<'a> {
+    fn new(format: TimeFormat<'a>) -> Self {
+        match format {
+            Some(format) => Self::Custom(Box::new(CompiledItems(
+                StrftimeItems::new(format).collect(),
+            ))),
+            None => Self::Default,
+        }
+    }
+}
+
 /// Format for displaying durations
 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
 #[non_exhaustive]
@@ -737,19 +756,25 @@ fn write_timestamp(
     f: &mut dyn Write,
     naive: NaiveDateTime,
     timezone: Option<Tz>,
-    format: Option<&str>,
+    format: &CompiledTimeFormat<'_>,
 ) -> FormatResult {
     match timezone {
         Some(tz) => {
             let date = Utc.from_utc_datetime(&naive).with_timezone(&tz);
             match format {
-                Some(s) => write!(f, "{}", date.format(s))?,
-                None => write!(f, "{}", 
date.to_rfc3339_opts(SecondsFormat::AutoSi, true))?,
+                CompiledTimeFormat::Custom(items) => {
+                    write!(f, "{}", date.format_with_items(items.0.iter()))?
+                }
+                CompiledTimeFormat::Default => {
+                    write!(f, "{}", 
date.to_rfc3339_opts(SecondsFormat::AutoSi, true))?
+                }
             }
         }
         None => match format {
-            Some(s) => write!(f, "{}", naive.format(s))?,
-            None => write!(f, "{naive:?}")?,
+            CompiledTimeFormat::Custom(items) => {
+                write!(f, "{}", naive.format_with_items(items.0.iter()))?
+            }
+            CompiledTimeFormat::Default => write!(f, "{naive:?}")?,
         },
     }
     Ok(())
@@ -758,12 +783,12 @@ fn write_timestamp(
 macro_rules! timestamp_display {
     ($($t:ty),+) => {
         $(impl<'a> DisplayIndexState<'a> for &'a PrimitiveArray<$t> {
-            type State = (Option<Tz>, TimeFormat<'a>);
+            type State = (Option<Tz>, CompiledTimeFormat<'a>);
 
             fn prepare(&self, options: &FormatOptions<'a>) -> 
Result<Self::State, ArrowError> {
                 match self.data_type() {
-                    DataType::Timestamp(_, Some(tz)) => Ok((Some(tz.parse()?), 
options.timestamp_tz_format)),
-                    DataType::Timestamp(_, None) => Ok((None, 
options.timestamp_format)),
+                    DataType::Timestamp(_, Some(tz)) => Ok((Some(tz.parse()?), 
CompiledTimeFormat::new(options.timestamp_tz_format))),
+                    DataType::Timestamp(_, None) => Ok((None, 
CompiledTimeFormat::new(options.timestamp_format))),
                     _ => unreachable!(),
                 }
             }
@@ -778,7 +803,7 @@ macro_rules! timestamp_display {
                     ))
                 })?;
 
-                write_timestamp(f, naive, s.0, s.1.clone())
+                write_timestamp(f, naive, s.0, &s.1)
             }
         })+
     };
@@ -794,10 +819,10 @@ timestamp_display!(
 macro_rules! temporal_display {
     ($convert:ident, $format:ident, $t:ty) => {
         impl<'a> DisplayIndexState<'a> for &'a PrimitiveArray<$t> {
-            type State = TimeFormat<'a>;
+            type State = CompiledTimeFormat<'a>;
 
             fn prepare(&self, options: &FormatOptions<'a>) -> 
Result<Self::State, ArrowError> {
-                Ok(options.$format)
+                Ok(CompiledTimeFormat::new(options.$format))
             }
 
             fn write(&self, fmt: &Self::State, idx: usize, f: &mut dyn Write) 
-> FormatResult {
@@ -811,8 +836,10 @@ macro_rules! temporal_display {
                 })?;
 
                 match fmt {
-                    Some(s) => write!(f, "{}", naive.format(s))?,
-                    None => write!(f, "{naive:?}")?,
+                    CompiledTimeFormat::Custom(items) => {
+                        write!(f, "{}", 
naive.format_with_items(items.0.iter()))?
+                    }
+                    CompiledTimeFormat::Default => write!(f, "{naive:?}")?,
                 }
                 Ok(())
             }
@@ -1456,6 +1483,38 @@ mod tests {
         (0..array.len()).map(|x| fmt.value(x).to_string()).collect()
     }
 
+    #[test]
+    fn test_temporal_custom_format() {
+        let options = FormatOptions::new()
+            .with_date_format(Some("%Y-%m-%d"))
+            .with_datetime_format(Some("%Y-%m-%d %H:%M:%S"))
+            .with_time_format(Some("%H:%M:%S"))
+            .with_timestamp_format(Some("%Y-%m-%d %H:%M:%S"))
+            .with_timestamp_tz_format(Some("%Y-%m-%d %H:%M:%S %:z"));
+
+        let date32 = Date32Array::from(vec![0]);
+        assert_eq!(format_array(&date32, &options), ["1970-01-01"]);
+
+        let date64 = Date64Array::from(vec![0]);
+        assert_eq!(format_array(&date64, &options), ["1970-01-01 00:00:00"]);
+
+        let time = Time32SecondArray::from(vec![3661]);
+        assert_eq!(format_array(&time, &options), ["01:01:01"]);
+
+        let timestamp = TimestampSecondArray::from(vec![0]);
+        assert_eq!(format_array(&timestamp, &options), ["1970-01-01 
00:00:00"]);
+
+        let timestamp_tz = 
TimestampSecondArray::from(vec![0]).with_timezone("+08:00");
+        assert_eq!(
+            format_array(&timestamp_tz, &options),
+            ["1970-01-01 08:00:00 +08:00"]
+        );
+
+        let invalid_options = 
FormatOptions::new().with_datetime_format(Some("%"));
+        let formatter = ArrayFormatter::try_new(&date64, 
&invalid_options).unwrap();
+        assert!(formatter.value(0).try_to_string().is_err());
+    }
+
     #[test]
     fn test_array_value_to_string_duration() {
         let iso_fmt = FormatOptions::new();

Reply via email to