日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
在Python調(diào)試過(guò)程中設(shè)置不中斷的斷點(diǎn)

你對(duì)如何讓調(diào)試器變得更快產(chǎn)生過(guò)興趣嗎?本文將分享我們?cè)跒?Python 構(gòu)建調(diào)試器時(shí)得到的一些經(jīng)驗(yàn)。

整段故事講的是我們?cè)?Rookout 公司的團(tuán)隊(duì)為 Python 調(diào)試器開(kāi)發(fā)不中斷斷點(diǎn)的經(jīng)歷,以及開(kāi)發(fā)過(guò)程中得到的經(jīng)驗(yàn)。我將在本月于舊金山舉辦的 PyBay 2019 上介紹有關(guān) Python 調(diào)試過(guò)程的更多細(xì)節(jié),但現(xiàn)在就讓我們立刻開(kāi)始這段故事。

Python 調(diào)試器的心臟:sys.set_trace

在諸多可選的 Python 調(diào)試器中,使用最廣泛的三個(gè)是:

  • pdb,它是 Python 標(biāo)準(zhǔn)庫(kù)的一部分
  • PyDev,它是內(nèi)嵌在 Eclipse 和 Pycharm 等 IDE 中的調(diào)試器
  • ipdb,它是 IPython 的調(diào)試器

Python 調(diào)試器的選擇雖多,但它們幾乎都基于同一個(gè)函數(shù):sys.settrace。 值得一提的是, sys.settrace 可能也是 Python 標(biāo)準(zhǔn)庫(kù)中最復(fù)雜的函數(shù)。

set_trace Python 2 docs page

簡(jiǎn)單來(lái)講,settrace 的作用是為解釋器注冊(cè)一個(gè)跟蹤函數(shù),它在下列四種情形發(fā)生時(shí)被調(diào)用:

  • 函數(shù)調(diào)用
  • 語(yǔ)句執(zhí)行
  • 函數(shù)返回
  • 異常拋出

一個(gè)簡(jiǎn)單的跟蹤函數(shù)看上去大概是這樣:

 
 
 
 
  1. def simple_tracer(frame, event, arg):
  2.   co = frame.f_code
  3.   func_name = co.co_name
  4.   line_no = frame.f_lineno
  5.   print("{e} {f} {l}".format(
  6. e=event, f=func_name, l=line_no))
  7.   return simple_tracer

在分析函數(shù)時(shí)我們首先關(guān)注的是參數(shù)和返回值,該跟蹤函數(shù)的參數(shù)分別是:

  • frame,當(dāng)前堆棧幀,它是包含當(dāng)前函數(shù)執(zhí)行時(shí)解釋器里完整狀態(tài)的對(duì)象
  • event,事件,它是一個(gè)值可能為 call、line、return 或 exception 的字符串
  • arg,參數(shù),它的取值基于 event 的類型,是一個(gè)可選項(xiàng)

該跟蹤函數(shù)的返回值是它自身,這是由于解釋器需要持續(xù)跟蹤兩類跟蹤函數(shù):

  • 全局跟蹤函數(shù)(每線程):該跟蹤函數(shù)由當(dāng)前線程調(diào)用 sys.settrace 來(lái)設(shè)置,并在解釋器創(chuàng)建一個(gè)新的堆棧幀時(shí)被調(diào)用(即代碼中發(fā)生函數(shù)調(diào)用時(shí))。雖然沒(méi)有現(xiàn)成的方式來(lái)為不同的線程設(shè)置跟蹤函數(shù),但你可以調(diào)用 threading.settrace 來(lái)為所有新創(chuàng)建的 threading 模塊線程設(shè)置跟蹤函數(shù)。
  • 局部跟蹤函數(shù)(每一幀):解釋器將該跟蹤函數(shù)的值設(shè)置為全局跟蹤函數(shù)創(chuàng)建幀時(shí)的返回值。同樣也沒(méi)有現(xiàn)成的方法能夠在幀被創(chuàng)建時(shí)自動(dòng)設(shè)置局部跟蹤函數(shù)。

該機(jī)制的目的是讓調(diào)試器對(duì)被跟蹤的幀有更精確的把握,以減少對(duì)性能的影響。

簡(jiǎn)單三步構(gòu)建調(diào)試器 (我們最初的設(shè)想)

僅僅依靠上文提到的內(nèi)容,用自制的跟蹤函數(shù)來(lái)構(gòu)建一個(gè)真正的調(diào)試器似乎有些不切實(shí)際。幸運(yùn)的是,Python 的標(biāo)準(zhǔn)調(diào)試器 pdb 是基于 Bdb 構(gòu)建的,后者是 Python 標(biāo)準(zhǔn)庫(kù)中專門用于構(gòu)建調(diào)試器的基類。

基于 Bdb 的簡(jiǎn)易斷點(diǎn)調(diào)試器看上去是這樣的:

 
 
 
 
  1. import bdb
  2. import inspect
  3.  
  4. class Debugger(bdb.Bdb):
  5.   def __init__(self):
  6.       Bdb.__init__(self)
  7.       self.breakpoints = dict()
  8.       self.set_trace()
  9.  
  10. def set_breakpoint(self, filename, lineno, method):
  11.   self.set_break(filename, lineno)
  12.   try :
  13.       self.breakpoints[(filename, lineno)].add(method)
  14.   except KeyError:
  15.       self.breakpoints[(filename, lineno)] = [method]
  16.  
  17. def user_line(self, frame):
  18.   if not self.break_here(frame):
  19.       return
  20.  
  21.   # Get filename and lineno from frame
  22.   (filename, lineno, _, _, _) = inspect.getframeinfo(frame)
  23.  
  24.   methods = self.breakpoints[(filename, lineno)]
  25.   for method in methods:
  26.       method(frame)

這個(gè)調(diào)試器類的全部構(gòu)成是:

  1. 繼承 Bdb,定義一個(gè)簡(jiǎn)單的構(gòu)造函數(shù)來(lái)初始化基類,并開(kāi)始跟蹤。
  2. 添加 set_breakpoint 方法,它使用 Bdb 來(lái)設(shè)置斷點(diǎn),并跟蹤這些斷點(diǎn)。
  3. 重載 Bdb 在當(dāng)前用戶行調(diào)用的 user_line 方法,該方法一定被一個(gè)斷點(diǎn)調(diào)用,之后獲取該斷點(diǎn)的源位置,并調(diào)用已注冊(cè)的斷點(diǎn)。

這個(gè)簡(jiǎn)易的 Bdb 調(diào)試器效率如何呢?

Rookout 的目標(biāo)是在生產(chǎn)級(jí)性能的使用場(chǎng)景下提供接近普通調(diào)試器的使用體驗(yàn)。那么,讓我們來(lái)看看先前構(gòu)建出來(lái)的簡(jiǎn)易調(diào)試器表現(xiàn)的如何。

為了衡量調(diào)試器的整體性能開(kāi)銷,我們使用如下兩個(gè)簡(jiǎn)單的函數(shù)來(lái)進(jìn)行測(cè)試,它們分別在不同的情景下執(zhí)行了 1600 萬(wàn)次。請(qǐng)注意,在所有情景下斷點(diǎn)都不會(huì)被執(zhí)行。

 
 
 
 
  1. def empty_method():
  2.    pass
  3.  
  4. def simple_method():
  5.    a = 1
  6.    b = 2
  7.    c = 3
  8.    d = 4
  9.    e = 5
  10.    f = 6
  11.    g = 7
  12.    h = 8
  13.    i = 9
  14.    j = 10

在使用調(diào)試器的情況下需要大量的時(shí)間才能完成測(cè)試。糟糕的結(jié)果指明了,這個(gè)簡(jiǎn)陋 Bdb 調(diào)試器的性能還遠(yuǎn)不足以在生產(chǎn)環(huán)境中使用。

First Bdb debugger results

對(duì)調(diào)試器進(jìn)行優(yōu)化

降低調(diào)試器的額外開(kāi)銷主要有三種方法:

  1. 盡可能的限制局部跟蹤:由于每一行代碼都可能包含大量事件,局部跟蹤比全局跟蹤的開(kāi)銷要大得多。
  2. 優(yōu)化 call 事件并盡快將控制權(quán)還給解釋器:在 call 事件發(fā)生時(shí)調(diào)試器的主要工作是判斷是否需要對(duì)該事件進(jìn)行跟蹤。
  3. 優(yōu)化 line 事件并盡快將控制權(quán)還給解釋器:在 line 事件發(fā)生時(shí)調(diào)試器的主要工作是判斷我們?cè)诖颂幨欠裥枰O(shè)置一個(gè)斷點(diǎn)。

于是我們復(fù)刻了 Bdb 項(xiàng)目,精簡(jiǎn)特征、簡(jiǎn)化代碼,針對(duì)使用場(chǎng)景進(jìn)行優(yōu)化。這些工作雖然得到了一些效果,但仍無(wú)法滿足我們的需求。因此我們又繼續(xù)進(jìn)行了其它的嘗試,將代碼優(yōu)化并遷移至 .pyx 使用 Cython 進(jìn)行編譯,可惜結(jié)果(如下圖所示)依舊不夠理想。最終,我們?cè)谏钊肓私?CPython 源碼之后意識(shí)到,讓跟蹤過(guò)程快到滿足生產(chǎn)需求是不可能的。

Second Bdb debugger results

放棄 Bdb 轉(zhuǎn)而嘗試字節(jié)碼操作

熬過(guò)先前對(duì)標(biāo)準(zhǔn)調(diào)試方法進(jìn)行的試驗(yàn)-失敗-再試驗(yàn)循環(huán)所帶來(lái)的失望,我們將目光轉(zhuǎn)向另一種選擇:字節(jié)碼操作。

Python 解釋器的工作主要分為兩個(gè)階段:

  1. 將 Python 源碼編譯成 Python 字節(jié)碼:這種(對(duì)人類而言)不可讀的格式專為執(zhí)行的效率而優(yōu)化,它們通常緩存在我們熟知的 .pyc 文件當(dāng)中。
  2. 遍歷 解釋器循環(huán)中的字節(jié)碼: 在這一步中解釋器會(huì)逐條的執(zhí)行指令。

我們選擇的模式是:使用字節(jié)碼操作來(lái)設(shè)置沒(méi)有全局額外開(kāi)銷的不中斷斷點(diǎn)。這種方式的實(shí)現(xiàn)首先需要在內(nèi)存中的字節(jié)碼里找到我們感興趣的部分,然后在該部分的相關(guān)機(jī)器指令前插入一個(gè)函數(shù)調(diào)用。如此一來(lái),解釋器無(wú)需任何額外的工作即可實(shí)現(xiàn)我們的不中斷斷點(diǎn)。

這種方法并不依靠魔法來(lái)實(shí)現(xiàn),讓我們簡(jiǎn)要地舉個(gè)例子。

首先定義一個(gè)簡(jiǎn)單的函數(shù):

 
 
 
 
  1. def multiply(a, b):
  2.    result = a * b
  3.    return result

在 inspect 模塊(其包含了許多實(shí)用的單元)的文檔里,我們得知可以通過(guò)訪問(wèn) multiply.func_code.co_code 來(lái)獲取函數(shù)的字節(jié)碼:

 
 
 
 
  1. '|\x00\x00|\x01\x00\x14}\x02\x00|\x02\x00S'

使用 Python 標(biāo)準(zhǔn)庫(kù)中的 dis 模塊可以翻譯這些不可讀的字符串。調(diào)用 dis.dis(multiply.func_code.co_code) 之后,我們就可以得到:

 
 
 
 
  1.   4          0 LOAD_FAST               0 (a)
  2.              3 LOAD_FAST               1 (b)
  3.              6 BINARY_MULTIPLY    
  4.              7 STORE_FAST              2 (result)
  5.  
  6.   5         10 LOAD_FAST               2 (result)
  7.             13 RETURN_VALUE      

與直截了當(dāng)?shù)慕鉀Q方案相比,這種方法讓我們更靠近發(fā)生在調(diào)試器背后的事情??上?Python 并沒(méi)有提供在解釋器中修改函數(shù)字節(jié)碼的方法。我們可以對(duì)函數(shù)對(duì)象進(jìn)行重寫,不過(guò)那樣做的效率滿足不了大多數(shù)實(shí)際的調(diào)試場(chǎng)景。最后我們不得不采用一種迂回的方式來(lái)使用原生拓展才能完成這一任務(wù)。

總結(jié)

在構(gòu)建一個(gè)新工具時(shí),總會(huì)學(xué)到許多事情的工作原理。這種刨根問(wèn)底的過(guò)程能夠使你的思路跳出桎梏,從而得到意料之外的解決方案。

在 Rookout 團(tuán)隊(duì)中構(gòu)建不中斷斷點(diǎn)的這段時(shí)間里,我學(xué)到了許多有關(guān)編譯器、調(diào)試器、服務(wù)器框架、并發(fā)模型等等領(lǐng)域的知識(shí)。如果你希望更深入的了解字節(jié)碼操作,谷歌的開(kāi)源項(xiàng)目 cloud-debug-python 為編輯字節(jié)碼提供了一些工具。


本文標(biāo)題:在Python調(diào)試過(guò)程中設(shè)置不中斷的斷點(diǎn)
文章位置:http://www.5511xx.com/article/cocsodd.html