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.
 
-[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/master/graph/badge.svg?token=pBvnAuazx0)](https://codecov.io/gh/jrfonseca/gprof2dot)
+[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/main/graph/badge.svg?token=pBvnAuazx0)](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.
+
+![Compare](./images/compare_diff.png)
+
+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.
 
-[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/master/graph/badge.svg?token=pBvnAuazx0)](https://codecov.io/gh/jrfonseca/gprof2dot)
+[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/main/graph/badge.svg?token=pBvnAuazx0)](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.
+
+![Compare](./images/compare_diff.png)
+
+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.
 
-[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
-[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/master/graph/badge.svg?token=pBvnAuazx0)](https://codecov.io/gh/jrfonseca/gprof2dot)
+[![Build 
Status](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml/badge.svg?branch=main)](https://github.com/jrfonseca/gprof2dot/actions/workflows/build.yml)
+[![codecov](https://codecov.io/gh/jrfonseca/gprof2dot/branch/main/graph/badge.svg?token=pBvnAuazx0)](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.
+
+![Compare](./images/compare_diff.png)
+
+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)

Reply via email to