Skip to content

Commit

Permalink
Redesign Commit viewer to scale better with large databases
Browse files Browse the repository at this point in the history
- Affected items are now listed in tables and the tables are tabbed
  by item type. The previous tree view could not really handle
  large number of items usability or performance wise.
- Affected items are now fetched in a worker thread to keep the
  window responsive.
- Parameter values are now shown like in the Parameter value table
  in DB editor proper instead of showing the JSON representation.
  This seems to give a huge performance boost: apparently Qt is not
  great when it needs to render large amounts of text in a tiny
  table cell.

Re #26062
  • Loading branch information
soininen committed Apr 18, 2024
1 parent 80a0360 commit eec1d8c
Show file tree
Hide file tree
Showing 14 changed files with 813 additions and 299 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Many parts of the Spine data structure have been redesigned.
- You can now select a different Julia executable & project or Julia kernel for each Tool spec.
This overrides the global setting from Toolbox Settings.
- Headless mode now supports remote execution (see 'python -m spinetoolbox --help')
- Commit Viewer's UI has undergone some redesigning and can now handle large databases.

### Deprecated

Expand Down
17 changes: 9 additions & 8 deletions benchmarks/db_mngr_get_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import os
import sys

from spinedb_api.db_mapping_base import PublicItem

if sys.platform == "win32" and "HOMEPATH" not in os.environ:
import pathlib
os.environ["HOMEPATH"] = str(pathlib.Path(sys.executable).parent)
Expand All @@ -15,18 +17,17 @@
from PySide6.QtWidgets import QApplication
from spinetoolbox.spine_db_manager import SpineDBManager
from spinedb_api import DatabaseMapping, to_database
from spinedb_api.temp_id import TempId
from benchmarks.utils import StdOutLogger


def db_mngr_get_value(
loops: int, db_mngr: SpineDBManager, db_map: DatabaseMapping, ids: Sequence[TempId], role: Qt.ItemDataRole
loops: int, db_mngr: SpineDBManager, db_map: DatabaseMapping, items: Sequence[PublicItem], role: Qt.ItemDataRole
) -> float:
duration = 0.0
for _ in range(loops):
for id_ in ids:
for item in items:
start = time.perf_counter()
db_mngr.get_value(db_map, "parameter_value", id_, role)
db_mngr.get_value(db_map, item, role)
duration += time.perf_counter() - start
return duration

Expand All @@ -40,7 +41,7 @@ def run_benchmark(output_file: Optional[str]):
db_map.add_entity_class_item(name="Object")
db_map.add_parameter_definition_item(name="x", entity_class_name="Object")
db_map.add_entity_item(name="object", entity_class_name="Object")
value_ids = []
value_items = []
for i in range(100):
alternative_name = str(i)
db_map.add_alternative_item(name=str(i))
Expand All @@ -54,16 +55,16 @@ def run_benchmark(output_file: Optional[str]):
type=value_type,
)
assert error is None
value_ids.append(item["id"])
value_items.append(item)
runner = pyperf.Runner()
benchmark = runner.bench_time_func(
"SpineDatabaseManager.get_value[parameter_value, DisplayRole]",
db_mngr_get_value,
db_mngr,
db_map,
value_ids,
value_items,
Qt.ItemDataRole.DisplayRole,
inner_loops=len(value_ids),
inner_loops=len(value_items),
)
if output_file:
pyperf.add_runs(output_file, benchmark)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ def data(self, column, role=Qt.ItemDataRole.DisplayRole):
if role == Qt.ItemDataRole.DisplayRole and not self.id:
return "Enter new list value here..."
if role in (Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole, Qt.ItemDataRole.ToolTipRole, PARSED_ROLE):
return self.db_mngr.get_value(self.db_map, self.item_type, self.id, role=role)
item = self.db_mngr.get_item(self.db_map, self.item_type, self.id)
return self.db_mngr.get_value(self.db_map, item, role=role)
return super().data(column, role)

def list_index(self):
Expand Down
3 changes: 2 additions & 1 deletion spinetoolbox/spine_db_editor/mvcmodels/pivot_table_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1270,7 +1270,8 @@ def _data(self, index, role):
if data[0][0] is None:
return None
db_map, id_ = data[0][0]
return self.db_mngr.get_value(db_map, "parameter_value", id_, role)
item = self.db_mngr.get_item(db_map, "parameter_value", id_)
return self.db_mngr.get_value(db_map, item, role)

def _do_batch_set_inner_data(self, row_map, column_map, data, values):
return self._batch_set_parameter_value_data(row_map, column_map, data, values)
Expand Down
3 changes: 2 additions & 1 deletion spinetoolbox/spine_db_editor/mvcmodels/single_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,8 @@ def data(self, index, role=Qt.ItemDataRole.DisplayRole):
PARSED_ROLE,
):
id_ = self._main_data[index.row()]
return self.db_mngr.get_value(self.db_map, self.item_type, id_, role)
item = self.db_mngr.get_item(self.db_map, self.item_type, id_)
return self.db_mngr.get_value(self.db_map, item, role)
return super().data(index, role)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
######################################################################################################################

################################################################################
## Form generated from reading UI file 'commit_viewer_affected_item_info.ui'
##
## Created by: Qt User Interface Compiler version 6.5.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################

from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHeaderView, QLabel, QSizePolicy,
QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget)

class Ui_Form(object):
def setupUi(self, Form):
if not Form.objectName():
Form.setObjectName(u"Form")
Form.resize(400, 300)
self.verticalLayout = QVBoxLayout(Form)
self.verticalLayout.setObjectName(u"verticalLayout")
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.affected_items_table = QTableWidget(Form)
self.affected_items_table.setObjectName(u"affected_items_table")

self.verticalLayout.addWidget(self.affected_items_table)

self.fetch_status_label = QLabel(Form)
self.fetch_status_label.setObjectName(u"fetch_status_label")

self.verticalLayout.addWidget(self.fetch_status_label)


self.retranslateUi(Form)

QMetaObject.connectSlotsByName(Form)
# setupUi

def retranslateUi(self, Form):
Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None))
self.fetch_status_label.setText(QCoreApplication.translate("Form", u"TextLabel", None))
# retranslateUi

Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
######################################################################################################################
# Copyright (C) 2017-2022 Spine project consortium
# Copyright Spine Toolbox contributors
# This file is part of Spine Toolbox.
# Spine Toolbox is free software: you can redistribute it and\/or modify it under the terms of the GNU Lesser General
# Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option)
# any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General
# Public License for more details. You should have received a copy of the GNU Lesser General Public License along with
# this program. If not, see <http:\/\/www.gnu.org\/licenses\/>.
######################################################################################################################
-->
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QTableWidget" name="affected_items_table"/>
</item>
<item>
<widget class="QLabel" name="fetch_status_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
95 changes: 75 additions & 20 deletions spinetoolbox/spine_db_editor/ui/db_commit_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,65 +26,118 @@
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QHeaderView, QSizePolicy,
QSplitter, QStackedWidget, QTextBrowser, QTreeWidget,
QTreeWidgetItem, QWidget)
from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView,
QLabel, QSizePolicy, QSplitter, QStackedWidget,
QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget)

class Ui_DBCommitViewer(object):
def setupUi(self, DBCommitViewer):
if not DBCommitViewer.objectName():
DBCommitViewer.setObjectName(u"DBCommitViewer")
DBCommitViewer.resize(716, 218)
self.horizontalLayout_2 = QHBoxLayout(DBCommitViewer)
self.horizontalLayout_2.setSpacing(0)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0)
self.splitter = QSplitter(DBCommitViewer)
self.splitter.setObjectName(u"splitter")
self.splitter.setOrientation(Qt.Horizontal)
self.commit_list = QTreeWidget(self.splitter)
__qtreewidgetitem = QTreeWidgetItem()
__qtreewidgetitem.setText(0, u"1");
self.commit_list.setHeaderItem(__qtreewidgetitem)
self.commit_list.setObjectName(u"commit_list")
self.splitter.addWidget(self.commit_list)
self.affected_items_widget_stack = QStackedWidget(self.splitter)
self.verticalFrame = QFrame(self.splitter)
self.verticalFrame.setObjectName(u"verticalFrame")
self.verticalFrame.setStyleSheet(u"QFrame {\n"
" background-color: white;\n"
"}")
self.verticalFrame.setFrameShape(QFrame.Box)
self.verticalLayout = QVBoxLayout(self.verticalFrame)
self.verticalLayout.setSpacing(3)
self.verticalLayout.setObjectName(u"verticalLayout")
self.verticalLayout.setContentsMargins(5, 5, 5, 0)
self.label = QLabel(self.verticalFrame)
self.label.setObjectName(u"label")

self.verticalLayout.addWidget(self.label)

self.affected_items_widget_stack = QStackedWidget(self.verticalFrame)
self.affected_items_widget_stack.setObjectName(u"affected_items_widget_stack")
self.page = QWidget()
self.page.setObjectName(u"page")
self.horizontalLayout_3 = QHBoxLayout(self.page)
self.items_page = QWidget()
self.items_page.setObjectName(u"items_page")
self.horizontalLayout_3 = QHBoxLayout(self.items_page)
self.horizontalLayout_3.setSpacing(0)
self.horizontalLayout_3.setObjectName(u"horizontalLayout_3")
self.horizontalLayout_3.setContentsMargins(0, 0, 0, 0)
self.affected_items = QTreeWidget(self.page)
self.affected_items.setObjectName(u"affected_items")
self.affected_item_tab_widget = QTabWidget(self.items_page)
self.affected_item_tab_widget.setObjectName(u"affected_item_tab_widget")
self.affected_item_tab_widget.setDocumentMode(True)

self.horizontalLayout_3.addWidget(self.affected_items)
self.horizontalLayout_3.addWidget(self.affected_item_tab_widget)

self.affected_items_widget_stack.addWidget(self.page)
self.page_2 = QWidget()
self.page_2.setObjectName(u"page_2")
self.horizontalLayout = QHBoxLayout(self.page_2)
self.affected_items_widget_stack.addWidget(self.items_page)
self.no_items_page = QWidget()
self.no_items_page.setObjectName(u"no_items_page")
self.horizontalLayout = QHBoxLayout(self.no_items_page)
self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.horizontalLayout.setContentsMargins(0, 0, 0, 0)
self.no_affected_items_notice = QTextBrowser(self.page_2)
self.no_affected_items_notice = QTextBrowser(self.no_items_page)
self.no_affected_items_notice.setObjectName(u"no_affected_items_notice")
self.no_affected_items_notice.setFocusPolicy(Qt.NoFocus)
self.no_affected_items_notice.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.no_affected_items_notice.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.no_affected_items_notice.setOpenLinks(False)

self.horizontalLayout.addWidget(self.no_affected_items_notice)

self.affected_items_widget_stack.addWidget(self.page_2)
self.splitter.addWidget(self.affected_items_widget_stack)
self.affected_items_widget_stack.addWidget(self.no_items_page)
self.loading_page = QWidget()
self.loading_page.setObjectName(u"loading_page")
self.horizontalLayout_4 = QHBoxLayout(self.loading_page)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.loading_label = QLabel(self.loading_page)
self.loading_label.setObjectName(u"loading_label")
self.loading_label.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)

self.horizontalLayout_4.addWidget(self.loading_label)

self.affected_items_widget_stack.addWidget(self.loading_page)
self.no_commit_selected_page = QWidget()
self.no_commit_selected_page.setObjectName(u"no_commit_selected_page")
self.verticalLayout_2 = QVBoxLayout(self.no_commit_selected_page)
self.verticalLayout_2.setSpacing(0)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
self.label_2 = QLabel(self.no_commit_selected_page)
self.label_2.setObjectName(u"label_2")
self.label_2.setAlignment(Qt.AlignLeading|Qt.AlignLeft|Qt.AlignTop)

self.verticalLayout_2.addWidget(self.label_2)

self.affected_items_widget_stack.addWidget(self.no_commit_selected_page)

self.verticalLayout.addWidget(self.affected_items_widget_stack)

self.splitter.addWidget(self.verticalFrame)

self.horizontalLayout_2.addWidget(self.splitter)


self.retranslateUi(DBCommitViewer)

self.affected_items_widget_stack.setCurrentIndex(3)
self.affected_item_tab_widget.setCurrentIndex(-1)


QMetaObject.connectSlotsByName(DBCommitViewer)
# setupUi

def retranslateUi(self, DBCommitViewer):
self.label.setText(QCoreApplication.translate("DBCommitViewer", u"Affected items", None))
self.no_affected_items_notice.setHtml(QCoreApplication.translate("DBCommitViewer", u"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd\">\n"
"<html><head><meta name=\"qrichtext\" content=\"1\" /><meta charset=\"utf-8\" /><style type=\"text/css\">\n"
"p, li { white-space: pre-wrap; }\n"
Expand All @@ -93,7 +146,9 @@ def retranslateUi(self, DBCommitViewer):
"li.checked::marker { content: \"\\2612\"; }\n"
"</style></head><body style=\" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;\">\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">No affected items found for selected commit.</p>\n"
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Note that we cannot show items that have been removed by this or a later commit.</p></body></html>", None))
"<p style=\" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;\">Note, that it is not possible to show items that have been removed by this or a later commit.</p></body></html>", None))
self.loading_label.setText(QCoreApplication.translate("DBCommitViewer", u"Loading...", None))
self.label_2.setText(QCoreApplication.translate("DBCommitViewer", u"Select a commit from the list on the left.", None))
pass
# retranslateUi

Loading

0 comments on commit eec1d8c

Please sign in to comment.