Illustrative Case Study (Good Case)

matplotlib__matplotlib-23476

DPI doubles after unpickling on M1 Mac — trace funnels localization into pickle/unpickle magic methods, enabling reranking to prioritize the correct serialization boundary.

← Back to Case Studies   |   Homepage

Outcome

Hit @1 (GT function)lib/matplotlib/figure.py:Figure.__getstate__
Core idea: the selected test does not assert DPI directly, but its execution trace exposes a must-pass serialization skeleton (__getstate__ / __setstate__), enabling accurate function-level reranking.

0) Metadata (Repro Anchors)

1) Input

Symptom. On M1 MacBooks, repeatedly pickling/unpickling a Matplotlib figure causes its DPI to double each time (200 → 400 → 800 → …), eventually triggering an OverflowError.

Issue excerpt (original, abridged)

The issue reports: “When a figure is unpickled, its dpi is doubled… if done in a loop it can cause an OverflowError.” The reproduction script repeatedly pickle.dump/pickle.load a figure and prints the DPI, showing exponential growth. The crash stack points to figure unpickling and backend manager creation on MacOSX.

[Bug]: DPI of a figure is doubled after unpickling on M1 Mac
...
0: 200.0
1: 400.0
2: 800.0
...
OverflowError: signed integer is greater than maximum

2) Step 1 — Relevant Test Retrieval (Two-Phase)

IssueExec localizes via issue → tests → traces → code. BM25 prioritizes recall; the LLM then selects a small, high-quality set Td.

BM25 Top-10 (lexical filtering)
lib/matplotlib/tests/test_axes.py::test_vert_violinplot_custompoints_200
lib/matplotlib/tests/test_axes.py::test_horiz_violinplot_custompoints_200
lib/matplotlib/tests/test_arrow_patches.py::test_fancyarrow_dpi_cor_200dpi
lib/matplotlib/tests/test_figure.py::test_change_dpi
lib/matplotlib/tests/test_figure.py::test_subfigure_dpi
lib/matplotlib/tests/test_figure.py::test_set_fig_size
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas
lib/matplotlib/tests/test_figure.py::TestSubplotMosaic::test_user_order
lib/matplotlib/tests/test_figure.py::TestSubplotMosaic::test_nested_user_order
lib/matplotlib/tests/test_png.py::test_truncated_file

Observation: BM25 retrieves many “dpi/figure” tests; the serialization-specific signal becomes decisive in semantic selection.

LLM-selected Td (semantic selection)
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas
lib/matplotlib/tests/test_figure.py::test_change_dpi
lib/matplotlib/tests/test_figure.py::test_subfigure_dpi
lib/matplotlib/tests/test_pickle.py::test_simple
lib/matplotlib/tests/test_pickle.py::test_complete

Why this intermediate layer matters: the issue mentions DPI, but the mechanism is tied to pickle/unpickle state transitions. Test retrieval anchors the search to executable serialization behavior, preventing drift toward speculative backend-only hypotheses.

3) Step 2 — Trace-Guided Analysis (Execution Causal Skeleton)

Key retrieved test (GT test).

def test_unpickle_canvas():
    fig = mfigure.Figure()
    assert fig.canvas is not None
    out = BytesIO()
    pickle.dump(fig, out)
    out.seek(0)
    fig2 = pickle.load(out)
    assert fig2.canvas is not None

Blind spot: this test checks canvas restoration, not DPI directly. Value: the trace still exposes must-pass pickle/unpickle boundaries (__getstate__/__setstate__).

Coverage / call graph (test → callees)
lib/matplotlib/tests/test_pickle.py::test_unpickle_canvas →
  • lib/matplotlib/_api/deprecation.py::wrapper
  • lib/matplotlib/artist.py::Rectangle.__getstate__
  • lib/matplotlib/cbook/__init__.py::CallbackRegistry.__getstate__
  • lib/matplotlib/cbook/__init__.py::CallbackRegistry.__setstate__
  • lib/matplotlib/figure.py::Figure.__getstate__
  • lib/matplotlib/figure.py::Figure.__setstate__
  • lib/matplotlib/transforms.py::Affine2D.__getstate__
  • lib/matplotlib/transforms.py::Affine2D.__setstate__
  • lib/matplotlib/transforms.py::Bbox.__getstate__
  • lib/matplotlib/transforms.py::Bbox.__setstate__
  • lib/matplotlib/transforms.py::BboxTransformTo.__getstate__
  • lib/matplotlib/transforms.py::BboxTransformTo.__setstate__
  • lib/matplotlib/transforms.py::TransformedBbox.__getstate__
  • lib/matplotlib/transforms.py::TransformedBbox.__setstate__

Suspicious locations (trace-guided). High/Critical candidates include Figure.__getstate__ and Figure.__setstate__, which are guaranteed to be on the unpickle path.

4) Step 3 — Contextual Refinement

IssueExec retrieves module structure and surrounding code context for files containing suspicious locations (here, primarily lib/matplotlib/figure.py). This enables reranking to distinguish root cause boundaries (state capture) from symptom-adjacent neighbors (state restore and backend interactions).

5) Step 4 — Final Reranking (Ranked Edit Locations)

Candidate contexts (abridged)

Location 1 (GT): Figure.__getstate__

def __getstate__(self):
    state = super().__getstate__()

    # The canvas cannot currently be pickled...
    state.pop("canvas")

    # Set cached renderer to None -- it can't be pickled.
    state["_cachedRenderer"] = None

    state['__mpl_version__'] = mpl.__version__

    from matplotlib import _pylab_helpers
    if self.canvas.manager in _pylab_helpers.Gcf.figs.values():
        state['_restore_to_pylab'] = True
    return state

Location 2 (neighbor): Figure.__setstate__

def __setstate__(self, state):
    version = state.pop('__mpl_version__')
    restore_to_pylab = state.pop('_restore_to_pylab', False)

    if version != mpl.__version__:
        _api.warn_external(...)

    self.__dict__ = state

    FigureCanvasBase(self)  # Set self.canvas.

    if restore_to_pylab:
        import matplotlib.pyplot as plt
        import matplotlib._pylab_helpers as pylab_helpers
        ...
        plt.draw_if_interactive()

    self.stale = True

Final ranked edit locations L* (Top-k).

  1. lib/matplotlib/figure.py:Figure.__getstate__ (GT)
  2. lib/matplotlib/figure.py:Figure.__setstate__

Rationale: although the stacktrace points to __setstate__, repeated DPI doubling suggests a state capture / serialization mismatch that compounds each pickle cycle. Reranking therefore prioritizes the serialization boundary __getstate__.

6) Baseline (SWE-agent) — Failure Summary

In short, SWE-agent never establishes the issue → test → code link, so it drifts between failed reproduction and speculative file inspection without reaching the serialization root cause.

7) Step 5 — Patch Generation

To connect localization improvements to end-to-end repair (RQ2), we feed IssueExec’s function-level edit locations into the downstream Agentless repair module. Rather than providing entire files, the repair stage operates on a pinpointed function-level target with minimal surrounding context required for editing.

Why this fixes “DPI doubles after unpickling”: on high-DPI backends, the figure’s runtime DPI may be temporarily scaled by device pixel ratio while the logical/original DPI is preserved in _original_dpi. If the scaled DPI is serialized, repeated pickle/unpickle will compound the scaling. The patch therefore corrects the serialization boundary in __getstate__ by restoring the logical DPI in the pickled state and removing _original_dpi.

Resolved patch
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -3023,6 +3023,15 @@ def __getstate__(self):
         # Set cached renderer to None -- it can't be pickled.
         state["_cachedRenderer"] = None

+        # High-DPI backends may temporarily scale the figure dpi by the device
+        # pixel ratio, keeping the logical/original dpi in _original_dpi.
+        # Do not pickle the scaled dpi (otherwise it will be scaled again on
+        # unpickle). Also drop _original_dpi from the pickled state.
+        _orig_dpi = state.pop("_original_dpi", None)
+        if _orig_dpi is not None:
+            state["_dpi"] = _orig_dpi
+
         # add version information to the state
         state['__mpl_version__'] = mpl.__version__

Outcome: this patch resolves the instance under the SWE-bench Lite evaluation setup. This supports our RQ2 claim: by pinpointing exact edit locations rather than providing entire files, the repair stage avoids being overwhelmed by extraneous code and focuses on the root-cause boundary.

8) Takeaway

This is a “trace-funneling” win: even when the retrieved test does not assert DPI, its execution trace exposes the correct low-entropy search space (serialization magic methods), enabling accurate function-level localization.