HouseMD是淘寶的聚石寫的一個(gè)非常優(yōu)秀的Java進(jìn)程運(yùn)行時(shí)診斷和調(diào)試工具,如果你接觸過btrace,那么HouseMD也許你應(yīng)該嘗試下,它比btrace更易用,不需要寫腳本,類似strace的方式attach到j(luò)vm進(jìn)程做跟蹤調(diào)試。
基本的安裝和使用請(qǐng)看這篇文檔《
UserGuide》,恕不重復(fù)。以下內(nèi)容都假設(shè)你正確安裝了housemd。
本文主要介紹下怎么用housemd診斷跟蹤clojure進(jìn)程。Clojure的java實(shí)現(xiàn)也是跑在JVM里,當(dāng)然也可以用housemd。
我們以一個(gè)簡(jiǎn)單的例子開始,假設(shè)我們有如下clojure代碼:
(loop [x 1]
(Thread/sleep 1000)
(prn x)
(recur (inc x)))
這段很簡(jiǎn)單,只是間隔一秒不斷地打印遞增的數(shù)字x。我們準(zhǔn)備用housemd跟蹤這個(gè)程序的運(yùn)行,首先運(yùn)行這個(gè)程序,你可以用lein,也可以直接java命令運(yùn)行:
java -cp clojure.jar clojure.main test.clj
運(yùn)行時(shí)不斷地在控制臺(tái)打印數(shù)字,通過jps或者ps查詢到該進(jìn)程的id,假設(shè)為pid,使用housemd連接到該進(jìn)程:
housemd <pid>
順利進(jìn)入housemd的交互控制臺(tái),通過help命令可以查詢支持的命令:
housemd> help
quit terminate the process.
help display this infomation.
trace display or output infomation of method invocaton.
loaded display loaded classes information.
要用housemd調(diào)試clojure,你需要對(duì)clojure的實(shí)現(xiàn)有一點(diǎn)點(diǎn)了解,有興趣可以看過去的一篇blog《
clojure hacking guide》,簡(jiǎn)單來說,clojure的編譯器會(huì)將clojure代碼編譯成java類并運(yùn)行。對(duì)于JVM來說,clojure生成的類,跟java編譯器生成類沒有什么不同。
具體到上面的clojure代碼,會(huì)生成一個(gè)名為
user$eval1的類,user是默認(rèn)的namespace,而eval1是clojure編譯器自動(dòng)生成的一個(gè)標(biāo)示類名,通過
loaded命令查詢類的加載情況:
housemd> loaded user$eval1 -h
user$eval1 -> null
- clojure.lang.DynamicClassLoader@1d25d06e
- clojure.lang.DynamicClassLoader@1d96f4b5
- sun.misc.Launcher$AppClassLoader@a6eb38a
- sun.misc.Launcher$ExtClassLoader@69cd2e5f
通過-h選項(xiàng)打印了加載user$eval1的類加載器的層次關(guān)系,因?yàn)閡ser$eval1是動(dòng)態(tài)生成的(clojure啟動(dòng)過程中),因此它不在任何一個(gè)class或者jar文件中。除了查詢user namespace的類之外,你還可以查詢clojure.core,clojure.lang,clojure.java等任何被加載進(jìn)來的類,例如查詢clojure.core.prn的類,在clojure里這是一個(gè)函數(shù),在jvm看來這只是一個(gè)類:
housemd> loaded -h core$prn
clojure.core$prn -> /Volumes/HDD/Users/apple/clojure/clojure.jar
- sun.misc.Launcher$AppClassLoader@a6eb38a
- sun.misc.Launcher$ExtClassLoader@69cd2e5f
注意,不需要完整的namespace——clojure.core,直接core$prn即可。其他也是類似。
小技巧:如果你實(shí)在不知道clojure編譯器生成的類名,你可以利用jvm自帶的jmap命令來查詢。 接下來,我們嘗試用trace命令跟蹤方法的運(yùn)行,例如例子中的clojure代碼用到了loop和recur兩個(gè)sepcial form,我們跟蹤下loop:
housemd> trace -t 5 core$loop
INFO : probe class clojure.core$loop
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.doInvoke(Object, Object, Object, Object) sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
core$loop.getRequiredArity() sun.misc.Launcher$AppClassLoader@a6eb38a 0 -ms null
INFO : Ended by timeout
INFO : reset class clojure.core$loop
在5秒內(nèi),clojure.core$loop類有兩個(gè)方法各被調(diào)用了5次,doInvoke是實(shí)際的調(diào)用,而getRequiredArity用來查詢loop所需要的參數(shù)個(gè)數(shù)。trace還可以跟蹤到具體的方法,例如我們跟蹤prn函數(shù)的調(diào)用情況:
housemd> trace -t 5 core$prn.doInvoke
INFO : probe class clojure.core$prn
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 1 1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 2 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 3 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 4 <1ms clojure.core$prn@3e4ac866
core$prn.doInvoke(Object) sun.misc.Launcher$AppClassLoader@a6eb38a 5 <1ms clojure.core$prn@3e4ac866
INFO : Ended by timeout
INFO : reset class clojure.core$prn
trace打印了方法的調(diào)用次數(shù)(5秒內(nèi))和每次調(diào)用的時(shí)間(毫秒級(jí)別),以及調(diào)用的target object
。小技巧:沒有可變參數(shù)的函數(shù)生成類最終調(diào)用的是invoke方法(參數(shù)個(gè)數(shù)可能重載),有可變參數(shù)的函數(shù)調(diào)用的是doInvoke方法。
trace命令還支持打印調(diào)用堆棧到文件,例如:
trace -t 5 -d -s core$prn.doInvoke
利用-s和-d命令會(huì)將詳細(xì)的調(diào)用信息輸出到臨時(shí)目錄,臨時(shí)目錄的路徑可以通過trace help命令查詢到,在我的機(jī)器上是/tmp/trace/<pid>@host目錄下。調(diào)用堆棧的輸出類似:
example$square.invoke(Long) call by thread [main]
example$eval9.invoke(test.clj:11)
clojure.lang.Compiler.eval(Compiler.java:6465)
clojure.lang.Compiler.load(Compiler.java:6902)
clojure.lang.Compiler.loadFile(Compiler.java:6863)
clojure.main$load_script.invoke(main.clj:282)
clojure.main$script_opt.invoke(main.clj:342)
clojure.main$main.doInvoke(main.clj:426)
clojure.lang.RestFn.invoke(RestFn.java:421)
clojure.lang.Var.invoke(Var.java:405)
clojure.lang.AFn.applyToHelper(AFn.java:163)
clojure.lang.Var.applyTo(Var.java:518)
clojure.main.main(main.java:37)
上面這個(gè)簡(jiǎn)單的例子展示了使用housemd跟蹤診斷clojure進(jìn)程的方法。
自定義ns和函數(shù)的調(diào)試與此類似,假設(shè)我們有下面的clojure代碼:
(ns example)
(defn square [x]
(* x x))
(loop [x 1]
(Thread/sleep 1000)
(square x)
(recur (inc x)))
ns為example,自定義函數(shù)square并定期循環(huán)調(diào)用。使用housemd診斷這段代碼:
loaded -h example$square #查詢square的加載情況
trace -t 10 -d -s example$square.invoke #跟蹤10秒內(nèi)square的調(diào)用情況