From b284b887461ea1790d0e3cfe62a27bb869551211 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 30 Aug 2023 15:31:13 +0000 Subject: [PATCH 1/2] add stacktracer.py: helper util to debug threading issues taken from https://code.activestate.com/recipes/577334-how-to-debug-deadlocked-multi-threaded-programs/ --- electrum/utils/__init__.py | 0 electrum/utils/stacktracer.py | 132 ++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 electrum/utils/__init__.py create mode 100644 electrum/utils/stacktracer.py diff --git a/electrum/utils/__init__.py b/electrum/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/electrum/utils/stacktracer.py b/electrum/utils/stacktracer.py new file mode 100644 index 000000000..a1fe4ea18 --- /dev/null +++ b/electrum/utils/stacktracer.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# +# Copyright (C) 2010 Laszlo Nagy (nagylzs) +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# Taken from: https://code.activestate.com/recipes/577334-how-to-debug-deadlocked-multi-threaded-programs/ + + +"""Stack tracer for multi-threaded applications. + + +Usage: + +import stacktracer +stacktracer.start_trace("trace.html",interval=5,auto=True) # Set auto flag to always update file! +.... +stacktracer.stop_trace() +""" + +import sys +import traceback +from pygments import highlight +from pygments.lexers import PythonLexer +from pygments.formatters import HtmlFormatter + + +# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/ + +def stacktraces(): + code = [] + for threadId, stack in sys._current_frames().items(): + code.append("\n# ThreadID: %s" % threadId) + for filename, lineno, name, line in traceback.extract_stack(stack): + code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + if line: + code.append(" %s" % (line.strip())) + + return highlight("\n".join(code), PythonLexer(), HtmlFormatter( + full=False, + # style="native", + noclasses=True, + )) + + +# This part was made by nagylzs +import os +import time +import threading + + +class TraceDumper(threading.Thread): + """Dump stack traces into a given file periodically.""" + + def __init__(self, fpath, interval, auto): + """ + @param fpath: File path to output HTML (stack trace file) + @param auto: Set flag (True) to update trace continuously. + Clear flag (False) to update only if file not exists. + (Then delete the file to force update.) + @param interval: In seconds: how often to update the trace file. + """ + assert (interval > 0.1) + self.auto = auto + self.interval = interval + self.fpath = os.path.abspath(fpath) + self.stop_requested = threading.Event() + threading.Thread.__init__(self) + + def run(self): + while not self.stop_requested.isSet(): + time.sleep(self.interval) + if self.auto or not os.path.isfile(self.fpath): + self.stacktraces() + + def stop(self): + self.stop_requested.set() + self.join() + try: + if os.path.isfile(self.fpath): + os.unlink(self.fpath) + except: + pass + + def stacktraces(self): + fout = file(self.fpath, "wb+") + try: + fout.write(stacktraces()) + finally: + fout.close() + + +_tracer = None + + +def trace_start(fpath, interval=5, auto=True): + """Start tracing into the given file.""" + global _tracer + if _tracer is None: + _tracer = TraceDumper(fpath, interval, auto) + _tracer.setDaemon(True) + _tracer.start() + else: + raise Exception("Already tracing to %s" % _tracer.fpath) + + +def trace_stop(): + """Stop tracing.""" + global _tracer + if _tracer is None: + raise Exception("Not tracing, cannot stop.") + else: + _trace.stop() + _trace = None From 59a6690986fe720a107594522205c41f230511a5 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 30 Aug 2023 16:00:11 +0000 Subject: [PATCH 2/2] stacktracer.py: small fixes and clean-up --- electrum/utils/stacktracer.py | 64 +++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/electrum/utils/stacktracer.py b/electrum/utils/stacktracer.py index a1fe4ea18..a71cd6c21 100644 --- a/electrum/utils/stacktracer.py +++ b/electrum/utils/stacktracer.py @@ -26,31 +26,43 @@ """Stack tracer for multi-threaded applications. - +Useful for debugging deadlocks and hangs. Usage: + import stacktracer + stacktracer.trace_start("trace.html", interval=5) + ... + stacktracer.trace_stop() -import stacktracer -stacktracer.start_trace("trace.html",interval=5,auto=True) # Set auto flag to always update file! -.... -stacktracer.stop_trace() +This will create a file named "trace.html" showing the stack traces of all threads, +updated every 5 seconds. """ +import os import sys +import threading +import time import traceback +from typing import Optional + +# 3rd-party dependency: from pygments import highlight from pygments.lexers import PythonLexer from pygments.formatters import HtmlFormatter -# Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/ +def _thread_from_id(ident) -> Optional[threading.Thread]: + return threading._active.get(ident) + def stacktraces(): + """Taken from http://bzimmer.ziclix.com/2008/12/17/python-thread-dumps/""" code = [] - for threadId, stack in sys._current_frames().items(): - code.append("\n# ThreadID: %s" % threadId) + for thread_id, stack in sys._current_frames().items(): + thread = _thread_from_id(thread_id) + code.append(f"\n# thread_id={thread_id}. thread={thread}") for filename, lineno, name, line in traceback.extract_stack(stack): - code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) + code.append(f'File: "{filename}", line {lineno}, in {name}') if line: code.append(" %s" % (line.strip())) @@ -61,14 +73,11 @@ def stacktraces(): )) -# This part was made by nagylzs -import os -import time -import threading - - class TraceDumper(threading.Thread): - """Dump stack traces into a given file periodically.""" + """Dump stack traces into a given file periodically. + + # written by nagylzs + """ def __init__(self, fpath, interval, auto): """ @@ -86,10 +95,10 @@ class TraceDumper(threading.Thread): threading.Thread.__init__(self) def run(self): - while not self.stop_requested.isSet(): + while not self.stop_requested.is_set(): time.sleep(self.interval) if self.auto or not os.path.isfile(self.fpath): - self.stacktraces() + self.dump_stacktraces() def stop(self): self.stop_requested.set() @@ -97,26 +106,23 @@ class TraceDumper(threading.Thread): try: if os.path.isfile(self.fpath): os.unlink(self.fpath) - except: + except OSError: pass - def stacktraces(self): - fout = file(self.fpath, "wb+") - try: + def dump_stacktraces(self): + with open(self.fpath, "w+") as fout: fout.write(stacktraces()) - finally: - fout.close() -_tracer = None +_tracer = None # type: Optional[TraceDumper] -def trace_start(fpath, interval=5, auto=True): +def trace_start(fpath, interval=5, *, auto=True): """Start tracing into the given file.""" global _tracer if _tracer is None: _tracer = TraceDumper(fpath, interval, auto) - _tracer.setDaemon(True) + _tracer.daemon = True _tracer.start() else: raise Exception("Already tracing to %s" % _tracer.fpath) @@ -128,5 +134,5 @@ def trace_stop(): if _tracer is None: raise Exception("Not tracing, cannot stop.") else: - _trace.stop() - _trace = None + _tracer.stop() + _tracer = None