很少有人聽過 DTrace,它是隱藏在 OS 中的小寶藏。DTrace 是強大的 debug 工具 - 因為它擁有極其靈活的特性,並且因為與其它工具差異很大而可能相對不那麼有名。
許多時候你的 app 的真正的用戶或測試人員會看到一些意外的行為。DTrace 可以讓你無需重啟 app 就能夠在生產版本上回答關於 app 的任何問題。
動態追蹤
大概 10 年前,Sun Microsystems創建了 DTrace,它的名字是 Dynamic Trace 的縮寫。2007 年底,蘋果公司將它集成在自己的操作系統中。
DTrace 是一個提供了 zero disable cost 的動態追蹤框架,也就是說當代碼中的探針關閉時,不會有額外的資源消耗 - 即使在生產版本中我們也可以將探針留在代碼中。只有使用的時候才產生消耗。
DTrace 是動態的,也就是說我們可以將它附加在一個已經在運行的程序上,也可以不打斷程序將它剝離。不需要重新編譯或啟動。
本文我們將重點介紹如何使用 DTrace 檢查我們的程序,但值得注意的是 DTrace 是系統級的: 例如,一個單獨的腳本可以觀察到系統中所有進程的內存分配操作。可以查看 /usr/share/examples/DTTk 來深入了解一些非常好的例子。
OS X vs. iOS
正如你現在可能已經猜到的,DTrace 只能在 OS X 上運行。蘋果也在 iOS 上使用 DTrace,用以支持像 Instruments 這樣的工具,但對於第三方開發者,DTrace 只能運行於 OS X 或 iOS 模擬器。
在 Wire中,即使我們被限制僅能在 iOS 模擬器上使用 DTrace,它也在 iOS 開發中非常有用。如果你讀到本文並且認為在 iOS 設備上支持 DTrace 是個好提議,請提交 enhancement request 給蘋果。
探針和腳本
DTrace 有兩部分:DTrace 探針,及附加在上面的 DTrace 腳本。
探針
你可以將內置 (所謂靜態的) 探針加入代碼中。IA 探針看起來和普通的 C 函數非常相似。在 Wire,我們的同步代碼有一個內部狀態機器,我們定義了如下兩個探針:
provider syncengine_sync { probe strategy_go_to_state(int); }
探針被分組成所謂的 providers。參數 int 是正要進入的狀態。在我們的 Objective-C (或 Swift) 代碼中,簡單的插入以下代碼即可:
- (void)goToState:(ZMSyncState *)state { [self.currentState didLeaveState]; self.currentState = state; SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier); [self.currentState didEnterState]; }
我們後面會討論如何整合並且把流程說清楚一些。
腳本
現在我們可以編寫一個 DTrace 小腳本來展示狀態轉變:
syncengine_sync*:::strategy_go_to_state { printf("Transitioning to state %d\n", arg0); }
(後面我們會詳細展示 DTrace 腳本如何工作。)
如果將 DTrace 保存進 state.d,接下來我們可以使用 dtrace(1) 命令行工具 來運行它:
% sudo dtrace -q -s state.d
我們可以看到:
Transitioning to state 1 Transitioning to state 2 Transitioning to state 5
正如我們所預期的,並沒什麼讓人激動的。最後使用 ^C 可以退出 DTrace。
一個定時例子
因為 DTrace 消耗非常小,所以非常適合用來測試性能 - 即使需要測試的時間非常短。DTrace 中的時間單位是納秒。
如果擴展上面的小例子,我們可以輸出每個狀態所花費的時間:
uint64_t last_state; uint64_t last_state_timestamp; dtrace:::BEGIN { syncState[4] = "EventProcessing"; syncState[5] = "QuickSync1"; syncState[6] = "QuickSync2"; } syncengine_sync*:::strategy_go_to_state / last_state_timestamp != 0 / { t = (walltimestamp - last_state_timestamp) / 1000000; printf("Spent %d ms in state %s\n", t, syncState[last_state]); } syncengine_sync*:::strategy_go_to_state { printf("Transitioning to state %s\n", syncState[arg0]); last_state = arg0; last_state_timestamp = walltimestamp; }
這些代碼會輸出:
Transitioning to state QuickSync1 Spent 2205 ms in state QuickSync1 Transitioning to state QuickSync2 Spent 115 ms in state QuickSync2 Transitioning to state EventProcessing
腳本中有些新東西。dtrace:::BEGIN 語句在腳本開始時運行。腳本退出時有一個相應的 END。
我們還給第一個探針增加了一個斷言 (predicate),/ last_state_timestamp != 0 /。
最後我們使用全局變量來追蹤最後的狀態,以及什麼時候進入該狀態。
內置的 walltimestamp 變量返回當前時間相對於 Unix epoch 時間以來的納秒數。
還有一個虛擬的單位為納秒的時間戳變量,vtimestamp。它表示當前的線程在 CPU 上運行的時間減去在 DTrace 上花費的時間。最後,machtimestamp 對應 mach_absolute_time()。
對於上面的腳本,執行的順序非常重要。我們有兩個所謂的語句對應同一個探針,(syncengine_sync*:::strategy_go_to_state)。它們會按照在 D 程序中出現的順序執行。
結合系統探針
操作系統,尤其是 kernel,提供了數以千計的探針,被分成不同的提供者 (provider) 組。其中的很多在 Oracle 的 DTrace 文檔中可以找到。
通過下面的腳本,我們可以用 ip 提供者中的 send 探針來檢查轉換到下一個狀態之前通過網絡發送了多少字節:
uint64_t bytes_sent; syncengine_sync$target:::strategy_go_to_state { printf("Transitioning to state %d\n", arg0); printf("Sent %d bytes in previous state\n", bytes_sent); bytes_sent = 0; } ip:::send / pid == $target / { bytes_sent += args[2]->ip_plength; }
這次我們的目標為某個特定的進程 - ip:::send 會匹配系統的所有進程,而我們只對 Wire 進程感興趣。我們運行如下的腳本:
sudo dtrace -q -s sample-timing-3.d -p 198
這裡 198 是進程標識 (亦稱 PID)。我們可以在活動監視器這個 app 中找到這個數字,或者使用 ps(1) 命令行工具。
我們會得到:
Transitioning to state 6 Sent 2043 bytes in previous state Transitioning to state 4 Sent 581 bytes in previous state
D 語言
注意:這不是W. Bright 和 A. Alexandrescu 的 D 語言。
D 語言的大部分跟 C 語言都非常相似,但總體架構是不同的。每一個 Dtrace 腳本由多個所謂的探針語句組成。
在上面的例子中,我們已經看到了一些這種探針語句。它們都符合如下的形式:
probe descriptions / predicate / { action statements }
斷言 (predicate) 和動作語句 (action statement) 部分都是可選的。
探針描述
探針描述定義了語句匹配什麼探針。所有的部分都可以省略,形式如下:
provider:module:function:name
例如,syscall::: 匹配所有 syscall 提供者的探針。我們可以使用 * 匹配任何字符串,例如 syscall::*lwp*:entry 匹配所有 syscall 提供者的 entry,並且函數名字包含 lwp 的探針。
一個探針描述可以包含多個探針,例如:
syscall::*lwp*:entry, syscall::*sock*:entry { trace(timestamp); }
斷言
當動作語句開始運行時我們可以使用斷言來限制。當觸發特定的探針時斷言會被計算。如果斷言結果為非 0,action statements 將會運行,這和 C 語言中的 if 語句類似。
我們可以使用不同的斷言來判斷同一個探針多次。如果有多個匹配,它們將會按照在 D 程序中的出現的順序執行。
動作
動作包含在花括號中。D 語言是輕量,精悍而且簡單的語言。
D 不支持控制流,比如循環和分支。我們不能定義任何用戶函數。變量定義也是可選的。
這限制了我們能做的事情。但是一旦知道了一些常見的模式,這種簡單也給了我們很多靈活性,我們將在下一節詳細討論。在 D Programming Language 的指南中可以查看更多的細節。
常見 D 語言模式
下面的例子會給讓我們認識一些我們能做的事情。
這個例子統計了 App Store 應用在 syscall (也就是一個系統調用,或對 kernel 中進行的調用) 中累計使用的時間。
syscall:::entry / execname == "App Store" / { self->ts = timestamp; } syscall:::return / execname == "App Store" && self->ts != 0 / { @totals[probefunc] = sum(timestamp - self->ts); }
如果運行這個並且開啟 App Store 應用,然後用 ^C 退出 DTrace 腳本,可以得到像這樣的輸出:
dtrace: script 'app-store.d' matched 980 probes ^C __disable_threadsignal 2303 __pthread_sigmask 2438 psynch_cvclrprepost 3216 ftruncate 3663 bsdthread_register 3754 shared_region_check_np 3939 getpid 4189 getegid 4276 gettimeofday 4285 flock 4825 sigaltstack 4874 kdebug_trace 5430 kqueue 5860 workq_open 6155 sigprocmask 6188 setrlimit 7085 psynch_cvsignal 8909 [...] stat64 6451260 read 6657207 fsync 8231130 rename 8340468 open_nocancel 8856035 workq_kernreturn 15835068 getdirentries64 17978504 bsdthread_ctl 25418263 open 29503041 psynch_mutexwait 453338483 ioctl 1049412360 __semwait_signal 1373514528 select 1632760820 kevent64 3656884980
在這個例子中,App Store 在 kevent64 中花費了 3.6 秒。
這個腳本中有兩個特別有意思的事情:線程本地變量 (self->ts) 和集積 (aggregation)。
變量作用域 (scope)
D 語言有 3 種變量作用域: 全局,線程本地,以及探針語句本地。
foo 或 bar 這樣的全局變量在整個 D 語言中都是可見的。
線程本地變量命名為 self->foo,self->bar 等,並且存在與特定的線程中。
探針語句本地變量與 C 或 Swift 中的本地變量類似。對於中間結果來說很有用。
在這個腳本中,當進入 syscall 時我們使用第一個探針語句來匹配。我們將當前時間戳賦值給線程本地變量 self->ts:
syscall:::entry / execname == "App Store" / { self->ts = timestamp; }
第二個語句在從 syscall 中返回時匹配。這個調用將和進入時是同一個線程,因此可以確定,即使有多個線程在同一時間進行系統調用,self->ts 也具有我們所期待的值。
我們在謂詞裡加入了 self->ts != 0 來確保即使腳本是在應用處於系統調用中的時候被追加的,它也能正確運行。否則,timestamp - self->ts 將會是一個非常大的值,因為這時 self->ts 是還沒有被設置的初始值:
syscall:::return / execname == "App Store" && self->ts != 0 / { @totals[probefunc] = sum(timestamp - self->ts); }
通過 Dynamic Tracing Guide, “Variables.” 可以查看關於變量的核心知識。
集積 (Aggregation)
這行代碼使用了集積:
@totals[probefunc] = sum(timestamp - self->ts);
這是 DTrace 的一個極其強大的特性。
我們將 totals 稱為集積變量。變量名前面的 @ 將它轉變為集積行為。probefunc 是一個內置變量 - 它是探針函數的名字。對於 syscall 探針,probefunc 是正在運行的系統調用的名字。
sum 是集積函數。在這個例子中,該集積用來計算每一個 probefunc 對應的 timestamp - self->ts 的和。
DTrace Guide 展示了一個小例子,該例子使用集積來打印每秒鐘調用系統最多的 10 個應用的系統調用的數量。
#pragma D option quiet BEGIN { last = timestamp; } syscall:::entry { @func[execname] = count(); } tick-10sec { trunc(@func, 10); normalize(@func, (timestamp - last) / 1000000000); printa(@func); clear(@func); last = timestamp; }
在大多數空閒的 OS X 上,可能會顯示如下:
kextd 7 ntpd 8 mds_stores 19 cfprefsd 20 dtrace 20 UserEventAgent 34 launchd 42 Safari 109 cloudd 115 com.apple.WebKi 177 mds 8 Wire 8 Terminal 10 com.apple.iClou 15 dtrace 20 securityd 20 tccd 37 syncdefaultsd 98 Safari 109 com.apple.WebKi 212
我們看到 Safari,WebKit 和 cloudd 很活躍。
下表為所有集積函數:
Function Name | Result ------------------|--------- count | Number of times called sum | Sum of the passed in values avg | Average of the passed in values min | Smallest of the passed in values max | Largest of the passed in values lquantize | Linear frequency distribution quantize | Power-of-two frequency distribution
quantize 和 lquantize 函數可以給出一個關於傳入的數量的概覽:
ip:::send { @bytes_sent[execname] = quantize(args[2]->ip_plength); }
上面的代碼會輸出類似這樣的結果:
discoveryd value ------------- Distribution ------------- count 16 | 0 32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2 64 | 0 syncdefaultsd value ------------- Distribution ------------- count 256 | 0 512 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4 1024 | 0 kernel_task value ------------- Distribution ------------- count 8 | 0 16 |@@@@@@@@@@@@@@ 37 32 |@@@@@@@@@@@@@@@@@@@@@@@@@@ 67 64 | 0 com.apple.WebKi value ------------- Distribution ------------- count 16 | 0 32 |@@@@@@@@@@@@@@@@ 28 64 |@@@@ 7 128 |@@@@ 6 256 | 0 512 |@@@@@@@@@@@@@@@@ 27 1024 | 0
查看 Dynamic Tracing Guide 的示例來了解如何使用 lquantize。
聯合數組
不管名字如何,D 語言中的數組更類似 Swift 或 Objective-C 中的字典。另外,它們都是可變的。
我們可以這樣定義一個聯合數組:
int x[unsigned long long, char];
然後我們可以給它賦值:
BEGIN { x[123ull, ’a’] = 456; }
對於 Wire 應用,我們想要追蹤 NSURLSessionTask 實例的往復時間。當開始一個任務時,我們觸發一個靜態定義的探針,當完成時還有另一個探針。我們可以寫一個簡單的腳本:
syncengine_sync$target:::operation_loop_enqueue / arg0 == 4 / { start_transport_request_timestamp[arg1] = timestamp; } syncengine_sync$target:::operation_loop_enqueue / arg0 == 6 && start_transport_request_timestamp[arg1] != 0 / { @time["time for transport request round-trip"] = quantize(timestamp - start_transport_request_timestamp[arg1]); }
我們傳入 taskIdentifer 作為 arg1,任務開始時 arg0 被設置為 4,任務完成時被設置為 6。
正如我們在第一個定時的例子中看到的那樣,聯合數組在為傳入語句的 enum 值提供描述時也非常有用。
探針和提供者
讓我們回過頭看看可用的探針。
可以使用如下的命令來獲得一個所有可用探針的列表:
sudo dtrace -l | awk '{ match($2, "([a-z,A-Z]*)"); print substr($2, RSTART, RLENGTH); }' | sort -u
在 OS X 10.10 中有 79 個提供者。其中許多都與 kernel 和系統調用相關。
其中一些提供者是 Dynamic Tracing Guide 文檔中的原始集合中的一部分。讓我們看看其中一些我們可用的。
dtrace 提供者
我們之前提到過 BEGIN 和 END 探針。當以安靜模式運行 DTrace 時,dtrace:::END 對於輸出摘要尤其有用。錯誤發生時還有 ERROR 探針。
profile 提供者
profile 提供者可以用來在某種程度上采樣,這對於 Instruments 的用戶來說應該非常熟悉。
我們可以以 1001 赫茲的頻率來采樣棧深度:
profile-1001 /pid == $1/ { @proc[execname] = lquantize(stackdepth, 0, 20, 1); }
輸出會是這樣:
Safari value ------------- Distribution ------------- count < 0 | 0 0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 704 1 |@ 12 2 |@@ 30 3 |@ 17 4 | 7 5 | 6 6 | 1 7 | 2 8 | 1 9 | 7 10 | 5 11 | 1 12 | 0
類似的,tick- 探針會每隔固定的時間間隔,以很高打斷的級別觸發。profile- 探針會在所有 CPU 上觸發,而 tick- 每個間隔只會在一個 CPU 上。我們在上面的集積例子中使用了 tick-10sec 。
pid 提供者
pid 是一個有點野蠻的提供者。大多數時候,我們真的應該使用下面將要提到的靜態探針。
pid 是進程標識 (process identifier) 的縮寫。它可以讓我們在進入和退出進程時進行探測。這在大多數情況下是可行的。注意函數的進入和返回並不總是可以很好地界定,尤其是在尾調用優化 (tail-call optimization)時。另外還有某些函數並不需要創建棧幀等等情況。
當你不能改變代碼來增加靜態探針時,pid 是一個強大的工具。
你可以追蹤任何可見的函數。例如這個探針:
pid123:libSystem:printf:return
這個探針會附加到進程標識 (PID) 為 123 的進程中的 printf 函數。
objc 提供者
與 pid 提供者直接對應的是 objc 提供者。它為 Objective-C 方法的進入和退出提供了探針。還是使用靜態探針可以提供更好的靈活性。
objc 探針的格式如下:
objcpid:[class-name[(category-name)]]:[[+|-]method-name]:[name]
舉個例子:
objc207:NSTableView:-*:entry
將匹配進程號 207 中的 NSTableView 的所有實例方法條目。因為冒號 (:) 在 DTrace 中表示探針的指定方案,因此 Objective-C 中方法名裡的冒號需要用一個問號 (?) 來替代。比如要匹配 -[NSDate dateByAddingTimeInterval:] 的話,可以這麼寫:
objc207:NSDate:-dateByAddingTimeInterval?:entry
通過查看 dtrace(1) 幫助頁面可以獲得更多詳細信息。
io 提供者
為了追蹤與磁盤輸入輸出相關的活動,io 提供者 定義了 6 個探針:
start done wait-start wait-done journal-start journal-done
Oracle 文檔中的例子展示了如何使用:
#pragma D option quiet BEGIN { printf("s %58s %2s\n", "DEVICE", "FILE", "RW"); } io:::start { printf("s %58s %2s\n", args[1]->dev_statname, args[2]->fi_pathname, args[0]->b_flags & B_READ ? "R" : "W"); }
上面的例子會輸出類似這樣的結果:
?? ??/com.apple.Safari.savedState/data.data R ?? ??/Preferences/com.apple.Terminal.plist.kn0E7LJ W ?? ??/vm/swapfile0 R ?? ??/Preferences/com.apple.Safari.plist.jEQRQ5N W ?? ??/Preferences/com.apple.HIToolbox.plist.yBPXSnY W ?? ??/fsCachedData/F2BF76DB-740F-49AF-94DC-71308E08B474 W ?? ??/com.apple.Safari/Cache.db-wal W ?? ??/com.apple.Safari/Cache.db-wal W ?? ??/fsCachedData/88C00A4D-4D8E-4DD8-906E-B1796AC949A2 W
ip 提供者
ip 提供者有 send 和 receive 兩個探針。任何時候數據通過 IP 被發送或接收都會觸發。參數 arg0 到 arg5 提供了與發送或接收的 IP 包所相關的 kernel 結構體的訪問入口。
可以將二者放入非常強大的網絡調試工具中。它可以使 tcpdump(1) 的看起來像過時的玩意。ip 提供者可以讓我們在需要的時候精確的輸出我們所需要的信息。
查看文檔獲得更多很棒的示例。
定義自己的靜態探針
DTrace 允許我們創建自己的探針,通過這個,我們可以為我們自己的 app 釋放 DTrace 的真正威力。
這些在 DTrace 中被稱作靜態探針。我們在第一個例子中曾經簡短的提到過。Wire 定義了自己的提供者和探針:
provider syncengine_sync { probe strategy_go_to_state(int); }
然後我們在代碼中調用探針:
- (void)goToState:(ZMSyncState *)state { [self.currentState didLeaveState]; self.currentState = state; SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier); [self.currentState didEnterState]; }
一個可能的爭論就是我們本來可以干脆直接使用 objc 提供者;使用我們自己的探針可以更具靈活性。以後我們可以修改 Objective-C 代碼而不影響 DTrace 探針。
另外,靜態探針給我們提供更方便的參數訪問方式。通過上面我們可以看到我們如何利用它來追蹤時間和輸出日志。
DTrace 靜態探針的強大之處在於給我們提供了穩定的接口來調試我們的代碼,並且即便是在生產代碼中這個接口也是存在的。即使對於應用的生產版本,當有人看到奇怪的行為,我們也可以給正在運行的應用附加一段 DTrace 腳本。DTrace 的靈活性還可以讓我們將同一個探針用於其他目的。
我們可以將 DTrace 作為日志工具使用。還可以用來收集與時間,網絡,請求等有關的詳細的量化信息。
我們可以將探針留在生產代碼中的原因是探針是零損耗的,或者公平點說,相當於一個測試和分支的 CPU 指令。
下面來看看如何將靜態探針加入到我們的工程。
提供者描述
首先我們需要創建一個 .d 文件並定義提供者和探針。如果我們創建了一個 provider.d 文件並寫入以下內容,會得到兩個提供者:
provider syncengine_sync { probe operation_loop_enqueue(int, int, intptr_t); probe operation_loop_push_channel_data(int, int); probe strategy_go_to_state(int); probe strategy_leave_state(int); probe strategy_update_event(int, int); probe strategy_update_event_string(int, char *); }; provider syncengine_ui { probe notification(int, intptr_t, char *, char *, int, int, int, int); };
提供者是 syncengine_sync 和 syncengine_ui。在每個提供者中,我們定義了一組探針。
創建頭文件
現在我們需要將 provider.d 加入到 Xcode 的構建目標中。確保將類型設置為 DTrace source,這十分重要。Xcode 現在會在構建時自動處理。在這個步驟中,DTrace 會創建一個對應的 provider.h 頭文件,我們可以引入它。將 provider.d 同時加入 Xcode 工程和相應的構建目標非常重要。
在處理時,Xcode 會調用 dtrace(1) 工具:
dtrace -h -s provider.d
這會生成相應的頭文件。該文件最後會出現在 DERIVED_FILE_DIR 中。可以通過以下方式在任何工程內的源文件中引用
#import "provider.h"
Xcode 有一個內置的所謂 build rule 來處理 DTrace 提供者描述。比較 objc.io Build 過程的內容來獲取關於構建規則和構建處理的更多的信息。
增加探針
對於每一個靜態探針,頭文件會包含兩個宏:
PROVIDER_PROBENAME() PROVIDER_PROBENAME_ENABLED()
第一個是探針本身。第二個會在探針關閉時取值為 0。
DTrace 探針自己本身在沒被啟用時是零消耗的,也就是說只要沒有附加在探針上的東西,它們就不會產生消耗。然而有時,我們可能想要提前判斷或將數據發送給探針之前做一些預處理。在這些不太常見的情況下,我們可以使用 _ENABLED() 宏:
if (SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE_ENABLED()) { argument = /* Expensive argument calculation code here */; SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(argument); };
包裝 DTrace 探針
代碼可讀性非常重要。隨著增加越來越多的探針到代碼中,我們需要確保代碼不會因此而變得亂七八糟和難以理解。畢竟探針的目的是幫助我們而不是將事情變得更復雜。
我們所需要做的就是增加另一個簡單的包裝器。這些包裝器既使代碼的可讀性更好了一些,也增加了 if (…_ENABLED*()) 檢查。
回到狀態機器的例子中,我們的探針宏是這樣的:
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state);
為了使它變得更簡單,我們創建另一個頭文件並定義:
static inline void ZMTraceSyncStrategyGoToState(int d) { SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(d); }
有了這個,之後我們就調用:
ZMTraceSyncStrategyGoToState(state);
這看起來有點取巧,但是駝峰式命名確實能在混合普通的 Objective-C 和 Swift 代碼風格方面做得更好。
更進一步的,如果我們看到上面定義的
provider syncengine_ui { probe notification(int, intptr_t, char *, char *, int, int, int, int); };
有一長串的參數。在 Wire 中,我們用這個來記錄 UI 通知日志。
這個探針有一長串的參數。我們決定只要一個探針來處理許多不同通知,這些通知都是同步代碼發給 UI 用以提醒變化的。第一個參數,arg0,定義了是什麼通知,第二個參數定義了 NSNotification 的 object。在這使得我們的 DTrace 腳本可以將感興趣的范圍限定在某幾個指定的通知中。
剩余的參數定義根據預先的通知不同可以稍微寬松些,而且對於各個單獨情況我們有多個包裝函數。當想要傳入兩個 NSUUID 對象的情況,我們類似這樣來調用包裝函數:
ZMTraceUserInterfaceNotification_UUID(1, note.object, conversation.remoteIdentifier, participant.user.remoteIdentifier, wasJoined, participant.isJoined, currentState.connectionState, 0);
這個包裝函數是這樣定義的:
static inline void ZMTraceUserInterfaceNotification_UUID(int d, NSObject *obj, NSUUID *remoteID1, NSUUID *remoteID2, int e, int f, int g, int h) { if (SYNCENGINE_UI_NOTIFICATION_ENABLED()) { SYNCENGINE_UI_NOTIFICATION(d, (intptr_t) (__bridge void *) obj, remoteID1.transportString.UTF8String, remoteID2.transportString.UTF8String, e, f, g, h); } }
正如之前提到的,我們有兩個目的。第一,不讓類似 (intptr_t) (__bridge void *) 這樣的代碼把我們的代碼搞的亂七八糟。另外,除非因為附加到探針的時候有需要,其他時候我們無需花費 CPU 周期將一個 NSUUID 轉換為 NSString 並進一步轉換為 char const *。
根據這個模式,我們可以定義多個包裝器 / 輔助函數來復用相同的 DTrace 探針。
DTrace 和 Swift
像這樣包裝 DTrace 探針可以讓我們整合 Swift 和 DTrace 靜態探針。static line 函數現在可以在 Swift 代碼中直接調用。
func goToState(state: ZMSyncState) { currentState.didLeaveState() currentState = state currentState.didEnterState() ZMTraceSyncStrategyGoToState(state.identifier) }
DTrace 如何工作
D 語言是編譯型語言。當運行 dtrace(1) 工具時,我們傳入的腳本被編譯成字節碼。接著字節碼被傳入 kernel。在 kernel 中有一個解釋器來運行這些字節碼。
這就是為什麼這種編程語言可以保持簡單。沒人希望 DTrace 腳本中的 bug 引起 kernel 的死循環並導致系統掛起。
當將靜態探針加入可執行程序 (一個 app 或 framework),它們被作為 S_DTRACE_DOF (Dtrace Object Format) 部分被加入,並且在程序運行時被加載進 kernel。這樣 DTrace 就知道當前的靜態探針。
最後的話
毫無疑問 DTrace 非常強大和靈活。然而需要注意的是 DTrace 並不是一些經過考驗和真正的工具的替代品,如 malloc_history,heap 等。記得始終使用正確的工具。
另外,DTrace 並不是魔法。你仍然需要知道你要解決的問題所在。
這就是說,DTrace 可以使你的開發技能和能力達到一個新的水准。它可以讓你在生產代碼中追蹤那些很難或不可能定位的問題。
如果你的代碼中有 #ifdef TRACING 或 #if LOG_LEVEL == 1,使用 DTrace 替換它們或許是很好的主意。
記得查看 Dynamic Tracing Guide (PDF版本)。並且在你系統的 /usr/share/examples/DTTk 文件夾中獲取更多的靈感。
調試快樂!