| # | |
| # 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. | |
| # | |
| """ | |
| Microbenchmarks for ``LocalDataToArrowConversion.convert``, the hot path of | |
| Spark Connect ``createDataFrame`` and Arrow-optimized Python UDF output. | |
| ``string`` / ``binary`` columns are converted element by element: the per-element | |
| converter is fast when the element is already the target type (``str`` / | |
| ``bytes``) and slower when it needs coercion. Each benchmark sweeps the fraction | |
| of non-target ("other") values from 0% (all fast path) to 100% (all slow path), | |
| for a scalar column, a one-level ``array`` column, and a two-level | |
| ``array<array<...>>`` column (which drives the inlined array fast path hardest). | |
| """ | |
| def _build(convert, is_string, leaf, other_val, other_frac, n_rows): | |
| import random | |
| from pyspark.sql.types import ArrayType, BinaryType, StringType, StructField, StructType | |
| leaf_type = StringType() if is_string else BinaryType() | |
| def target(i): | |
| return f"s{i}" if is_string else b"s%d" % i | |
| rnd = random.Random(0) | |
| def elem(i): | |
| return other_val(i) if rnd.random() * 100 < other_frac else target(i) | |
| if leaf == "array2": | |
| schema = StructType([StructField("c", ArrayType(ArrayType(leaf_type)), True)]) | |
| data = [([[elem(i), elem(i + 1)], [elem(i + 2), elem(i + 3)]],) for i in range(n_rows)] | |
| elif leaf == "array": | |
| schema = StructType([StructField("c", ArrayType(leaf_type), True)]) | |
| data = [([elem(i), elem(i + 1), elem(i + 2)],) for i in range(n_rows)] | |
| else: # scalar | |
| schema = StructType([StructField("c", leaf_type, True)]) | |
| data = [(elem(i),) for i in range(n_rows)] | |
| return data, schema | |
| class LocalDataToArrowStringBenchmark: | |
| """ | |
| Benchmark ``convert`` on a ``string`` column, sweeping the kind and fraction | |
| of non-target values. | |
| - ``leaf``: ``scalar``, one-level ``array``, or two-level ``array2`` | |
| (``array<array<string>>``). | |
| - ``other``: the non-target values -- ``none`` (null), ``int`` or ``bool`` | |
| (a single non-str type, isolating one coercion branch), or ``mix`` (a | |
| rotation of int / float / bool / Decimal / date / datetime). | |
| - ``other_frac``: fraction of elements that are non-target, ``0`` to ``100``. | |
| """ | |
| params = [ | |
| [1000000], | |
| ["scalar", "array", "array2"], | |
| ["none", "int", "bool", "mix"], | |
| [0, 30, 70, 100], | |
| ] | |
| param_names = ["n_rows", "leaf", "other", "other_frac"] | |
| def setup(self, n_rows, leaf, other, other_frac): | |
| import datetime | |
| import decimal | |
| from pyspark.sql.conversion import LocalDataToArrowConversion | |
| self.convert = LocalDataToArrowConversion.convert | |
| if other == "none": | |
| def other_val(i): | |
| return None | |
| elif other == "int": | |
| def other_val(i): | |
| return i # int coerced to string via str() | |
| elif other == "bool": | |
| def other_val(i): | |
| return i % 2 == 0 # bool coerced to "true" / "false" | |
| else: # mix: a rotation of non-str types, each coerced to string | |
| _pool = [ | |
| 123, | |
| 4.5, | |
| True, | |
| False, | |
| decimal.Decimal("1.50"), | |
| datetime.date(2020, 1, 1), | |
| datetime.datetime(2020, 1, 1, 3, 4, 5), | |
| ] | |
| def other_val(i): | |
| return _pool[i % len(_pool)] | |
| self.data, self.schema = _build(self.convert, True, leaf, other_val, other_frac, n_rows) | |
| def time_convert(self, n_rows, leaf, other, other_frac): | |
| self.convert(self.data, self.schema, False) | |
| def peakmem_convert(self, n_rows, leaf, other, other_frac): | |
| self.convert(self.data, self.schema, False) | |
| class LocalDataToArrowBinaryBenchmark: | |
| """ | |
| Benchmark ``convert`` on a ``binary`` column, sweeping the fraction of | |
| non-target values. | |
| - ``leaf``: ``scalar``, one-level ``array``, or two-level ``array2`` | |
| (``array<array<binary>>``). | |
| - ``other``: ``none`` (null) or ``bytearray`` (the only non-bytes input | |
| binary accepts, copied to immutable bytes). | |
| - ``other_frac``: fraction of elements that are non-target, ``0`` to ``100``. | |
| """ | |
| params = [ | |
| [1000000], | |
| ["scalar", "array", "array2"], | |
| ["none", "bytearray"], | |
| [0, 30, 70, 100], | |
| ] | |
| param_names = ["n_rows", "leaf", "other", "other_frac"] | |
| def setup(self, n_rows, leaf, other, other_frac): | |
| from pyspark.sql.conversion import LocalDataToArrowConversion | |
| self.convert = LocalDataToArrowConversion.convert | |
| if other == "none": | |
| def other_val(i): | |
| return None | |
| else: # bytearray coerced to immutable bytes | |
| def other_val(i): | |
| return bytearray(b"s%d" % i) | |
| self.data, self.schema = _build(self.convert, False, leaf, other_val, other_frac, n_rows) | |
| def time_convert(self, n_rows, leaf, other, other_frac): | |
| self.convert(self.data, self.schema, False) | |
| def peakmem_convert(self, n_rows, leaf, other, other_frac): | |
| self.convert(self.data, self.schema, False) |