在 Python 除錯過程中設定不中斷的斷點
你對如何讓偵錯程式變得更快產生過興趣嗎?本文將分享我們在為 Python 構建偵錯程式時得到的一些經驗。
整段故事講的是我們在 Rookout 公司的團隊為 Python 偵錯程式開發不中斷斷點的經歷,以及開發過程中得到的經驗。我將在本月於舊金山舉辦的 PyBay 2019 上介紹有關 Python 除錯過程的更多細節,但現在就讓我們立刻開始這段故事。
Python 偵錯程式的心臟:sys.set_trace
在諸多可選的 Python 偵錯程式中,使用最廣泛的三個是:
- pdb,它是 Python 標準庫的一部分
- PyDev,它是內嵌在 Eclipse 和 Pycharm 等 IDE 中的偵錯程式
- ipdb,它是 IPython 的偵錯程式
Python 偵錯程式的選擇雖多,但它們幾乎都基於同一個函式:sys.settrace
。 值得一提的是, sys.settrace 可能也是 Python 標準庫中最複雜的函式。
簡單來講,settrace
的作用是為直譯器註冊一個跟蹤函式,它在下列四種情形發生時被呼叫:
- 函式呼叫
- 語句執行
- 函式返回
- 異常丟擲
一個簡單的跟蹤函式看上去大概是這樣:
def simple_tracer(frame, event, arg):
co = frame.f_code
func_name = co.co_name
line_no = frame.f_lineno
print("{e} {f} {l}".format(
e=event, f=func_name, l=line_no))
return simple_tracer
在分析函式時我們首先關注的是引數和返回值,該跟蹤函式的引數分別是:
frame
,當前堆疊幀,它是包含當前函式執行時直譯器裡完整狀態的物件event
,事件,它是一個值可能為call
、line
、return
或exception
的字串arg
,引數,它的取值基於event
的型別,是一個可選項
該跟蹤函式的返回值是它自身,這是由於直譯器需要持續跟蹤兩類跟蹤函式:
- 全域性跟蹤函式(每執行緒):該跟蹤函式由當前執行緒呼叫
sys.settrace
來設定,並在直譯器建立一個新的堆疊幀時被呼叫(即程式碼中發生函式呼叫時)。雖然沒有現成的方式來為不同的執行緒設定跟蹤函式,但你可以呼叫threading.settrace
來為所有新建立的threading
模組執行緒設定跟蹤函式。 - 區域性跟蹤函式(每一幀):直譯器將該跟蹤函式的值設定為全域性跟蹤函式建立幀時的返回值。同樣也沒有現成的方法能夠在幀被建立時自動設定區域性跟蹤函式。
該機制的目的是讓偵錯程式對被跟蹤的幀有更精確的把握,以減少對效能的影響。
簡單三步構建偵錯程式 (我們最初的設想)
僅僅依靠上文提到的內容,用自制的跟蹤函式來構建一個真正的偵錯程式似乎有些不切實際。幸運的是,Python 的標準偵錯程式 pdb 是基於 Bdb 構建的,後者是 Python 標準庫中專門用於構建偵錯程式的基類。
基於 Bdb 的簡易斷點偵錯程式看上去是這樣的:
import bdb
import inspect
class Debugger(bdb.Bdb):
def __init__(self):
Bdb.__init__(self)
self.breakpoints = dict()
self.set_trace()
def set_breakpoint(self, filename, lineno, method):
self.set_break(filename, lineno)
try :
self.breakpoints[(filename, lineno)].add(method)
except KeyError:
self.breakpoints[(filename, lineno)] = [method]
def user_line(self, frame):
if not self.break_here(frame):
return
# Get filename and lineno from frame
(filename, lineno, _, _, _) = inspect.getframeinfo(frame)
methods = self.breakpoints[(filename, lineno)]
for method in methods:
method(frame)
這個偵錯程式類的全部構成是:
- 繼承
Bdb
,定義一個簡單的建構函式來初始化基類,並開始跟蹤。 - 新增
set_breakpoint
方法,它使用Bdb
來設定斷點,並跟蹤這些斷點。 - 過載
Bdb
在當前使用者行呼叫的user_line
方法,該方法一定被一個斷點呼叫,之後獲取該斷點的源位置,並呼叫已註冊的斷點。
這個簡易的 Bdb 偵錯程式效率如何呢?
Rookout 的目標是在生產級效能的使用場景下提供接近普通偵錯程式的使用體驗。那麼,讓我們來看看先前構建出來的簡易偵錯程式表現的如何。
為了衡量偵錯程式的整體效能開銷,我們使用如下兩個簡單的函式來進行測試,它們分別在不同的情景下執行了 1600 萬次。請注意,在所有情景下斷點都不會被執行。
def empty_method():
pass
def simple_method():
a = 1
b = 2
c = 3
d = 4
e = 5
f = 6
g = 7
h = 8
i = 9
j = 10
在使用偵錯程式的情況下需要大量的時間才能完成測試。糟糕的結果指明瞭,這個簡陋 Bdb
偵錯程式的效能還遠不足以在生產環境中使用。
對偵錯程式進行優化
降低偵錯程式的額外開銷主要有三種方法:
- 儘可能的限制區域性跟蹤:由於每一行程式碼都可能包含大量事件,區域性跟蹤比全域性跟蹤的開銷要大得多。
- 優化
call
事件並儘快將控制權還給直譯器:在call
事件發生時偵錯程式的主要工作是判斷是否需要對該事件進行跟蹤。 - 優化
line
事件並儘快將控制權還給直譯器:在line
事件發生時偵錯程式的主要工作是判斷我們在此處是否需要設定一個斷點。
於是我們復刻了 Bdb
專案,精簡特徵、簡化程式碼,針對使用場景進行優化。這些工作雖然得到了一些效果,但仍無法滿足我們的需求。因此我們又繼續進行了其它的嘗試,將程式碼優化並遷移至 .pyx
使用 Cython 進行編譯,可惜結果(如下圖所示)依舊不夠理想。最終,我們在深入瞭解 CPython 原始碼之後意識到,讓跟蹤過程快到滿足生產需求是不可能的。
放棄 Bdb 轉而嘗試位元組碼操作
熬過先前對標準除錯方法進行的試驗-失敗-再試驗迴圈所帶來的失望,我們將目光轉向另一種選擇:位元組碼操作。
Python 直譯器的工作主要分為兩個階段:
- 將 Python 原始碼編譯成 Python 位元組碼:這種(對人類而言)不可讀的格式專為執行的效率而優化,它們通常快取在我們熟知的
.pyc
檔案當中。 - 遍歷 直譯器迴圈中的位元組碼: 在這一步中直譯器會逐條的執行指令。
我們選擇的模式是:使用位元組碼操作來設定沒有全域性額外開銷的不中斷斷點。這種方式的實現首先需要在記憶體中的位元組碼裡找到我們感興趣的部分,然後在該部分的相關機器指令前插入一個函式呼叫。如此一來,直譯器無需任何額外的工作即可實現我們的不中斷斷點。
這種方法並不依靠魔法來實現,讓我們簡要地舉個例子。
首先定義一個簡單的函式:
def multiply(a, b):
result = a * b
return result
在 inspect 模組(其包含了許多實用的單元)的文件裡,我們得知可以通過訪問 multiply.func_code.co_code
來獲取函式的位元組碼:
'|\x00\x00|\x01\x00\x14}\x02\x00|\x02\x00S'
使用 Python 標準庫中的 dis 模組可以翻譯這些不可讀的字串。呼叫 dis.dis(multiply.func_code.co_code)
之後,我們就可以得到:
4 0 LOAD_FAST 0 (a)
3 LOAD_FAST 1 (b)
6 BINARY_MULTIPLY
7 STORE_FAST 2 (result)
5 10 LOAD_FAST 2 (result)
13 RETURN_VALUE
與直截了當的解決方案相比,這種方法讓我們更靠近發生在偵錯程式背後的事情。可惜 Python 並沒有提供在直譯器中修改函式位元組碼的方法。我們可以對函式物件進行重寫,不過那樣做的效率滿足不了大多數實際的除錯場景。最後我們不得不採用一種迂迴的方式來使用原生擴充才能完成這一任務。
總結
在構建一個新工具時,總會學到許多事情的工作原理。這種刨根問底的過程能夠使你的思路跳出桎梏,從而得到意料之外的解決方案。
在 Rookout 團隊中構建不中斷斷點的這段時間裡,我學到了許多有關編譯器、偵錯程式、伺服器框架、併發模型等等領域的知識。如果你希望更深入的瞭解位元組碼操作,谷歌的開源專案 cloud-debug-python 為編輯位元組碼提供了一些工具。
via: https://opensource.com/article/19/8/debug-python
作者:Liran Haimovitch 選題:lujun9972 譯者:caiichenr 校對:wxy
訂閱“Linux 中國”官方小程式來檢視
相關文章
- AS斷點除錯斷點除錯
- Pycharm的斷點除錯PyCharm斷點除錯
- webstorm 斷點除錯WebORM斷點除錯
- Vscode斷點除錯VSCode斷點除錯
- phpstorm + xdebug 斷點除錯PHPORM斷點除錯
- vscode除錯使用斷點VSCode除錯斷點
- 除錯——條件斷點除錯斷點
- Windows PHPstorm xdebug 斷點除錯WindowsPHPORM斷點除錯
- 斷點除錯 debug模式 1006斷點除錯模式
- 【IDEA】2020 斷點(BreakPoints)除錯Idea斷點除錯
- 【前端除錯】- 斷點除錯的正確開啟方式前端除錯斷點
- Lua中如何實現類似gdb的斷點除錯—07支援通過函式名稱新增斷點斷點除錯函式
- 如何斷點除錯Tomcat原始碼斷點除錯Tomcat原始碼
- 除錯篇——斷點與單步除錯斷點
- 【Java】Debug斷點除錯常用技巧Java斷點除錯
- Xcode斷點除錯出現的問題XCode斷點除錯
- VS code中斷點除錯Vue CLI 3專案斷點除錯Vue
- VS - 打斷點/本地除錯/遠端除錯 問題斷點除錯
- VS Code + Homestead + Xdebug 斷點除錯配置斷點除錯
- vs斷點除錯unity安卓包斷點除錯Unity安卓
- VS斷點除錯簡單筆記斷點除錯筆記
- 斷點除錯之壓縮引發的血案斷點除錯
- vscode遠端連線docker容器打斷點除錯python專案VSCodeDocker斷點除錯Python
- Node.js 開發中熱更新配置和 vscode 中斷點除錯Node.jsVSCode斷點除錯
- 硬核除錯實操 | 手把手帶你實現 Serverless 斷點除錯除錯Server斷點
- 核心不中斷前提下,Gaussdb(DWS)記憶體報錯排查方法記憶體
- Homestead 下關於 PhpStorm Xdebug 斷點除錯工具的安裝PHPORM斷點除錯
- fiddler之設定斷點的學習記錄斷點
- MySQL GTID複製中斷修復過程MySql
- stm32 SysTick/EXTI/USART中斷過程
- Linux 核心處理中斷全過程解析Linux
- 自己動手實現java斷點/單步除錯(二)Java斷點除錯
- IDEA多執行緒下空指標斷點除錯Idea執行緒指標斷點除錯
- 自己動手實現java斷點/單步除錯(一)Java斷點除錯
- 詳解新支點雙機熱備如何保證業務的不中斷
- 除錯js碰到迴圈斷點(debugger),應該怎麼做?除錯JS斷點
- 輕鬆兩步,搭建斷點除錯 PHP 原始碼環境斷點除錯PHP原始碼
- Swift 首次除錯斷點慢的問題解法 | 優酷 Swift 實踐Swift除錯斷點