Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-gprof2dot for
openSUSE:Factory checked in at 2026-03-25 21:18:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-gprof2dot (Old)
and /work/SRC/openSUSE:Factory/.python-gprof2dot.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-gprof2dot"
Wed Mar 25 21:18:39 2026 rev:8 rq:1342331 version:2025.4.14
Changes:
--------
--- /work/SRC/openSUSE:Factory/python-gprof2dot/python-gprof2dot.changes
2024-06-09 20:24:00.473694179 +0200
+++
/work/SRC/openSUSE:Factory/.python-gprof2dot.new.8177/python-gprof2dot.changes
2026-03-27 06:47:52.629942066 +0100
@@ -1,0 +2,7 @@
+Tue Mar 17 22:17:43 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 2025.04.14:
+ * Remove "%3" tooltip in SVG output
+ * Add an option to specify time format
+
+-------------------------------------------------------------------
Old:
----
gprof2dot-2024.6.6.tar.gz
New:
----
gprof2dot-2025.4.14.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-gprof2dot.spec ++++++
--- /var/tmp/diff_new_pack.Up7W8O/_old 2026-03-27 06:47:53.145963367 +0100
+++ /var/tmp/diff_new_pack.Up7W8O/_new 2026-03-27 06:47:53.145963367 +0100
@@ -1,7 +1,7 @@
#
# spec file for package python-gprof2dot
#
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-gprof2dot
-Version: 2024.6.6
+Version: 2025.4.14
Release: 0
Summary: Script to generate a dot graph from the output of several
profilers
License: LGPL-3.0-or-later
++++++ gprof2dot-2024.6.6.tar.gz -> gprof2dot-2025.4.14.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/PKG-INFO
new/gprof2dot-2025.4.14/PKG-INFO
--- old/gprof2dot-2024.6.6/PKG-INFO 2024-06-06 07:48:44.426972200 +0200
+++ new/gprof2dot-2025.4.14/PKG-INFO 2025-04-14 09:21:40.634104000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: gprof2dot
-Version: 2024.6.6
+Version: 2025.4.14
Summary: Generate a dot graph from the output of several profilers.
Home-page: https://github.com/jrfonseca/gprof2dot
Author: Jose Fonseca
@@ -16,6 +16,7 @@
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.txt
+Dynamic: license-file
# About _gprof2dot_
@@ -39,7 +40,8 @@
* prune nodes and edges below a certain threshold;
* use an heuristic to propagate time inside mutually recursive functions;
* use color efficiently to draw attention to hot-spots;
- * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere.
+ * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere;
+ * compare two graphs with almost identical structures for the analysis of
performance metrics such as time or function calls.
**If you want an interactive viewer for the graphs generated by _gprof2dot_,
check [xdot.py](https://github.com/jrfonseca/xdot.py).**
@@ -50,8 +52,8 @@
maintenance. So I'm afraid that any requested features are unlikely to be
implemented, and I might be slow processing issue reports or pull requests.
-[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[](https://codecov.io/gh/jrfonseca/gprof2dot)
+[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[](https://codecov.io/gh/jrfonseca/gprof2dot)
# Example
@@ -87,7 +89,7 @@
pip install gprof2dot
- * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/master/gprof2dot.py)
+ * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/main/gprof2dot.py)
* [Git repository](https://github.com/jrfonseca/gprof2dot)
@@ -148,7 +150,24 @@
variety to lower percentages. Values > 1.0 give less
variety to lower percentages
-p FILTER_PATHS, --path=FILTER_PATHS
- Filter all modules not in a specified path
+ Filter all modules not in a specified path
+ --compare Compare two graphs with almost identical structure.
With this
+ option two files should be provided.gprof2dot.py
+ [options] --compare [file1] [file2] ...
+ --compare-tolerance=TOLERANCE
+ Tolerance threshold for node difference
+ (default=0.001%).If the difference is below this value
+ the nodes are considered identical.
+ --compare-only-slower
+ Display comparison only for function which are slower
+ in second graph.
+ --compare-only-faster
+ Display comparison only for function which are faster
+ in second graph.
+ --compare-color-by-difference
+ Color nodes based on the value of the difference.
+ Nodes with the largest differences represent the hot
+ spots.
```
## Examples
@@ -263,6 +282,32 @@
py-spy record -p <pidfile> -f raw -o out.collapse
gprof2dot.py -f collapse out.collapse | dot -Tpng -o output.png
+## Compare Example
+
+This image illustrates an example usage of the `--compare` and
`--compare-color-by-difference` options.
+
+
+
+Arrow pointing to the right indicate node where the function performed faster
+in the profile provided as the second one (second profile), while arrow
+pointing to the left indicate node where the function was faster in the profile
+provided as the first one (first profile).
+
+### Node
+
+ +-----------------------------+
+ | function name \
+ | total time % -/+ total_diff \
+ | ( self time % ) -/+ self_diff /
+ | total calls1 / total calls2 /
+ +-----------------------------+
+
+Where
+- `total time %` and `self time %` come from the first profile
+- `diff` is calculated as the absolute value of `time in the first profile -
time in the second profile`.
+
+> **Note** The compare option has been tested for pstats, axe and callgrind
profiles.
+
## Output
A node in the output graph represents a function and has the following layout:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/README.md
new/gprof2dot-2025.4.14/README.md
--- old/gprof2dot-2024.6.6/README.md 2024-06-06 07:48:36.000000000 +0200
+++ new/gprof2dot-2025.4.14/README.md 2025-04-14 09:21:31.000000000 +0200
@@ -20,7 +20,8 @@
* prune nodes and edges below a certain threshold;
* use an heuristic to propagate time inside mutually recursive functions;
* use color efficiently to draw attention to hot-spots;
- * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere.
+ * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere;
+ * compare two graphs with almost identical structures for the analysis of
performance metrics such as time or function calls.
**If you want an interactive viewer for the graphs generated by _gprof2dot_,
check [xdot.py](https://github.com/jrfonseca/xdot.py).**
@@ -31,8 +32,8 @@
maintenance. So I'm afraid that any requested features are unlikely to be
implemented, and I might be slow processing issue reports or pull requests.
-[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[](https://codecov.io/gh/jrfonseca/gprof2dot)
+[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[](https://codecov.io/gh/jrfonseca/gprof2dot)
# Example
@@ -68,7 +69,7 @@
pip install gprof2dot
- * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/master/gprof2dot.py)
+ * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/main/gprof2dot.py)
* [Git repository](https://github.com/jrfonseca/gprof2dot)
@@ -129,7 +130,24 @@
variety to lower percentages. Values > 1.0 give less
variety to lower percentages
-p FILTER_PATHS, --path=FILTER_PATHS
- Filter all modules not in a specified path
+ Filter all modules not in a specified path
+ --compare Compare two graphs with almost identical structure.
With this
+ option two files should be provided.gprof2dot.py
+ [options] --compare [file1] [file2] ...
+ --compare-tolerance=TOLERANCE
+ Tolerance threshold for node difference
+ (default=0.001%).If the difference is below this value
+ the nodes are considered identical.
+ --compare-only-slower
+ Display comparison only for function which are slower
+ in second graph.
+ --compare-only-faster
+ Display comparison only for function which are faster
+ in second graph.
+ --compare-color-by-difference
+ Color nodes based on the value of the difference.
+ Nodes with the largest differences represent the hot
+ spots.
```
## Examples
@@ -244,6 +262,32 @@
py-spy record -p <pidfile> -f raw -o out.collapse
gprof2dot.py -f collapse out.collapse | dot -Tpng -o output.png
+## Compare Example
+
+This image illustrates an example usage of the `--compare` and
`--compare-color-by-difference` options.
+
+
+
+Arrow pointing to the right indicate node where the function performed faster
+in the profile provided as the second one (second profile), while arrow
+pointing to the left indicate node where the function was faster in the profile
+provided as the first one (first profile).
+
+### Node
+
+ +-----------------------------+
+ | function name \
+ | total time % -/+ total_diff \
+ | ( self time % ) -/+ self_diff /
+ | total calls1 / total calls2 /
+ +-----------------------------+
+
+Where
+- `total time %` and `self time %` come from the first profile
+- `diff` is calculated as the absolute value of `time in the first profile -
time in the second profile`.
+
+> **Note** The compare option has been tested for pstats, axe and callgrind
profiles.
+
## Output
A node in the output graph represents a function and has the following layout:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/gprof2dot.egg-info/PKG-INFO
new/gprof2dot-2025.4.14/gprof2dot.egg-info/PKG-INFO
--- old/gprof2dot-2024.6.6/gprof2dot.egg-info/PKG-INFO 2024-06-06
07:48:44.000000000 +0200
+++ new/gprof2dot-2025.4.14/gprof2dot.egg-info/PKG-INFO 2025-04-14
09:21:40.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
Name: gprof2dot
-Version: 2024.6.6
+Version: 2025.4.14
Summary: Generate a dot graph from the output of several profilers.
Home-page: https://github.com/jrfonseca/gprof2dot
Author: Jose Fonseca
@@ -16,6 +16,7 @@
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.txt
+Dynamic: license-file
# About _gprof2dot_
@@ -39,7 +40,8 @@
* prune nodes and edges below a certain threshold;
* use an heuristic to propagate time inside mutually recursive functions;
* use color efficiently to draw attention to hot-spots;
- * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere.
+ * work on any platform where Python and Graphviz is available, i.e,
virtually anywhere;
+ * compare two graphs with almost identical structures for the analysis of
performance metrics such as time or function calls.
**If you want an interactive viewer for the graphs generated by _gprof2dot_,
check [xdot.py](https://github.com/jrfonseca/xdot.py).**
@@ -50,8 +52,8 @@
maintenance. So I'm afraid that any requested features are unlikely to be
implemented, and I might be slow processing issue reports or pull requests.
-[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[](https://codecov.io/gh/jrfonseca/gprof2dot)
+[](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[](https://codecov.io/gh/jrfonseca/gprof2dot)
# Example
@@ -87,7 +89,7 @@
pip install gprof2dot
- * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/master/gprof2dot.py)
+ * [Standalone
script](https://raw.githubusercontent.com/jrfonseca/gprof2dot/main/gprof2dot.py)
* [Git repository](https://github.com/jrfonseca/gprof2dot)
@@ -148,7 +150,24 @@
variety to lower percentages. Values > 1.0 give less
variety to lower percentages
-p FILTER_PATHS, --path=FILTER_PATHS
- Filter all modules not in a specified path
+ Filter all modules not in a specified path
+ --compare Compare two graphs with almost identical structure.
With this
+ option two files should be provided.gprof2dot.py
+ [options] --compare [file1] [file2] ...
+ --compare-tolerance=TOLERANCE
+ Tolerance threshold for node difference
+ (default=0.001%).If the difference is below this value
+ the nodes are considered identical.
+ --compare-only-slower
+ Display comparison only for function which are slower
+ in second graph.
+ --compare-only-faster
+ Display comparison only for function which are faster
+ in second graph.
+ --compare-color-by-difference
+ Color nodes based on the value of the difference.
+ Nodes with the largest differences represent the hot
+ spots.
```
## Examples
@@ -263,6 +282,32 @@
py-spy record -p <pidfile> -f raw -o out.collapse
gprof2dot.py -f collapse out.collapse | dot -Tpng -o output.png
+## Compare Example
+
+This image illustrates an example usage of the `--compare` and
`--compare-color-by-difference` options.
+
+
+
+Arrow pointing to the right indicate node where the function performed faster
+in the profile provided as the second one (second profile), while arrow
+pointing to the left indicate node where the function was faster in the profile
+provided as the first one (first profile).
+
+### Node
+
+ +-----------------------------+
+ | function name \
+ | total time % -/+ total_diff \
+ | ( self time % ) -/+ self_diff /
+ | total calls1 / total calls2 /
+ +-----------------------------+
+
+Where
+- `total time %` and `self time %` come from the first profile
+- `diff` is calculated as the absolute value of `time in the first profile -
time in the second profile`.
+
+> **Note** The compare option has been tested for pstats, axe and callgrind
profiles.
+
## Output
A node in the output graph represents a function and has the following layout:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/gprof2dot.py
new/gprof2dot-2025.4.14/gprof2dot.py
--- old/gprof2dot-2024.6.6/gprof2dot.py 2024-06-06 07:48:36.000000000 +0200
+++ new/gprof2dot-2025.4.14/gprof2dot.py 2025-04-14 09:21:31.000000000
+0200
@@ -34,6 +34,7 @@
import fnmatch
import codecs
import io
+import hashlib
assert sys.version_info[0] >= 3
@@ -43,6 +44,7 @@
MULTIPLICATION_SIGN = chr(0xd7)
+timeFormat = "%.7g"
def times(x):
@@ -51,12 +53,38 @@
def percentage(p):
return "%.02f%%" % (p*100.0,)
+def fmttime(t):
+ return timeFormat % t
+
def add(a, b):
return a + b
def fail(a, b):
assert False
+# To enhance readability, labels are rounded to the number of decimal
+# places corresponding to the tolerance value.
+def round_difference(difference, tolerance):
+ n = -math.floor(math.log10(tolerance))
+ return round(difference, n)
+
+
+def rescale_difference(x, min_val, max_val):
+ return (x - min_val) / (max_val - min_val)
+
+
+def min_max_difference(profile1, profile2):
+ f1_events = [f1[TOTAL_TIME_RATIO] for _, f1 in
sorted_iteritems(profile1.functions)]
+ f2_events = [f2[TOTAL_TIME_RATIO] for _, f2 in
sorted_iteritems(profile2.functions)]
+ differences = []
+ for i in range(len(f1_events)):
+ try:
+ differences.append(abs(f1_events[i] - f2_events[i]) * 100)
+ except IndexError:
+ differences.append(0)
+
+ return min(differences), max(differences)
+
tol = 2 ** -23
@@ -88,7 +116,7 @@
return 'unspecified event %s' % self.event.name
-class Event(object):
+class Event:
"""Describe a kind of event, and its basic operations."""
def __init__(self, name, null, aggregator, formatter = str):
@@ -100,12 +128,6 @@
def __repr__(self):
return self.name
- def __eq__(self, other):
- return self is other
-
- def __hash__(self):
- return id(self)
-
def null(self):
return self._null
@@ -135,9 +157,9 @@
# Used only when totalMethod == callstacks
TOTAL_SAMPLES = Event("Samples", 0, add, times)
-TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')')
+TIME = Event("Time", 0.0, add, lambda x: '(' + fmttime(x) + ')')
TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')')
-TOTAL_TIME = Event("Total time", 0.0, fail)
+TOTAL_TIME = Event("Total time", 0.0, fail, fmttime)
TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage)
labels = {
@@ -151,7 +173,7 @@
totalMethod = 'callratios'
-class Object(object):
+class Object:
"""Base class for all objects in profile which can store events."""
def __init__(self, events=None):
@@ -160,12 +182,6 @@
else:
self.events = events
- def __hash__(self):
- return id(self)
-
- def __eq__(self, other):
- return self is other
-
def __lt__(self, other):
return id(self) < id(other)
@@ -3252,6 +3268,166 @@
show_function_events = [TOTAL_TIME_RATIO, TIME_RATIO]
show_edge_events = [TOTAL_TIME_RATIO, CALLS]
+ def graphs_compare(self, profile1, profile2, theme, options):
+ self.begin_graph()
+
+ fontname = theme.graph_fontname()
+ fontcolor = theme.graph_fontcolor()
+ nodestyle = theme.node_style()
+
+ tolerance, only_slower, only_faster, color_by_difference = (
+ options.tolerance, options.only_slower, options.only_faster,
options.color_by_difference)
+ self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
+ self.attr('node', fontname=fontname, style=nodestyle,
fontcolor=fontcolor, width=0, height=0)
+ self.attr('edge', fontname=fontname)
+
+ functions2 = {function.name: function for _, function in
sorted_iteritems(profile2.functions)}
+
+ for _, function1 in sorted_iteritems(profile1.functions):
+ labels = []
+
+ name = function1.name
+ try:
+ function2 = functions2[name]
+ if self.wrap:
+ name = self.wrap_function_name(name)
+ if color_by_difference:
+ min_diff, max_diff = min_max_difference(profile1, profile2)
+ labels.append(name)
+ weight_difference = 0
+ shape = 'box'
+ orientation = '0'
+ for event in self.show_function_events:
+ if event in function1.events:
+ event1 = function1[event]
+ event2 = function2[event]
+
+ difference = abs(event1 - event2) * 100
+
+ if event == TOTAL_TIME_RATIO:
+ weight_difference = difference
+ if difference >= tolerance:
+ if event2 > event1 and not only_faster:
+ shape = 'cds'
+ label = (f'{event.format(event1)} +'
+ f' {round_difference(difference,
tolerance)}%')
+ elif event2 < event1 and not only_slower:
+ orientation = "90"
+ shape = 'cds'
+ label = (f'{event.format(event1)} - '
+ f'{round_difference(difference,
tolerance)}%')
+ else:
+ # protection to not color by difference if
we choose to show only_faster/only_slower
+ weight_difference = 0
+ label = event.format(function1[event])
+ else:
+ weight_difference = 0
+ label = event.format(function1[event])
+ else:
+ if difference >= tolerance:
+ if event2 > event1:
+ label = (f'{event.format(event1)} +'
+ f' {round_difference(difference,
tolerance)}%')
+ elif event2 < event1:
+ label = (f'{event.format(event1)} - '
+ f'{round_difference(difference,
tolerance)}%')
+ else:
+ label = event.format(function1[event])
+
+ labels.append(label)
+ if function1.called is not None:
+ labels.append(f"{function1.called}
{MULTIPLICATION_SIGN}/ {function2.called} {MULTIPLICATION_SIGN}")
+
+ except KeyError:
+ shape = 'box'
+ orientation = '0'
+ weight_difference = 0
+ if function1.process is not None:
+ labels.append(function1.process)
+ if function1.module is not None:
+ labels.append(function1.module)
+
+ if self.strip:
+ function_name = function1.stripped_name()
+ else:
+ function_name = function1.name
+ if color_by_difference:
+ min_diff, max_diff = 0, 0
+
+ # dot can't parse quoted strings longer than YY_BUF_SIZE, which
+ # defaults to 16K. But some annotated C++ functions (e.g.,
boost,
+ # https://github.com/jrfonseca/gprof2dot/issues/30) can exceed
that
+ MAX_FUNCTION_NAME = 4096
+ if len(function_name) >= MAX_FUNCTION_NAME:
+ sys.stderr.write('warning: truncating function name with
%u chars (%s)\n' % (len(function_name), function_name[:32] + '...'))
+ function_name = function_name[:MAX_FUNCTION_NAME - 1] +
chr(0x2026)
+
+ if self.wrap:
+ function_name = self.wrap_function_name(function_name)
+ labels.append(function_name)
+
+ for event in self.show_function_events:
+ if event in function1.events:
+ label = event.format(function1[event])
+ labels.append(label)
+ if function1.called is not None:
+ labels.append("%u%s" % (function1.called,
MULTIPLICATION_SIGN))
+
+ if color_by_difference and weight_difference:
+ # min and max is calculated whe color_by_difference is true
+ weight = rescale_difference(weight_difference, min_diff,
max_diff)
+
+ elif function1.weight is not None and not color_by_difference:
+ weight = function1.weight
+ else:
+ weight = 0.0
+
+ label = '\n'.join(labels)
+
+ self.node(function1.id,
+ label=label,
+ orientation=orientation,
+ color=self.color(theme.node_bgcolor(weight)),
+ shape=shape,
+ fontcolor=self.color(theme.node_fgcolor(weight)),
+ fontsize="%f" % theme.node_fontsize(weight),
+ tooltip=function1.filename,
+ )
+
+ calls2 = {call.callee_id: call for _, call in
sorted_iteritems(function2.calls)}
+ functions_by_id1 = {function.id: function for _, function in
sorted_iteritems(profile1.functions)}
+
+ for _, call1 in sorted_iteritems(function1.calls):
+ labels = []
+ try:
+ # if profiles do not have identical setups, callee_id will
not be identical either
+ call_id1 = call1.callee_id
+ call_name = functions_by_id1[call_id1].name
+ call_id2 = functions2[call_name].id
+ call2 = calls2[call_id2]
+ for event in self.show_edge_events:
+ if event in call1.events:
+ label = f'{event.format(call1[event])} /
{event.format(call2[event])}'
+ labels.append(label)
+ except KeyError:
+ for event in self.show_edge_events:
+ if event in call1.events:
+ label = f'{event.format(call1[event])}'
+ labels.append(label)
+
+ weight = 0 if color_by_difference else call1.weight
+ label = '\n'.join(labels)
+ self.edge(function1.id, call1.callee_id,
+ label=label,
+ color=self.color(theme.edge_color(weight)),
+ fontcolor=self.color(theme.edge_color(weight)),
+ fontsize="%.2f" % theme.edge_fontsize(weight),
+ penwidth="%.2f" % theme.edge_penwidth(weight),
+ labeldistance="%.2f" % theme.edge_penwidth(weight),
+ arrowsize="%.2f" % theme.edge_arrowsize(weight),
+ )
+ self.end_graph()
+
def graph(self, profile, theme):
self.begin_graph()
@@ -3340,6 +3516,11 @@
def begin_graph(self):
self.write('digraph {\n')
+ # Work-around graphviz bug[1]: unnamed graphs have "%3" tooltip in SVG
+ # output. The bug was fixed upstream, but graphviz shipped in recent
+ # Linux distros (for example, Ubuntu 24.04) still has the bug.
+ # [1] https://gitlab.com/graphviz/graphviz/-/issues/1376
+ self.write('\ttooltip=" "\n')
def end_graph(self):
self.write('}\n')
@@ -3352,15 +3533,15 @@
def node(self, node, **attrs):
self.write("\t")
- self.id(node)
+ self.node_id(node)
self.attr_list(attrs)
self.write(";\n")
def edge(self, src, dst, **attrs):
self.write("\t")
- self.id(src)
+ self.node_id(src)
self.write(" -> ")
- self.id(dst)
+ self.node_id(dst)
self.attr_list(attrs)
self.write(";\n")
@@ -3376,11 +3557,22 @@
first = False
else:
self.write(", ")
- self.id(name)
+ assert isinstance(name, str)
+ assert name.isidentifier()
+ self.write(name)
self.write('=')
self.id(value)
self.write(']')
+ def node_id(self, id):
+ # Node IDs need to be unique (can't be truncated) but dot doesn't allow
+ # IDs longer than 16384 characters, so use an hash instead for the huge
+ # C++ symbols that can arise, as seen in
+ # https://github.com/jrfonseca/gprof2dot/issues/99
+ if isinstance(id, str) and len(id) > 1024:
+ id = '_' + hashlib.sha1(id.encode('utf-8'),
usedforsecurity=False).hexdigest()
+ self.id(id)
+
def id(self, id):
if isinstance(id, (int, float)):
s = str(id)
@@ -3432,7 +3624,7 @@
def main(argv=sys.argv[1:]):
"""Main program."""
- global totalMethod
+ global totalMethod, timeFormat
formatNames = list(formats.keys())
formatNames.sort()
@@ -3498,6 +3690,10 @@
dest="show_samples", default=False,
help="show function samples")
optparser.add_option(
+ '--time-format',
+ default=timeFormat,
+ help="format to use for showing time values [default: %default]")
+ optparser.add_option(
'--node-label', metavar='MEASURE',
type='choice', choices=labelNames,
action='append',
@@ -3543,9 +3739,36 @@
'-p', '--path', action="append",
type="string", dest="filter_paths",
help="Filter all modules not in a specified path")
+ optparser.add_option(
+ '--compare',
+ action="store_true",
+ dest="compare", default=False,
+ help="Compare two graphs with almost identical structure. With this
option two files should be provided."
+ "gprof2dot.py [options] --compare [file1] [file2] ...")
+ optparser.add_option(
+ '--compare-tolerance',
+ type="float", dest="tolerance", default=0.001,
+ help="Tolerance threshold for node difference (default=0.001%)."
+ "If the difference is below this value the nodes are considered
identical.")
+ optparser.add_option(
+ '--compare-only-slower',
+ action="store_true",
+ dest="only_slower", default=False,
+ help="Display comparison only for function which are slower in second
graph.")
+ optparser.add_option(
+ '--compare-only-faster',
+ action="store_true",
+ dest="only_faster", default=False,
+ help="Display comparison only for function which are faster in second
graph.")
+ optparser.add_option(
+ '--compare-color-by-difference',
+ action="store_true",
+ dest="color_by_difference", default=False,
+ help="Color nodes based on the value of the difference. "
+ "Nodes with the largest differences represent the hot spots.")
(options, args) = optparser.parse_args(argv)
- if len(args) > 1 and options.format != 'pstats':
+ if len(args) > 1 and options.format != 'pstats' and not options.compare:
optparser.error('incorrect number of arguments')
try:
@@ -3558,6 +3781,7 @@
theme.skew = options.theme_skew
totalMethod = options.totalMethod
+ timeFormat = options.time_format
try:
Format = formats[options.format]
@@ -3567,6 +3791,12 @@
if Format.stdinInput:
if not args:
fp = sys.stdin
+ parser = Format(fp)
+ elif options.compare:
+ fp1 = open(args[0], 'rt', encoding='UTF-8')
+ fp2 = open(args[1], 'rt', encoding='UTF-8')
+ parser1 = Format(fp1)
+ parser2 = Format(fp2)
else:
fp = open(args[0], 'rb')
bom = fp.read(2)
@@ -3577,17 +3807,25 @@
encoding = 'utf-8'
fp.seek(0)
fp = io.TextIOWrapper(fp, encoding=encoding)
- parser = Format(fp)
+ parser = Format(fp)
elif Format.multipleInput:
if not args:
optparser.error('at least a file must be specified for %s input' %
options.format)
- parser = Format(*args)
+ if options.compare:
+ parser1 = Format(args[-2])
+ parser2 = Format(args[-1])
+ else:
+ parser = Format(*args)
else:
if len(args) != 1:
optparser.error('exactly one file must be specified for %s input'
% options.format)
parser = Format(args[0])
- profile = parser.parse()
+ if options.compare:
+ profile1 = parser1.parse()
+ profile2 = parser2.parse()
+ else:
+ profile = parser.parse()
if options.output is None:
output = open(sys.stdout.fileno(), mode='wt', encoding='UTF-8',
closefd=False)
@@ -3603,18 +3841,29 @@
if options.show_samples:
dot.show_function_events.append(SAMPLES)
- profile.prune(options.node_thres/100.0, options.edge_thres/100.0,
options.filter_paths, options.color_nodes_by_selftime)
+ if options.compare:
+ profile1.prune(options.node_thres/100.0, options.edge_thres/100.0,
options.filter_paths,
+ options.color_nodes_by_selftime)
+ profile2.prune(options.node_thres/100.0, options.edge_thres/100.0,
options.filter_paths,
+ options.color_nodes_by_selftime)
+
+ if options.root:
+ profile1.prune_root(profile1.getFunctionIds(options.root),
options.depth)
+ profile2.prune_root(profile2.getFunctionIds(options.root),
options.depth)
+ else:
+ profile.prune(options.node_thres/100.0, options.edge_thres/100.0,
options.filter_paths,
+ options.color_nodes_by_selftime)
+ if options.root:
+ rootIds = profile.getFunctionIds(options.root)
+ if not rootIds:
+ sys.stderr.write('root node ' + options.root + ' not found
(might already be pruned : try -e0 -n0 flags)\n')
+ sys.exit(1)
+ profile.prune_root(rootIds, options.depth)
if options.list_functions:
profile.printFunctionIds(selector=options.list_functions)
sys.exit(0)
- if options.root:
- rootIds = profile.getFunctionIds(options.root)
- if not rootIds:
- sys.stderr.write('root node ' + options.root + ' not found (might
already be pruned : try -e0 -n0 flags)\n')
- sys.exit(1)
- profile.prune_root(rootIds, options.depth)
if options.leaf:
leafIds = profile.getFunctionIds(options.leaf)
if not leafIds:
@@ -3622,7 +3871,10 @@
sys.exit(1)
profile.prune_leaf(leafIds, options.depth)
- dot.graph(profile, theme)
+ if options.compare:
+ dot.graphs_compare(profile1, profile2, theme, options)
+ else:
+ dot.graph(profile, theme)
if __name__ == '__main__':
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/setup.cfg
new/gprof2dot-2025.4.14/setup.cfg
--- old/gprof2dot-2024.6.6/setup.cfg 2024-06-06 07:48:44.430972000 +0200
+++ new/gprof2dot-2025.4.14/setup.cfg 2025-04-14 09:21:40.635104000 +0200
@@ -1,6 +1,6 @@
[metadata]
name = gprof2dot
-version = 2024.06.06
+version = 2025.04.14
author = Jose Fonseca
author_email = [email protected]
url = https://github.com/jrfonseca/gprof2dot
@@ -25,9 +25,6 @@
console_scripts =
gprof2dot = gprof2dot:main
-[bdist_wheel]
-universal = 1
-
[egg_info]
tag_build =
tag_date = 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/gprof2dot-2024.6.6/tests/test.py
new/gprof2dot-2025.4.14/tests/test.py
--- old/gprof2dot-2024.6.6/tests/test.py 2024-06-06 07:48:36.000000000
+0200
+++ new/gprof2dot-2025.4.14/tests/test.py 2025-04-14 09:21:31.000000000
+0200
@@ -44,6 +44,11 @@
"xperf",
"dtrace",
]
+formats_compare = [
+ "axe",
+ "callgrind",
+ "pstats"
+]
NB_RUN_FAILURES = 0
@@ -91,7 +96,7 @@
b_lines = open(b, 'rt', encoding='UTF-8').readlines()
diff_lines = difflib.unified_diff(a_lines, b_lines, fromfile=a, tofile=b)
- diff_txt= ''.join(diff_lines)
+ diff_txt = ''.join(diff_lines)
if len(diff_txt) > 0:
NB_DIFF_FAILURES += 1
sys.stdout.write("Non empty diff for files %s and %s" %(a,b))
@@ -167,10 +172,41 @@
else:
diff(ref_dot, dot)
+ for format_compare in formats_compare:
+ subdir = os.path.join(test_dir, 'compare', format_compare)
+ for dirname in os.listdir(subdir):
+ test_subdir = os.path.join(subdir, dirname)
+ name1 = dirname + '1.'
+ name2 = dirname + '2.'
+ filename1 = name1 + 'txt' if format_compare == 'axe' else name1 +
format_compare
+ filename2 = name2 + 'txt' if format_compare == 'axe' else name2 +
format_compare
+
+ sys.stdout.write(filename1 + '\n')
+
+ profile1 = os.path.join(test_subdir, filename1)
+ profile2 = os.path.join(test_subdir, filename2)
+ dot = os.path.join(test_subdir, name1 + '.dot')
+ png = os.path.join(test_subdir, name1 + '.png')
+
+ ref_dot = os.path.join(test_subdir, name1 + '.orig.dot')
+ ref_png = os.path.join(test_subdir, name1 + '.orig.png')
+
+ if run_gprof2dot(['-f', format_compare, '-o', dot, '--compare',
profile1, profile2]) != 0:
+ continue
+
+ if run(['dot', '-Tpng', '-o', png, dot]) != 0:
+ continue
+
+ if options.force or not os.path.exists(ref_dot):
+ shutil.copy(dot, ref_dot)
+ shutil.copy(png, ref_png)
+ else:
+ diff(ref_dot, dot)
+
# test the --list-functions flag only for pstats format
profile = os.path.join(test_dir, 'pstats', 'memtrail.pstats')
genfileNm = os.path.join(test_dir, 'pstats', 'function-list.testgen.txt')
- outfile = open(genfileNm, "w")
+ outfile = open(genfileNm, "w")
for flagVal in ("+", "execfile", "*execfile", "*:execfile", "*parse",
"*parse_*"):
run_gprof2dot(['-f', "pstats", "--list-functions="+flagVal, profile],
stderr=outfile)
@@ -187,7 +223,7 @@
if NB_RUN_FAILURES or NB_DIFF_FAILURES:
print("Nb runs ending in error: %d" % NB_RUN_FAILURES)
print("Nb diffs showing a difference: %d" % NB_DIFF_FAILURES)
- if ( options.max_acceptable is not None
+ if (options.max_acceptable is not None
and NB_RUN_FAILURES + NB_DIFF_FAILURES > options.max_acceptable ):
print("Too many errors: returning non-zero code")
sys.exit(1)