很少有人聽過 DTrace,它是隱藏在 OS 中的小寶藏。DTrace 是強(qiáng)大的 debug 工具 - 因?yàn)樗鼡碛袠O其靈活的特性,并且因?yàn)榕c其它工具差異很大而可能相對(duì)不那么有名。
許多時(shí)候你的 app 的真正的用戶或測(cè)試人員會(huì)看到一些意外的行為。DTrace 可以讓你無需重啟 app 就能夠在生產(chǎn)版本上回答關(guān)于 app 的任何問題。
大概 10 年前,Sun Microsystems 創(chuàng)建了 DTrace,它的名字是 Dynamic Trace 的縮寫。2007 年底,蘋果公司將它集成在自己的 操作系統(tǒng) 中。
DTrace 是一個(gè)提供了 zero disable cost 的動(dòng)態(tài)追蹤框架,也就是說當(dāng)代碼中的探針關(guān)閉時(shí),不會(huì)有額外的資源消耗 - 即使在生產(chǎn)版本中我們也可以將探針留在代碼中。只有使用的時(shí)候才產(chǎn)生消耗。
DTrace 是動(dòng)態(tài)的,也就是說我們可以將它附加在一個(gè)已經(jīng)在運(yùn)行的程序上,也可以不打斷程序?qū)⑺鼊冸x。不需要重新編譯或啟動(dòng)。
本文我們將重點(diǎn)介紹如何使用 DTrace 檢查我們的程序,但值得注意的是 DTrace 是系統(tǒng)級(jí)的: 例如,一個(gè)單獨(dú)的腳本可以觀察到系統(tǒng)中所有進(jìn)程的內(nèi)存分配操作。可以查看 /usr/share/examples/DTTk
來深入了解一些非常好的例子。
正如你現(xiàn)在可能已經(jīng)猜到的,DTrace 只能在 OS X 上運(yùn)行。蘋果也在 iOS 上使用 DTrace,用以支持像 Instruments 這樣的工具,但對(duì)于第三方開發(fā)者,DTrace 只能運(yùn)行于 OS X 或 iOS 模擬器。
在 Wire,即使我們被限制僅能在 iOS 模擬器上使用 DTrace,它也在 iOS 開發(fā)中非常有用。如果你讀到本文并且認(rèn)為在 iOS 設(shè)備上支持 DTrace 是個(gè)好提議,請(qǐng)?zhí)峤?enhancement request 給蘋果。
DTrace 有兩部分:DTrace 探針,及附加在上面的 DTrace 腳本。
你可以將內(nèi)置 (所謂靜態(tài)的) 探針加入代碼中。IA 探針看起來和普通的 C 函數(shù)非常相似。在 Wire,我們的同步代碼有一個(gè)內(nèi)部狀態(tài)機(jī)器,我們定義了如下兩個(gè)探針:
provider syncengine_sync {
probe strategy_go_to_state(int);
}
探針被分組成所謂的 providers。參數(shù) int
是正要進(jìn)入的狀態(tài)。在我們的 Objective-C (或 Swift) 代碼中,簡單的插入以下代碼即可:
- (void)goToState:(ZMSyncState *)state
{
[self.currentState didLeaveState];
self.currentState = state;
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier);
[self.currentState didEnterState];
}
我們后面會(huì)討論如何整合并且把流程說清楚一些。
現(xiàn)在我們可以編寫一個(gè) DTrace 小腳本來展示狀態(tài)轉(zhuǎn)變:
syncengine_sync*:::strategy_go_to_state
{
printf("Transitioning to state %d\n", arg0);
}
(后面我們會(huì)詳細(xì)展示 DTrace 腳本如何工作。)
如果將 DTrace 保存進(jìn) state.d
,接下來我們可以使用 dtrace(1)
命令行工具 來運(yùn)行它:
% sudo dtrace -q -s state.d
我們可以看到:
Transitioning to state 1
Transitioning to state 2
Transitioning to state 5
正如我們所預(yù)期的,并沒什么讓人激動(dòng)的。最后使用 ^C
可以退出 DTrace。
因?yàn)?DTrace 消耗非常小,所以非常適合用來測(cè)試性能 - 即使需要測(cè)試的時(shí)間非常短。DTrace 中的時(shí)間單位是納秒。
如果擴(kuò)展上面的小例子,我們可以輸出每個(gè)狀態(tài)所花費(fèi)的時(shí)間:
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;
}
這些代碼會(huì)輸出:
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
語句在腳本開始時(shí)運(yùn)行。腳本退出時(shí)有一個(gè)相應(yīng)的 END
。
我們還給第一個(gè)探針增加了一個(gè)斷言 (predicate),/ last_state_timestamp != 0 /
。
最后我們使用全局變量來追蹤最后的狀態(tài),以及什么時(shí)候進(jìn)入該狀態(tài)。
內(nèi)置的 walltimestamp
變量返回當(dāng)前時(shí)間相對(duì)于 Unix epoch 時(shí)間以來的納秒數(shù)。
還有一個(gè)虛擬的單位為納秒的時(shí)間戳變量,vtimestamp
。它表示當(dāng)前的線程在 CPU 上運(yùn)行的時(shí)間減去在 DTrace 上花費(fèi)的時(shí)間。最后,machtimestamp
對(duì)應(yīng) mach_absolute_time()
。
對(duì)于上面的腳本,執(zhí)行的順序非常重要。我們有兩個(gè)所謂的語句對(duì)應(yīng)同一個(gè)探針,(syncengine_sync*:::strategy_go_to_state
)。它們會(huì)按照在 D 程序中出現(xiàn)的順序執(zhí)行。
操作系統(tǒng),尤其是 kernel,提供了數(shù)以千計(jì)的探針,被分成不同的提供者 (provider) 組。其中的很多在 Oracle 的 DTrace 文檔中可以找到。
通過下面的腳本,我們可以用 ip
提供者中的 send
探針來檢查轉(zhuǎn)換到下一個(gè)狀態(tài)之前通過網(wǎng)絡(luò)發(fā)送了多少字節(jié):
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;
}
這次我們的目標(biāo)為某個(gè)特定的進(jìn)程 - ip:::send
會(huì)匹配系統(tǒng)的所有進(jìn)程,而我們只對(duì) Wire
進(jìn)程感興趣。我們運(yùn)行如下的腳本:
sudo dtrace -q -s sample-timing-3.d -p 198
這里 198
是進(jìn)程標(biāo)識(shí) (亦稱 PID)。我們可以在活動(dòng)監(jiān)視器這個(gè) app 中找到這個(gè)數(shù)字,或者使用 ps(1)
命令行工具。
我們會(huì)得到:
Transitioning to state 6
Sent 2043 bytes in previous state
Transitioning to state 4
Sent 581 bytes in previous state
注意:這不是W. Bright 和 A. Alexandrescu 的 D 語言。
D 語言的大部分跟 C 語言都非常相似,但總體架構(gòu)是不同的。每一個(gè) Dtrace 腳本由多個(gè)所謂的探針語句組成。
在上面的例子中,我們已經(jīng)看到了一些這種探針語句。它們都符合如下的形式:
probe descriptions
/ predicate /
{
action statements
}
斷言 (predicate) 和動(dòng)作語句 (action statement) 部分都是可選的。
探針描述定義了語句匹配什么探針。所有的部分都可以省略,形式如下:
provider:module:function:name
例如,syscall:::
匹配所有 syscall
提供者的探針。我們可以使用 *
匹配任何字符串,例如 syscall::*lwp*:entry
匹配所有 syscall
提供者的 entry
,并且函數(shù)名字包含 lwp
的探針。
一個(gè)探針描述可以包含多個(gè)探針,例如:
syscall::*lwp*:entry, syscall::*sock*:entry
{
trace(timestamp);
}
當(dāng)動(dòng)作語句開始運(yùn)行時(shí)我們可以使用斷言來限制。當(dāng)觸發(fā)特定的探針時(shí)斷言會(huì)被計(jì)算。如果斷言結(jié)果為非 0,action statements 將會(huì)運(yùn)行,這和 C 語言中的 if
語句類似。
我們可以使用不同的斷言來判斷同一個(gè)探針多次。如果有多個(gè)匹配,它們將會(huì)按照在 D 程序中的出現(xiàn)的順序執(zhí)行。
動(dòng)作包含在花括號(hào)中。D 語言是輕量,精悍而且簡單的語言。
D 不支持控制流,比如循環(huán)和分支。我們不能定義任何用戶函數(shù)。變量定義也是可選的。
這限制了我們能做的事情。但是一旦知道了一些常見的模式,這種簡單也給了我們很多靈活性,我們將在下一節(jié)詳細(xì)討論。在 D Programming Language 的指南中可以查看更多的細(xì)節(jié)。
下面的例子會(huì)給讓我們認(rèn)識(shí)一些我們能做的事情。
這個(gè)例子統(tǒng)計(jì)了 App Store 應(yīng)用在 syscall (也就是一個(gè)系統(tǒng)調(diào)用,或?qū)?kernel 中進(jìn)行的調(diào)用) 中累計(jì)使用的時(shí)間。
syscall:::entry
/ execname == "App Store" /
{
self->ts = timestamp;
}
syscall:::return
/ execname == "App Store" && self->ts != 0 /
{
@totals[probefunc] = sum(timestamp - self->ts);
}
如果運(yùn)行這個(gè)并且開啟 App Store 應(yīng)用,然后用 ^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
在這個(gè)例子中,App Store 在 kevent64
中花費(fèi)了 3.6 秒。
這個(gè)腳本中有兩個(gè)特別有意思的事情:線程本地變量 (self->ts
) 和集積 (aggregation)。
D 語言有 3 種變量作用域: 全局,線程本地,以及探針語句本地。
foo
或 bar
這樣的全局變量在整個(gè) D 語言中都是可見的。
線程本地變量命名為 self->foo
,self->bar
等,并且存在與特定的線程中。
探針語句本地變量與 C 或 Swift 中的本地變量類似。對(duì)于中間結(jié)果來說很有用。
在這個(gè)腳本中,當(dāng)進(jìn)入 syscall 時(shí)我們使用第一個(gè)探針語句來匹配。我們將當(dāng)前時(shí)間戳賦值給線程本地變量 self->ts
:
syscall:::entry
/ execname == "App Store" /
{
self->ts = timestamp;
}
第二個(gè)語句在從 syscall 中返回時(shí)匹配。這個(gè)調(diào)用將和進(jìn)入時(shí)是同一個(gè)線程,因此可以確定,即使有多個(gè)線程在同一時(shí)間進(jìn)行系統(tǒng)調(diào)用,self->ts
也具有我們所期待的值。
我們?cè)谥^詞里加入了 self->ts != 0
來確保即使腳本是在應(yīng)用處于系統(tǒng)調(diào)用中的時(shí)候被追加的,它也能正確運(yùn)行。否則,timestamp - self->ts
將會(huì)是一個(gè)非常大的值,因?yàn)檫@時(shí) self->ts
是還沒有被設(shè)置的初始值:
syscall:::return
/ execname == "App Store" && self->ts != 0 /
{
@totals[probefunc] = sum(timestamp - self->ts);
}
通過 Dynamic Tracing Guide, “Variables.” 可以查看關(guān)于變量的核心知識(shí)。
這行代碼使用了集積:
@totals[probefunc] = sum(timestamp - self->ts);
這是 DTrace 的一個(gè)極其強(qiáng)大的特性。
我們將 totals
稱為集積變量。變量名前面的 @
將它轉(zhuǎn)變?yōu)榧e行為。probefunc
是一個(gè)內(nèi)置變量 - 它是探針函數(shù)的名字。對(duì)于 syscall
探針,probefunc
是正在運(yùn)行的系統(tǒng)調(diào)用的名字。
sum
是集積函數(shù)。在這個(gè)例子中,該集積用來計(jì)算每一個(gè) probefunc
對(duì)應(yīng)的 timestamp - self->ts
的和。
DTrace Guide 展示了一個(gè)小例子,該例子使用集積來打印每秒鐘調(diào)用系統(tǒng)最多的 10 個(gè)應(yīng)用的系統(tǒng)調(diào)用的數(shù)量。
#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;
}
在大多數(shù)空閑的 OS X 上,可能會(huì)顯示如下:
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
很活躍。
下表為所有集積函數(shù):
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
函數(shù)可以給出一個(gè)關(guān)于傳入的數(shù)量的概覽:
ip:::send
{
@bytes_sent[execname] = quantize(args[2]->ip_plength);
}
上面的代碼會(huì)輸出類似這樣的結(jié)果:
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 語言中的數(shù)組更類似 Swift 或 Objective-C 中的字典。另外,它們都是可變的。
我們可以這樣定義一個(gè)聯(lián)合數(shù)組:
int x[unsigned long long, char];
然后我們可以給它賦值:
BEGIN
{
x[123ull, ’a’] = 456;
}
對(duì)于 Wire 應(yīng)用,我們想要追蹤 NSURLSessionTask 實(shí)例的往復(fù)時(shí)間。當(dāng)開始一個(gè)任務(wù)時(shí),我們觸發(fā)一個(gè)靜態(tài)定義的探針,當(dāng)完成時(shí)還有另一個(gè)探針。我們可以寫一個(gè)簡單的腳本:
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
,任務(wù)開始時(shí) arg0
被設(shè)置為 4,任務(wù)完成時(shí)被設(shè)置為 6。
正如我們?cè)诘谝粋€(gè)定時(shí)的例子中看到的那樣,聯(lián)合數(shù)組在為傳入語句的 enum
值提供描述時(shí)也非常有用。
讓我們回過頭看看可用的探針。
可以使用如下的命令來獲得一個(gè)所有可用探針的列表:
sudo dtrace -l | awk '{ match($2, "([a-z,A-Z]*)"); print substr($2, RSTART, RLENGTH); }' | sort -u
在 OS X 10.10 中有 79 個(gè)提供者。其中許多都與 kernel 和系統(tǒng)調(diào)用相關(guān)。
其中一些提供者是 Dynamic Tracing Guide 文檔中的原始集合中的一部分。讓我們看看其中一些我們可用的。
dtrace
提供者我們之前提到過 BEGIN
和 END
探針。當(dāng)以安靜模式運(yùn)行 DTrace 時(shí),dtrace:::END
對(duì)于輸出摘要尤其有用。錯(cuò)誤發(fā)生時(shí)還有 ERROR
探針。
profile
提供者profile
提供者可以用來在某種程度上采樣,這對(duì)于 Instruments 的用戶來說應(yīng)該非常熟悉。
我們可以以 1001 赫茲的頻率來采樣棧深度:
profile-1001
/pid == $1/
{
@proc[execname] = lquantize(stackdepth, 0, 20, 1);
}
輸出會(huì)是這樣:
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-
探針會(huì)每隔固定的時(shí)間間隔,以很高打斷的級(jí)別觸發(fā)。profile-
探針會(huì)在所有 CPU 上觸發(fā),而 tick-
每個(gè)間隔只會(huì)在一個(gè) CPU 上。我們?cè)谏厦娴?a href="#Aggregations">集積例子中使用了 tick-10sec
。
pid
提供者pid
是一個(gè)有點(diǎn)野蠻的提供者。大多數(shù)時(shí)候,我們真的應(yīng)該使用下面將要提到的靜態(tài)探針。
pid
是進(jìn)程標(biāo)識(shí) (process identifier) 的縮寫。它可以讓我們?cè)谶M(jìn)入和退出進(jìn)程時(shí)進(jìn)行探測(cè)。這在大多數(shù)情況下是可行的。注意函數(shù)的進(jìn)入和返回并不總是可以很好地界定,尤其是在尾調(diào)用優(yōu)化 (tail-call optimization)時(shí)。另外還有某些函數(shù)并不需要?jiǎng)?chuàng)建棧幀等等情況。
當(dāng)你不能改變代碼來增加靜態(tài)探針時(shí),pid
是一個(gè)強(qiáng)大的工具。
你可以追蹤任何可見的函數(shù)。例如這個(gè)探針:
pid123:libSystem:printf:return
這個(gè)探針會(huì)附加到進(jìn)程標(biāo)識(shí) (PID) 為 123 的進(jìn)程中的 printf
函數(shù)。
objc
提供者與 pid
提供者直接對(duì)應(yīng)的是 objc
提供者。它為 Objective-C 方法的進(jìn)入和退出提供了探針。還是使用靜態(tài)探針可以提供更好的靈活性。
objc
探針的格式如下:
objcpid:[class-name[(category-name)]]:[[+|-]method-name]:[name]
舉個(gè)例子:
objc207:NSTableView:-*:entry
將匹配進(jìn)程號(hào) 207 中的 NSTableView
的所有實(shí)例方法條目。因?yàn)槊疤?hào) (:
) 在 DTrace 中表示探針的指定方案,因此 Objective-C 中方法名里的冒號(hào)需要用一個(gè)問號(hào) (?
) 來替代。比如要匹配 -[NSDate dateByAddingTimeInterval:]
的話,可以這么寫:
objc207:NSDate:-dateByAddingTimeInterval?:entry
通過查看 dtrace(1)
幫助頁面可以獲得更多詳細(xì)信息。
io
提供者為了追蹤與磁盤輸入輸出相關(guān)的活動(dòng),io
提供者 定義了 6 個(gè)探針:
start
done
wait-start
wait-done
journal-start
journal-done
Oracle 文檔中的例子展示了如何使用:
#pragma D option quiet
BEGIN
{
printf("%10s %58s %2s\n", "DEVICE", "FILE", "RW");
}
io:::start
{
printf("%10s %58s %2s\n", args[1]->dev_statname,
args[2]->fi_pathname, args[0]->b_flags & B_READ ? "R" : "W");
}
上面的例子會(huì)輸出類似這樣的結(jié)果:
?? ??/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
兩個(gè)探針。任何時(shí)候數(shù)據(jù)通過 IP 被發(fā)送或接收都會(huì)觸發(fā)。參數(shù) arg0
到 arg5
提供了與發(fā)送或接收的 IP 包所相關(guān)的 kernel 結(jié)構(gòu)體的訪問入口。
可以將二者放入非常強(qiáng)大的網(wǎng)絡(luò)調(diào)試工具中。它可以使 tcpdump(1)
的看起來像過時(shí)的玩意。ip
提供者可以讓我們?cè)谛枰臅r(shí)候精確的輸出我們所需要的信息。
查看文檔獲得更多很棒的示例。
DTrace 允許我們創(chuàng)建自己的探針,通過這個(gè),我們可以為我們自己的 app 釋放 DTrace 的真正威力。
這些在 DTrace 中被稱作靜態(tài)探針。我們?cè)?a href="#ATimingExample">第一個(gè)例子中曾經(jīng)簡短的提到過。Wire 定義了自己的提供者和探針:
provider syncengine_sync {
probe strategy_go_to_state(int);
}
然后我們?cè)诖a中調(diào)用探針:
- (void)goToState:(ZMSyncState *)state
{
[self.currentState didLeaveState];
self.currentState = state;
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier);
[self.currentState didEnterState];
}
一個(gè)可能的爭論就是我們本來可以干脆直接使用 objc
提供者;使用我們自己的探針可以更具靈活性。以后我們可以修改 Objective-C 代碼而不影響 DTrace 探針。
另外,靜態(tài)探針給我們提供更方便的參數(shù)訪問方式。通過上面我們可以看到我們?nèi)绾卫盟鼇碜粉檿r(shí)間和輸出日志。
DTrace 靜態(tài)探針的強(qiáng)大之處在于給我們提供了穩(wěn)定的接口來調(diào)試我們的代碼,并且即便是在生產(chǎn)代碼中這個(gè)接口也是存在的。即使對(duì)于應(yīng)用的生產(chǎn)版本,當(dāng)有人看到奇怪的行為,我們也可以給正在運(yùn)行的應(yīng)用附加一段 DTrace 腳本。DTrace 的靈活性還可以讓我們將同一個(gè)探針用于其他目的。
我們可以將 DTrace 作為日志工具使用。還可以用來收集與時(shí)間,網(wǎng)絡(luò),請(qǐng)求等有關(guān)的詳細(xì)的量化信息。
我們可以將探針留在生產(chǎn)代碼中的原因是探針是零損耗的,或者公平點(diǎn)說,相當(dāng)于一個(gè)測(cè)試和分支的 CPU 指令。
下面來看看如何將靜態(tài)探針加入到我們的工程。
首先我們需要?jiǎng)?chuàng)建一個(gè) .d
文件并定義提供者和探針。如果我們創(chuàng)建了一個(gè) provider.d
文件并寫入以下內(nèi)容,會(huì)得到兩個(gè)提供者:
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
。在每個(gè)提供者中,我們定義了一組探針。
現(xiàn)在我們需要將 provider.d
加入到 Xcode 的構(gòu)建目標(biāo)中。確保將類型設(shè)置為 DTrace source,這十分重要。Xcode 現(xiàn)在會(huì)在構(gòu)建時(shí)自動(dòng)處理。在這個(gè)步驟中,DTrace 會(huì)創(chuàng)建一個(gè)對(duì)應(yīng)的 provider.h
頭文件,我們可以引入它。將 provider.d
同時(shí)加入 Xcode 工程和相應(yīng)的構(gòu)建目標(biāo)非常重要。
在處理時(shí),Xcode 會(huì)調(diào)用 dtrace(1)
工具:
dtrace -h -s provider.d
這會(huì)生成相應(yīng)的頭文件。該文件最后會(huì)出現(xiàn)在 DERIVED_FILE_DIR
中??梢酝ㄟ^以下方式在任何工程內(nèi)的源文件中引用
#import "provider.h"
Xcode 有一個(gè)內(nèi)置的所謂 build rule 來處理 DTrace 提供者描述。比較 objc.io Build 過程的內(nèi)容來獲取關(guān)于構(gòu)建規(guī)則和構(gòu)建處理的更多的信息。
對(duì)于每一個(gè)靜態(tài)探針,頭文件會(huì)包含兩個(gè)宏:
PROVIDER_PROBENAME()
PROVIDER_PROBENAME_ENABLED()
第一個(gè)是探針本身。第二個(gè)會(huì)在探針關(guān)閉時(shí)取值為 0。
DTrace 探針自己本身在沒被啟用時(shí)是零消耗的,也就是說只要沒有附加在探針上的東西,它們就不會(huì)產(chǎn)生消耗。然而有時(shí),我們可能想要提前判斷或?qū)?shù)據(jù)發(fā)送給探針之前做一些預(yù)處理。在這些不太常見的情況下,我們可以使用 _ENABLED()
宏:
if (SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE_ENABLED()) {
argument = /* Expensive argument calculation code here */;
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(argument);
};
代碼可讀性非常重要。隨著增加越來越多的探針到代碼中,我們需要確保代碼不會(huì)因此而變得亂七八糟和難以理解。畢竟探針的目的是幫助我們而不是將事情變得更復(fù)雜。
我們所需要做的就是增加另一個(gè)簡單的包裝器。這些包裝器既使代碼的可讀性更好了一些,也增加了 if (…_ENABLED*())
檢查。
回到狀態(tài)機(jī)器的例子中,我們的探針宏是這樣的:
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state);
為了使它變得更簡單,我們創(chuàng)建另一個(gè)頭文件并定義:
static inline void ZMTraceSyncStrategyGoToState(int d) {
SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(d);
}
有了這個(gè),之后我們就調(diào)用:
ZMTraceSyncStrategyGoToState(state);
這看起來有點(diǎn)取巧,但是駝峰式命名確實(shí)能在混合普通的 Objective-C 和 Swift 代碼風(fēng)格方面做得更好。
更進(jìn)一步的,如果我們看到上面定義的
provider syncengine_ui {
probe notification(int, intptr_t, char *, char *, int, int, int, int);
};
有一長串的參數(shù)。在 Wire 中,我們用這個(gè)來記錄 UI 通知日志。
這個(gè)探針有一長串的參數(shù)。我們決定只要一個(gè)探針來處理許多不同通知,這些通知都是同步代碼發(fā)給 UI 用以提醒變化的。第一個(gè)參數(shù),arg0
,定義了是什么通知,第二個(gè)參數(shù)定義了 NSNotification
的 object
。在這使得我們的 DTrace 腳本可以將感興趣的范圍限定在某幾個(gè)指定的通知中。
剩余的參數(shù)定義根據(jù)預(yù)先的通知不同可以稍微寬松些,而且對(duì)于各個(gè)單獨(dú)情況我們有多個(gè)包裝函數(shù)。當(dāng)想要傳入兩個(gè) NSUUID
對(duì)象的情況,我們類似這樣來調(diào)用包裝函數(shù):
MTraceUserInterfaceNotification_UUID(1, note.object,
conversation.remoteIdentifier, participant.user.remoteIdentifier,
wasJoined, participant.isJoined, currentState.connectionState, 0);
這個(gè)包裝函數(shù)是這樣定義的:
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);
}
}
正如之前提到的,我們有兩個(gè)目的。第一,不讓類似 (intptr_t) (__bridge void *)
這樣的代碼把我們的代碼搞的亂七八糟。另外,除非因?yàn)楦郊拥教结樀臅r(shí)候有需要,其他時(shí)候我們無需花費(fèi) CPU 周期將一個(gè) NSUUID
轉(zhuǎn)換為 NSString
并進(jìn)一步轉(zhuǎn)換為 char const *
。
根據(jù)這個(gè)模式,我們可以定義多個(gè)包裝器 / 輔助函數(shù)來復(fù)用相同的 DTrace 探針。
像這樣包裝 DTrace 探針可以讓我們整合 Swift 和 DTrace 靜態(tài)探針。static line
函數(shù)現(xiàn)在可以在 Swift 代碼中直接調(diào)用。
func goToState(state: ZMSyncState) {
currentState.didLeaveState()
currentState = state
currentState.didEnterState()
ZMTraceSyncStrategyGoToState(state.identifier)
}
D 語言是編譯型語言。當(dāng)運(yùn)行 dtrace(1)
工具時(shí),我們傳入的腳本被編譯成字節(jié)碼。接著字節(jié)碼被傳入 kernel。在 kernel 中有一個(gè)解釋器來運(yùn)行這些字節(jié)碼。
這就是為什么這種編程語言可以保持簡單。沒人希望 DTrace 腳本中的 bug 引起 kernel 的死循環(huán)并導(dǎo)致系統(tǒng)掛起。
當(dāng)將靜態(tài)探針加入可執(zhí)行程序 (一個(gè) app 或 framework),它們被作為 S_DTRACE_DOF
(Dtrace Object Format) 部分被加入,并且在程序運(yùn)行時(shí)被加載進(jìn) kernel。這樣 DTrace 就知道當(dāng)前的靜態(tài)探針。
毫無疑問 DTrace 非常強(qiáng)大和靈活。然而需要注意的是 DTrace 并不是一些經(jīng)過考驗(yàn)和真正的工具的替代品,如 malloc_history
,heap
等。記得始終使用正確的工具。
另外,DTrace 并不是魔法。你仍然需要知道你要解決的問題所在。
這就是說,DTrace 可以使你的開發(fā)技能和能力達(dá)到一個(gè)新的水準(zhǔn)。它可以讓你在生產(chǎn)代碼中追蹤那些很難或不可能定位的問題。
如果你的代碼中有 #ifdef TRACING
或 #if LOG_LEVEL == 1
,使用 DTrace 替換它們或許是很好的主意。
記得查看 Dynamic Tracing Guide (PDF version)。并且在你系統(tǒng)的 /usr/share/examples/DTTk
文件夾中獲取更多的靈感。
調(diào)試快樂!