Skip to content

Conversation

@codeflash-ai
Copy link

@codeflash-ai codeflash-ai bot commented Dec 9, 2025

📄 432% (4.32x) speedup for FontProperties.copy in lib/matplotlib/font_manager.py

⏱️ Runtime : 232 microseconds 43.6 microseconds (best of 67 runs)

📝 Explanation and details

The optimized copy method replaces the generic copy.copy(self) call with a direct, manual instance creation approach. This provides a 431% speedup by avoiding the overhead of Python's copy module machinery.

Key optimization changes:

  • Direct instance creation: Uses cls.__new__(cls) instead of relying on copy.copy()
  • Manual attribute copying: Directly assigns result.__dict__ = self.__dict__.copy()
  • Eliminates module lookup overhead: Avoids the internal dispatch and type checking mechanisms in the copy module

Why this is faster:
The copy.copy() function performs type introspection, checks for special copy methods, and uses a general-purpose copying strategy. For simple objects like FontProperties that only contain basic attributes, this overhead is unnecessary. The optimized version bypasses all this machinery by directly creating a new instance and shallow-copying the attribute dictionary.

Performance characteristics from tests:

  • Consistent 4-5x speedup across all test scenarios (414% to 564% faster)
  • Scales well with object complexity: Even with 500 custom attributes, maintains 188% speedup
  • Memory efficient: The __dict__.copy() approach provides the same shallow copy semantics as the original

Workload impact:
Since FontProperties objects are likely created and copied frequently in matplotlib's text rendering pipeline, this optimization could provide meaningful performance improvements in applications that render lots of text with varying font properties, especially in plotting libraries where font copying might occur in tight loops during layout calculations.

The optimization preserves all original behavior including shallow copy semantics for mutable attributes like family lists.

Correctness verification report:

Test Status
⚙️ Existing Unit Tests 2 Passed
🌀 Generated Regression Tests 166 Passed
⏪ Replay Tests 🔘 None Found
🔎 Concolic Coverage Tests 🔘 None Found
📊 Tests Coverage 100.0%
⚙️ Existing Unit Tests and Runtime
🌀 Generated Regression Tests and Runtime
# Patch external dependencies

# imports
import pytest
from matplotlib.font_manager import FontProperties


class DummyApi:
    @staticmethod
    def check_in_list(valid, **kwargs):
        for k, v in kwargs.items():
            if v not in valid:
                raise ValueError(f"{k}={v} is invalid")


class DummyFontManager:
    @staticmethod
    def get_default_size():
        return 10.0


class DummyLog:
    @staticmethod
    def info(msg, *args):
        pass


class DummyMpl:
    rcParams = {
        "font.family": ["sans-serif"],
        "font.style": "normal",
        "font.variant": "normal",
        "font.weight": "normal",
        "font.stretch": "normal",
        "font.size": 10.0,
        "mathtext.fontset": "dejavusans",
    }


mpl = DummyMpl
_api = DummyApi

# font_scalings, stretch_dict, weight_dict, DummyFontManager, DummyLog, etc. from above
font_scalings = {
    "xx-small": 0.579,
    "x-small": 0.694,
    "small": 0.833,
    "medium": 1.0,
    "large": 1.200,
    "x-large": 1.440,
    "xx-large": 1.728,
    "larger": 1.2,
    "smaller": 0.833,
    None: 1.0,
}
stretch_dict = {
    "ultra-condensed": 100,
    "extra-condensed": 200,
    "condensed": 300,
    "semi-condensed": 400,
    "normal": 500,
    "semi-expanded": 600,
    "semi-extended": 600,
    "expanded": 700,
    "extended": 700,
    "extra-expanded": 800,
    "extra-extended": 800,
    "ultra-expanded": 900,
    "ultra-extended": 900,
}
weight_dict = {
    "ultralight": 100,
    "light": 200,
    "normal": 400,
    "regular": 400,
    "book": 400,
    "medium": 500,
    "roman": 500,
    "semibold": 600,
    "demibold": 600,
    "demi": 600,
    "bold": 700,
    "heavy": 800,
    "extra bold": 800,
    "black": 900,
}

# ------------------- UNIT TESTS -------------------

# 1. Basic Test Cases


def test_basic_copy_simple():
    # Test copying with only defaults
    fp1 = FontProperties()
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 8.33μs -> 1.44μs (478% faster)


def test_basic_copy_with_all_params():
    # Test copying with all parameters set
    fp1 = FontProperties(
        family=["Arial", "serif"],
        style="italic",
        variant="small-caps",
        weight="bold",
        stretch="expanded",
        size=18,
        fname="/tmp/font.ttf",
        math_fontfamily="cm",
    )
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.97μs -> 1.43μs (456% faster)


def test_basic_copy_mutation_independence():
    # Changing the copy does not affect the original
    fp1 = FontProperties(family=["Arial"], style="normal", size=12)
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.92μs -> 1.43μs (454% faster)
    fp2.set_family(["Comic Sans"])
    fp2.set_size(20)


def test_basic_copy_list_family_deepcopy():
    # Changing the list in the copy does not affect the original
    fp1 = FontProperties(family=["Arial", "Times"])
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.93μs -> 1.43μs (456% faster)
    fp2.get_family().append("Comic Sans")


# 2. Edge Test Cases


def test_edge_copy_none_and_empty_values():
    # Test copying when some fields are None or empty
    fp1 = FontProperties(
        family=[],
        style="normal",
        variant="normal",
        weight=None,
        stretch=None,
        size=None,
        fname=None,
        math_fontfamily=None,
    )
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.92μs -> 1.38μs (472% faster)


def test_edge_copy_with_int_weight_and_stretch():
    # Test copying with integer weight and stretch
    fp1 = FontProperties(weight=700, stretch=300)
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 8.00μs -> 1.45μs (454% faster)


def test_edge_copy_with_float_and_string_size():
    # Test copying with float and string size
    fp1 = FontProperties(size="large")
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.88μs -> 1.44μs (446% faster)
    # Should be scaled appropriately
    expected_size = font_scalings["large"] * DummyFontManager.get_default_size()


def test_edge_copy_with_invalid_weight_raises():
    # Test that invalid weight raises on creation, not on copy
    with pytest.raises(ValueError):
        FontProperties(weight="not_a_weight")


def test_edge_copy_with_invalid_stretch_raises():
    # Test that invalid stretch raises on creation, not on copy
    with pytest.raises(ValueError):
        FontProperties(stretch="not_a_stretch")


def test_edge_copy_with_invalid_size_raises():
    # Test that invalid size raises on creation, not on copy
    with pytest.raises(ValueError):
        FontProperties(size="not_a_size")


def test_edge_copy_with_invalid_math_fontfamily_raises():
    # Test that invalid math_fontfamily raises on creation, not on copy
    with pytest.raises(ValueError):
        FontProperties(math_fontfamily="not_a_font")


def test_edge_copy_with_file_none_and_string():
    # Test copying with None and string file
    fp1 = FontProperties(fname=None)
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 8.10μs -> 1.44μs (464% faster)
    fp3 = FontProperties(fname="/tmp/font.ttf")
    codeflash_output = fp3.copy()
    fp4 = codeflash_output  # 2.90μs -> 436ns (564% faster)


def test_edge_copy_family_as_string():
    # Test copying when family is a string
    fp1 = FontProperties(family="Comic Sans")
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.91μs -> 1.35μs (487% faster)


def test_edge_copy_size_below_one():
    # Test size below one is set to 1.0
    fp1 = FontProperties(size=0.5)
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 8.17μs -> 1.25μs (556% faster)


# 3. Large Scale Test Cases


def test_large_scale_copy_many_fonts():
    # Test copying with a large family list
    families = [f"Font{i}" for i in range(500)]
    fp1 = FontProperties(
        family=families,
        style="oblique",
        variant="small-caps",
        weight="bold",
        stretch="expanded",
        size=24,
        fname="/tmp/font.ttf",
        math_fontfamily="stix",
    )
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 8.01μs -> 1.33μs (501% faster)
    # Mutate copy's family, original unaffected
    fp2.get_family().append("ExtraFont")


def test_large_scale_copy_performance():
    # Test that copy works efficiently for large objects
    families = [f"Font{i}" for i in range(999)]
    fp1 = FontProperties(
        family=families,
        style="normal",
        variant="normal",
        weight="normal",
        stretch="normal",
        size=10,
        fname=None,
        math_fontfamily="dejavusans",
    )
    codeflash_output = fp1.copy()
    fp2 = codeflash_output  # 7.78μs -> 1.32μs (490% faster)
    # Check that copy is fast (should not take long)
    import time

    start = time.time()
    codeflash_output = fp1.copy()
    fp3 = codeflash_output  # 2.80μs -> 497ns (464% faster)
    duration = time.time() - start


def test_large_scale_copy_multiple_instances():
    # Test copying many instances in a loop
    fps = [
        FontProperties(
            family=[f"Font{i}"],
            style="normal",
            variant="normal",
            weight="normal",
            stretch="normal",
            size=10 + i,
            fname=None,
            math_fontfamily="dejavusans",
        )
        for i in range(100)
    ]
    copies = [fp.copy() for fp in fps]
    for i in range(100):
        # Check independence
        copies[i].set_family([f"FontCopy{i}"])


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.
# imports
import pytest
from matplotlib.font_manager import FontProperties

# function to test (FontProperties class and FontProperties.copy are defined above)

# -------------------- BASIC TEST CASES --------------------


def test_copy_returns_new_instance():
    """Test that copy returns a new instance with the same attributes."""
    fp = FontProperties(
        family="serif",
        style="italic",
        variant="normal",
        weight="bold",
        stretch="condensed",
        size=14.0,
        fname="/path/to/font.ttf",
        math_fontfamily="dejavusans",
    )
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.98μs -> 1.47μs (444% faster)
    # Changing the copy does not affect the original
    fp2.set_family("monospace")


def test_copy_preserves_all_fields():
    """Test that all fields are preserved after copying."""
    fp = FontProperties(
        family=["fantasy", "serif"],
        style="oblique",
        variant="small-caps",
        weight=500,
        stretch=400,
        size=18.5,
        fname="/tmp/font.otf",
        math_fontfamily="cm",
    )
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.47μs -> 1.36μs (451% faster)
    for attr in [
        "_family",
        "_slant",
        "_variant",
        "_weight",
        "_stretch",
        "_size",
        "_file",
        "_math_fontfamily",
    ]:
        pass


def test_copy_with_default_constructor():
    """Test copy on a FontProperties object created with all defaults."""
    fp = FontProperties()
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.45μs -> 1.45μs (414% faster)


def test_copy_with_fontconfig_pattern():
    """Test copy when FontProperties is constructed with a fontconfig pattern."""
    pattern = "DejaVu Sans:bold:italic"
    fp = FontProperties(pattern)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.85μs -> 1.33μs (488% faster)


# -------------------- EDGE TEST CASES --------------------


def test_copy_with_mutable_family_list():
    """Test that mutating the family list in the copy does not affect the original."""
    fp = FontProperties(family=["serif", "cursive"])
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.47μs -> 1.43μs (424% faster)
    fp2._family.append("fantasy")


def test_copy_with_none_fields():
    """Test copy when some fields are explicitly set to None."""
    fp = FontProperties(
        family=None,
        style=None,
        variant=None,
        weight=None,
        stretch=None,
        size=None,
        fname=None,
        math_fontfamily=None,
    )
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.40μs -> 1.39μs (433% faster)


def test_copy_with_numeric_weight_and_stretch():
    """Test copy when weight and stretch are numeric values."""
    fp = FontProperties(weight=123, stretch=456)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.45μs -> 1.38μs (441% faster)


def test_copy_with_float_size():
    """Test copy when size is a float."""
    fp = FontProperties(size=9.75)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.45μs -> 1.40μs (433% faster)


def test_copy_with_file_path():
    """Test copy with a font file path."""
    fp = FontProperties(fname="/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf")
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.35μs -> 1.42μs (418% faster)


def test_copy_with_invalid_math_fontfamily_raises():
    """Test that copy does not validate fields and does not raise for invalid math_fontfamily."""
    # The constructor will raise, but copy should not
    with pytest.raises(ValueError):
        FontProperties(math_fontfamily="notafont")
    # But if the object already exists, copy() should not raise
    fp = FontProperties()
    fp._math_fontfamily = "notafont"
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.13μs -> 1.46μs (387% faster)


def test_copy_is_shallow_for_nested_objects():
    """Test that copy is shallow, not deep, for nested objects."""

    class Dummy:
        def __init__(self, x):
            self.x = x

    fp = FontProperties()
    fp._family = [Dummy(1)]
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.53μs -> 1.44μs (425% faster)


# -------------------- LARGE SCALE TEST CASES --------------------


def test_copy_large_family_list():
    """Test copy with a large family list."""
    families = [f"font{i}" for i in range(500)]
    fp = FontProperties(family=families)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.59μs -> 1.41μs (440% faster)
    # Mutate copy, original remains unchanged
    fp2._family.append("extra_font")


def test_copy_performance_large_number_of_fields():
    """Test copy performance with many attributes (simulate by adding many attributes)."""
    fp = FontProperties()
    # Add many custom attributes to the instance
    for i in range(500):
        setattr(fp, f"custom_attr_{i}", i)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 9.90μs -> 3.44μs (188% faster)
    for i in range(500):
        pass
    # Changing a custom attribute in copy does not affect original
    fp2.custom_attr_0 = 999


# -------------------- FUNCTIONALITY MUTATION TESTS --------------------


def test_copy_is_not_deepcopy():
    """Test that copy is not a deepcopy (i.e., shallow copy)."""
    fp = FontProperties(family=[["serif", "sans-serif"]])
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.71μs -> 1.46μs (429% faster)


def test_copy_does_not_affect_original_after_mutation():
    """Test that mutating attributes in the copy does not affect the original."""
    fp = FontProperties(family=["serif"], size=10)
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.43μs -> 1.46μs (410% faster)
    fp2._size = 20


def test_copy_preserves_equality_semantics():
    """Test that a copy is equal to the original (by __eq__)."""
    fp = FontProperties(
        family=["serif"],
        style="normal",
        variant="normal",
        weight="normal",
        stretch="normal",
        size=12,
    )
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.52μs -> 1.34μs (459% faster)


def test_copy_preserves_hash_semantics():
    """Test that a copy has the same hash as the original."""
    fp = FontProperties(
        family=["serif"],
        style="normal",
        variant="normal",
        weight="normal",
        stretch="normal",
        size=12,
    )
    codeflash_output = fp.copy()
    fp2 = codeflash_output  # 7.45μs -> 1.35μs (449% faster)


# codeflash_output is used to check that the output of the original code is the same as that of the optimized code.

To edit these changes git checkout codeflash/optimize-FontProperties.copy-miy8owsq and push.

Codeflash Static Badge

The optimized `copy` method replaces the generic `copy.copy(self)` call with a direct, manual instance creation approach. This provides a **431% speedup** by avoiding the overhead of Python's copy module machinery.

**Key optimization changes:**
- **Direct instance creation**: Uses `cls.__new__(cls)` instead of relying on `copy.copy()`
- **Manual attribute copying**: Directly assigns `result.__dict__ = self.__dict__.copy()` 
- **Eliminates module lookup overhead**: Avoids the internal dispatch and type checking mechanisms in the copy module

**Why this is faster:**
The `copy.copy()` function performs type introspection, checks for special copy methods, and uses a general-purpose copying strategy. For simple objects like `FontProperties` that only contain basic attributes, this overhead is unnecessary. The optimized version bypasses all this machinery by directly creating a new instance and shallow-copying the attribute dictionary.

**Performance characteristics from tests:**
- **Consistent 4-5x speedup** across all test scenarios (414% to 564% faster)
- **Scales well with object complexity**: Even with 500 custom attributes, maintains 188% speedup
- **Memory efficient**: The `__dict__.copy()` approach provides the same shallow copy semantics as the original

**Workload impact:**
Since `FontProperties` objects are likely created and copied frequently in matplotlib's text rendering pipeline, this optimization could provide meaningful performance improvements in applications that render lots of text with varying font properties, especially in plotting libraries where font copying might occur in tight loops during layout calculations.

The optimization preserves all original behavior including shallow copy semantics for mutable attributes like family lists.
@codeflash-ai codeflash-ai bot requested a review from mashraf-222 December 9, 2025 07:09
@codeflash-ai codeflash-ai bot added ⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash labels Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

⚡️ codeflash Optimization PR opened by Codeflash AI 🎯 Quality: High Optimization Quality according to Codeflash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant