鍍金池/ 教程/ iOS/ DTrace
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

DTrace

很少有人聽過 DTrace,它是隱藏在 OS 中的小寶藏。DTrace 是強(qiáng)大的 debug 工具 - 因?yàn)樗鼡碛袠O其靈活的特性,并且因?yàn)榕c其它工具差異很大而可能相對(duì)不那么有名。

許多時(shí)候你的 app 的真正的用戶或測(cè)試人員會(huì)看到一些意外的行為。DTrace 可以讓你無需重啟 app 就能夠在生產(chǎn)版本上回答關(guān)于 app 的任何問題。

動(dòng)態(tài)追蹤

大概 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 來深入了解一些非常好的例子。

OS X vs. iOS

正如你現(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。

一個(gè)定時(shí)例子

因?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í)行。

結(jié)合系統(tǒng)探針

操作系統(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

D 語言

注意:這不是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)作

動(dòng)作包含在花括號(hào)中。D 語言是輕量,精悍而且簡單的語言。

D 不支持控制流,比如循環(huán)和分支。我們不能定義任何用戶函數(shù)。變量定義也是可選的。

這限制了我們能做的事情。但是一旦知道了一些常見的模式,這種簡單也給了我們很多靈活性,我們將在下一節(jié)詳細(xì)討論。在 D Programming Language 的指南中可以查看更多的細(xì)節(jié)。

常見 D 語言模式

下面的例子會(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 Storekevent64 中花費(fèi)了 3.6 秒。

這個(gè)腳本中有兩個(gè)特別有意思的事情:線程本地變量 (self->ts) 和集積 (aggregation)。

變量作用域 (scope)

D 語言有 3 種變量作用域: 全局,線程本地,以及探針語句本地。

foobar 這樣的全局變量在整個(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í)。

集積 (Aggregation)

這行代碼使用了集積:

@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

quantizelquantize 函數(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。

聯(lián)合數(shù)組

不管名字如何,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 提供者

我們之前提到過 BEGINEND 探針。當(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 提供者sendreceive 兩個(gè)探針。任何時(shí)候數(shù)據(jù)通過 IP 被發(fā)送或接收都會(huì)觸發(fā)。參數(shù) arg0arg5 提供了與發(fā)送或接收的 IP 包所相關(guān)的 kernel 結(jié)構(gòu)體的訪問入口。

可以將二者放入非常強(qiáng)大的網(wǎng)絡(luò)調(diào)試工具中。它可以使 tcpdump(1) 的看起來像過時(shí)的玩意。ip 提供者可以讓我們?cè)谛枰臅r(shí)候精確的輸出我們所需要的信息。

查看文檔獲得更多很棒的示例。

定義自己的靜態(tài)探針

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_syncsyncengine_ui。在每個(gè)提供者中,我們定義了一組探針。

創(chuàng)建頭文件

現(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);
};

包裝 DTrace 探針

代碼可讀性非常重要。隨著增加越來越多的探針到代碼中,我們需要確保代碼不會(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ù)定義了 NSNotificationobject。在這使得我們的 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 探針可以讓我們整合 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)
}

DTrace 如何工作

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)試快樂!