Re: [PATCH 4/6] perf scripts python: exported-sql-viewer.py: Add copy to clipboard

From: Arnaldo Carvalho de Melo
Date: Mon May 13 2019 - 16:05:05 EST


Em Fri, May 03, 2019 at 03:08:26PM +0300, Adrian Hunter escreveu:
> Add support for copying to clipboard. Two menu options are added to copy the
> selected rows / columns with normal spacing, or as comma-separated-values.
> In the case of trees, only entire rows can be copied.

Cool, it works:

Comitter testing:

$ python ~acme/libexec/perf-core/scripts/python/exported-sql-viewer.py ~/c/adrian.hunter/simple-retpoline.db

Select the lines, press control+C and on the same terminal,
press control+shift+V and voilÃ:

Call Path Object Count Time (ns) Time (%) Branch Count Branch Count (%)
â 14503:14503
â _start ld-2.28.so 1 156267 100.0 10602 100.0
unknown unknown 1 2276 1.5 1 0.0
â _dl_start ld-2.28.so 1 137047 87.7 10088 95.2
â unknown unknown 4 4127 3.0 4 0.0
_dl_setup_hash ld-2.28.so 1 0 0.0 1 0.0
â _dl_sysdep_start ld-2.28.so 1 131342 95.8 9981 98.9
â _dl_init ld-2.28.so 1 9142 5.9 326 3.1
â call_init.part.0 ld-2.28.so 3 9133 99.9 319 97.9
â _init libc-2.28.so 1 6877 75.3 110 34.5
â check_stdfiles_vtables libc-2.28.so 1 76 0.8 2 0.6
â init_cacheinfo libc-2.28.so 1 1991 21.8 197 61.8
â _start simple-retpoline 1 7457 4.8 182 1.7


> Signed-off-by: Adrian Hunter <adrian.hunter@xxxxxxxxx>
> ---
> .../scripts/python/exported-sql-viewer.py | 217 ++++++++++++++++++
> 1 file changed, 217 insertions(+)
>
> diff --git a/tools/perf/scripts/python/exported-sql-viewer.py b/tools/perf/scripts/python/exported-sql-viewer.py
> index c0fb88d440ba..5804d9705ab7 100755
> --- a/tools/perf/scripts/python/exported-sql-viewer.py
> +++ b/tools/perf/scripts/python/exported-sql-viewer.py
> @@ -898,6 +898,8 @@ class TreeWindowBase(QMdiSubWindow):
> self.find_bar = None
>
> self.view = QTreeView()
> + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
> + self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
>
> def DisplayFound(self, ids):
> if not len(ids):
> @@ -1666,6 +1668,8 @@ class BranchWindow(QMdiSubWindow):
>
> self.view = QTreeView()
> self.view.setUniformRowHeights(True)
> + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
> + self.view.CopyCellsToClipboard = CopyTreeCellsToClipboard
> self.view.setModel(self.model)
>
> self.ResizeColumnsToContents()
> @@ -2278,6 +2282,207 @@ class ResizeColumnsToContentsBase(QObject):
> self.data_model.rowsInserted.disconnect(self.UpdateColumnWidths)
> self.ResizeColumnsToContents()
>
> +# Convert value to CSV
> +
> +def ToCSValue(val):
> + if '"' in val:
> + val = val.replace('"', '""')
> + if "," in val or '"' in val:
> + val = '"' + val + '"'
> + return val
> +
> +# Key to sort table model indexes by row / column, assuming fewer than 1000 columns
> +
> +glb_max_cols = 1000
> +
> +def RowColumnKey(a):
> + return a.row() * glb_max_cols + a.column()
> +
> +# Copy selected table cells to clipboard
> +
> +def CopyTableCellsToClipboard(view, as_csv=False, with_hdr=False):
> + indexes = sorted(view.selectedIndexes(), key=RowColumnKey)
> + idx_cnt = len(indexes)
> + if not idx_cnt:
> + return
> + if idx_cnt == 1:
> + with_hdr=False
> + min_row = indexes[0].row()
> + max_row = indexes[0].row()
> + min_col = indexes[0].column()
> + max_col = indexes[0].column()
> + for i in indexes:
> + min_row = min(min_row, i.row())
> + max_row = max(max_row, i.row())
> + min_col = min(min_col, i.column())
> + max_col = max(max_col, i.column())
> + if max_col > glb_max_cols:
> + raise RuntimeError("glb_max_cols is too low")
> + max_width = [0] * (1 + max_col - min_col)
> + for i in indexes:
> + c = i.column() - min_col
> + max_width[c] = max(max_width[c], len(str(i.data())))
> + text = ""
> + pad = ""
> + sep = ""
> + if with_hdr:
> + model = indexes[0].model()
> + for col in range(min_col, max_col + 1):
> + val = model.headerData(col, Qt.Horizontal)
> + if as_csv:
> + text += sep + ToCSValue(val)
> + sep = ","
> + else:
> + c = col - min_col
> + max_width[c] = max(max_width[c], len(val))
> + width = max_width[c]
> + align = model.headerData(col, Qt.Horizontal, Qt.TextAlignmentRole)
> + if align & Qt.AlignRight:
> + val = val.rjust(width)
> + text += pad + sep + val
> + pad = " " * (width - len(val))
> + sep = " "
> + text += "\n"
> + pad = ""
> + sep = ""
> + last_row = min_row
> + for i in indexes:
> + if i.row() > last_row:
> + last_row = i.row()
> + text += "\n"
> + pad = ""
> + sep = ""
> + if as_csv:
> + text += sep + ToCSValue(str(i.data()))
> + sep = ","
> + else:
> + width = max_width[i.column() - min_col]
> + if i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
> + val = str(i.data()).rjust(width)
> + else:
> + val = str(i.data())
> + text += pad + sep + val
> + pad = " " * (width - len(val))
> + sep = " "
> + QApplication.clipboard().setText(text)
> +
> +def CopyTreeCellsToClipboard(view, as_csv=False, with_hdr=False):
> + indexes = view.selectedIndexes()
> + if not len(indexes):
> + return
> +
> + selection = view.selectionModel()
> +
> + first = None
> + for i in indexes:
> + above = view.indexAbove(i)
> + if not selection.isSelected(above):
> + first = i
> + break
> +
> + if first is None:
> + raise RuntimeError("CopyTreeCellsToClipboard internal error")
> +
> + model = first.model()
> + row_cnt = 0
> + col_cnt = model.columnCount(first)
> + max_width = [0] * col_cnt
> +
> + indent_sz = 2
> + indent_str = " " * indent_sz
> +
> + expanded_mark_sz = 2
> + if sys.version_info[0] == 3:
> + expanded_mark = "\u25BC "
> + not_expanded_mark = "\u25B6 "
> + else:
> + expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xBC) + " ", "utf-8")
> + not_expanded_mark = unicode(chr(0xE2) + chr(0x96) + chr(0xB6) + " ", "utf-8")
> + leaf_mark = " "
> +
> + if not as_csv:
> + pos = first
> + while True:
> + row_cnt += 1
> + row = pos.row()
> + for c in range(col_cnt):
> + i = pos.sibling(row, c)
> + if c:
> + n = len(str(i.data()))
> + else:
> + n = len(str(i.data()).strip())
> + n += (i.internalPointer().level - 1) * indent_sz
> + n += expanded_mark_sz
> + max_width[c] = max(max_width[c], n)
> + pos = view.indexBelow(pos)
> + if not selection.isSelected(pos):
> + break
> +
> + text = ""
> + pad = ""
> + sep = ""
> + if with_hdr:
> + for c in range(col_cnt):
> + val = model.headerData(c, Qt.Horizontal, Qt.DisplayRole).strip()
> + if as_csv:
> + text += sep + ToCSValue(val)
> + sep = ","
> + else:
> + max_width[c] = max(max_width[c], len(val))
> + width = max_width[c]
> + align = model.headerData(c, Qt.Horizontal, Qt.TextAlignmentRole)
> + if align & Qt.AlignRight:
> + val = val.rjust(width)
> + text += pad + sep + val
> + pad = " " * (width - len(val))
> + sep = " "
> + text += "\n"
> + pad = ""
> + sep = ""
> +
> + pos = first
> + while True:
> + row = pos.row()
> + for c in range(col_cnt):
> + i = pos.sibling(row, c)
> + val = str(i.data())
> + if not c:
> + if model.hasChildren(i):
> + if view.isExpanded(i):
> + mark = expanded_mark
> + else:
> + mark = not_expanded_mark
> + else:
> + mark = leaf_mark
> + val = indent_str * (i.internalPointer().level - 1) + mark + val.strip()
> + if as_csv:
> + text += sep + ToCSValue(val)
> + sep = ","
> + else:
> + width = max_width[c]
> + if c and i.data(Qt.TextAlignmentRole) & Qt.AlignRight:
> + val = val.rjust(width)
> + text += pad + sep + val
> + pad = " " * (width - len(val))
> + sep = " "
> + pos = view.indexBelow(pos)
> + if not selection.isSelected(pos):
> + break
> + text = text.rstrip() + "\n"
> + pad = ""
> + sep = ""
> +
> + QApplication.clipboard().setText(text)
> +
> +def CopyCellsToClipboard(view, as_csv=False, with_hdr=False):
> + view.CopyCellsToClipboard(view, as_csv, with_hdr)
> +
> +def CopyCellsToClipboardHdr(view):
> + CopyCellsToClipboard(view, False, True)
> +
> +def CopyCellsToClipboardCSV(view):
> + CopyCellsToClipboard(view, True, True)
> +
> # Table window
>
> class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
> @@ -2296,6 +2501,8 @@ class TableWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
> self.view.verticalHeader().setVisible(False)
> self.view.sortByColumn(-1, Qt.AscendingOrder)
> self.view.setSortingEnabled(True)
> + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
> + self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
>
> self.ResizeColumnsToContents()
>
> @@ -2412,6 +2619,8 @@ class TopCallsWindow(QMdiSubWindow, ResizeColumnsToContentsBase):
> self.view.setModel(self.model)
> self.view.setEditTriggers(QAbstractItemView.NoEditTriggers)
> self.view.verticalHeader().setVisible(False)
> + self.view.setSelectionMode(QAbstractItemView.ContiguousSelection)
> + self.view.CopyCellsToClipboard = CopyTableCellsToClipboard
>
> self.ResizeColumnsToContents()
>
> @@ -2749,6 +2958,8 @@ class MainWindow(QMainWindow):
> file_menu.addAction(CreateExitAction(glb.app, self))
>
> edit_menu = menu.addMenu("&Edit")
> + edit_menu.addAction(CreateAction("&Copy", "Copy to clipboard", self.CopyToClipboard, self, QKeySequence.Copy))
> + edit_menu.addAction(CreateAction("Copy as CS&V", "Copy to clipboard as CSV", self.CopyToClipboardCSV, self))
> edit_menu.addAction(CreateAction("&Find...", "Find items", self.Find, self, QKeySequence.Find))
> edit_menu.addAction(CreateAction("Fetch &more records...", "Fetch more records", self.FetchMoreRecords, self, [QKeySequence(Qt.Key_F8)]))
> edit_menu.addAction(CreateAction("&Shrink Font", "Make text smaller", self.ShrinkFont, self, [QKeySequence("Ctrl+-")]))
> @@ -2781,6 +2992,12 @@ class MainWindow(QMainWindow):
> except:
> pass
>
> + def CopyToClipboard(self):
> + self.Try(CopyCellsToClipboardHdr)
> +
> + def CopyToClipboardCSV(self):
> + self.Try(CopyCellsToClipboardCSV)
> +
> def Find(self):
> win = self.mdi_area.activeSubWindow()
> if win:
> --
> 2.17.1

--

- Arnaldo