Uploaded image for project: 'Qt for Python'
  1. Qt for Python
  2. PYSIDE-1697

Memory leak when invoking QModelIndex.data() from python with keyword arguments

    XMLWordPrintable

Details

    • Bug
    • Resolution: Done
    • Not Evaluated
    • 5.15.15
    • 5.15.2, 6.2.0
    • PySide
    • None
    • python 3.9.6 + PySide 6.2.0
      python 3.9.6 + PySide 5.15.2
    • Linux/X11
    • 3d9fa77b9fd556fdd87125aa33ce0a3bf8dda3f9 (pyside/pyside-setup/dev) 3860ee7aef2407bb0bbc01e69e7d381f2c481d5d (pyside/pyside-setup/6.2) 5f1459ac96ab97f85d1391b7d3ec424c782e5b52 (pyside/tqtc-pyside-setup/5.15)

    Description

      This is probably related to an earlier report I made (PYSIDE-1695). The TL;DR of this previous bug report was that, yes, calling index.data was indeed creating new objects, but that was an expected corollary of calling the virtual QAbstractListModel.data from cpp (through index.data) instead of directly calling Model.data from python. I closed it as "not a bug" because, although the new object creation is ugly, I understand if it is unavoidable because of the python -> cpp -> python roundtrip.

      Anyway, I fixed it on my end and plugged the major memory leak I was experiencing, only to discover there was another one that seemed closely related to the initial one. Indeed, these new objects created by the call to data are never discarded.

      To reproduce, try this code. Resize the window multiple times to trigger as many repaints as possible (that'll call index.data from python), while closely monitoring the memory footprint of the running process.

      import string
      import random
      from PySide6 import QtCore, QtWidgets
      
      
      class Model(QtCore.QAbstractListModel):
          def __init__(self, *args, **kwargs):
              super().__init__(*args, **kwargs)
              chars = string.ascii_uppercase + string.ascii_lowercase + string.digits
              self.dataset = [
                  ''.join(random.choices(chars, k=50))
                  for _ in range(1000)
              ]
      
          def rowCount(self, parent=QtCore.QModelIndex()):
              if not parent.isValid():
                  return len(self.dataset)
              return 0
      
          def data(self, index, role=QtCore.Qt.DisplayRole):
              if not index.isValid() or index.column() != 0:
                  return
      
              if role == QtCore.Qt.DisplayRole:
                  return self.dataset[index.row()]
      
      
      class Delegate(QtWidgets.QStyledItemDelegate):
          def paint(self, painter, options, index):
              text = index.data(role=QtCore.Qt.DisplayRole)
              painter.drawText(options.rect.bottomLeft(), text)
      
      
      class View(QtWidgets.QListView):
          def __init__(
              self,
              *args,
              **kwargs
          ):
              super().__init__(*args, **kwargs)
              self.setResizeMode(QtWidgets.QListView.Adjust)
              self.setWrapping(True)
              self.setViewMode(QtWidgets.QListView.IconMode)
              self.setUniformItemSizes(False)
      
      
      if __name__ == '__main__':
          app = QtWidgets.QApplication([])
      
          view = View()
          delegate = Delegate(parent=view)
          view.setItemDelegate(delegate)
      
          model = Model(parent=view)
          view.setModel(model)
      
          app.connect(QtCore.SIGNAL("aboutToQuit()"), view.destroy)
          view.show()
          app.exec()
      

      If I change this line in Delegate.paint():

      text = index.data(role=QtCore.Qt.DisplayRole)
      

      to this (i.e.: calling the python override directly without a cpp roundtrip):

      text = index.model().data(index, role=QtCore.Qt.DisplayRole)
      

      The memory leak completely disappears. This leads me to believe the new objects are never discarded. Also, it is worth noting that this behavior also happens with

      text = index.model().data(index, role=QtCore.Qt.DisplayRole)
      

      if the model is proxied through one of the Qt model proxies. i.e.:

      proxy = QtCore.QSortFilterProxyModel()
      proxy.setSourceModel(model)
      proxy.sort(0)
      view.setModel(proxy)
      

      so the fix isn't as simple as changing usage in my delegate because I cannot guarantee the model won't be used through a proxy like above.

      Finally I should mention that the problem gets worse with more intricate delegates. The one in the example only calls data once per repaint of each item and I can see the process's memory footprint jump in 1-2MB increments when I resize the window. I'm currently working on a delegate that calls data both in paint and in sizeHint (multiple times) and I can see the process's memory jump in 8MB increments when I resize the window.

      Attachments

        1. pyside1697_no_kwargs.py
          5 kB
          Friedemann Kleint
        2. pyside1697_old.py
          2 kB
          Friedemann Kleint
        3. pyside1697.py
          5 kB
          Friedemann Kleint
        4. Screenshot_20211027_211717.jpg
          316 kB
          Thomas Mc Kay
        No reviews matched the request. Check your Options in the drop-down menu of this sections header.

        Activity

          People

            kleint Friedemann Kleint
            thomas701 Thomas Mc Kay
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved:

              Gerrit Reviews

                There are no open Gerrit changes