diff --git a/tools/Makefile.am b/tools/Makefile.am
index 2dbcb89fd6..4c5420f4e5 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -96,6 +96,8 @@ AM_LDFLAGS = \
$(xnone)
EXTRA_DIST = \
- defcheck.py \
- gimp-mkenums \
- gimppath2svg.py
+ defcheck.py \
+ gimp-mkenums \
+ gimppath2svg.py \
+ performance-log-expand.py \
+ performance-log-resolve.py
diff --git a/tools/performance-log-expand.py b/tools/performance-log-expand.py
new file mode 100755
index 0000000000..c0f7cc9471
--- /dev/null
+++ b/tools/performance-log-expand.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+
+"""
+performance-log-expand.py -- Delta-decodes GIMP performance logs
+Copyright (C) 2018 Ell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+
+Usage: performance-log-expand.py < infile > outfile
+"""
+
+from xml.etree import ElementTree
+import sys
+
+empty_element = ElementTree.Element ("")
+
+# Read performance log from STDIN
+log = ElementTree.fromstring (sys.stdin.buffer.read ())
+
+try:
+ has_backtrace = bool (int (log.find ("params").find ("backtrace").text))
+except:
+ has_backtrace = False
+
+def expand_simple (element, last_values):
+ last_values.update ({value.tag: value.text for value in element})
+
+ for value in list (element):
+ element.remove (value)
+
+ for tag, text in last_values.items ():
+ value = ElementTree.SubElement (element, tag)
+ value.text = text
+ value.tail = "\n"
+
+# Expand samples
+last_vars = {}
+last_backtrace = {}
+
+for sample in (log.find ("samples") or empty_element).iterfind ("sample"):
+ # Expand variables
+ vars = sample.find ("vars") or \
+ ElementTree.SubElement (sample, "vars")
+
+ expand_simple (vars, last_vars)
+
+ # Expand backtrace
+ if has_backtrace:
+ backtrace = sample.find ("backtrace") or \
+ ElementTree.SubElement (sample, "backtrace")
+
+ for thread in backtrace:
+ id = thread.get ("id")
+ frames = list (thread)
+
+ if not frames:
+ last_backtrace.pop (id, None)
+ else:
+ last_thread = last_backtrace.setdefault (id, [None, []])
+ last_frames = last_thread[1]
+
+ name = thread.get ("name")
+ head = thread.get ("head")
+ tail = thread.get ("tail")
+
+ if head:
+ frames = last_frames[:int (head)] + frames
+ if tail:
+ frames = frames + last_frames[-int (tail):]
+
+ last_thread[0] = name
+ last_thread[1] = frames
+
+ for thread in list (backtrace):
+ backtrace.remove (thread)
+
+ for id, (name, frames) in last_backtrace.items ():
+ thread = ElementTree.SubElement (backtrace, "thread", id=id)
+ thread.text = "\n"
+ thread.tail = "\n"
+
+ if name:
+ thread.set ("name", name)
+
+ thread.extend (frames)
+
+# Expand address map
+last_address = {}
+
+for address in (log.find ("address-map") or empty_element).iterfind ("address"):
+ expand_simple (address, last_address)
+
+# Write performance log to STDOUT
+sys.stdout.buffer.write (ElementTree.tostring (log))
diff --git a/tools/performance-log-resolve.py b/tools/performance-log-resolve.py
new file mode 100755
index 0000000000..272f0ebb24
--- /dev/null
+++ b/tools/performance-log-resolve.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+
+"""
+performance-log-resolve.py -- Resolve GIMP performance log backtrace symbols
+Copyright (C) 2018 Ell
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU 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 General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+
+
+Usage: performance-log-resolve.py < infile > outfile
+"""
+
+from xml.etree import ElementTree
+import sys
+
+empty_element = ElementTree.Element ("")
+
+# Read performance log from STDIN
+log = ElementTree.fromstring (sys.stdin.buffer.read ())
+
+address_map = log.find ("address-map")
+
+if address_map:
+ # Create address dictionary
+ addresses = {}
+
+ for address in address_map.iterfind ("address"):
+ addresses[address.get ("value")] = list (address)
+
+ # Resolve addresses in backtraces
+ for sample in (log.find ("samples") or empty_element).iterfind ("sample"):
+ for thread in sample.find ("backtrace") or ():
+ for frame in thread:
+ address = addresses.get (frame.get ("address"))
+
+ if address:
+ frame.text = "\n"
+ frame.extend (address)
+
+ # Remove address map
+ log.remove (address_map)
+
+# Write performance log to STDOUT
+sys.stdout.buffer.write (ElementTree.tostring (log))