arthas icon indicating copy to clipboard operation
arthas copied to clipboard

添加line命令,用来观测方法内的局部变量,可以通过行号或者LineCode来指定位置

Open isadliliying opened this issue 1 year ago • 9 comments

为什么要做这个呢?

  • 符合Debug的习惯,查看方法内的局部变量是理所当然的想法
  • 协助快速定位排查问题,若通过加日志观测的话自由度会更大,但retransform会比较麻烦,而且往往随着排查深入,可能要加多个地方
  • 有看到isuues里边也有一些小伙伴提到这个,需求有其合理性
  • 所在业务已经使用1年有余,虽使用频率不如watch、trace,但仍能提高问题排查效率(上次提的PR没有通过,这次完善了一些点🙏)

为什么要定义新的命令?

  • 跟观测非常契合的命令是watch,但是watch的对象是method,而我们观测local variables的时候,用来作为定位点是比较合适的(因为同一个变量会被多次重复赋值),两者的回调监听是不一致的
  • 独立一个命令也能降低对旧命令的影响,也能降低实现的复杂度

实现思路:

  • 定义新的Advice变量 varMap,类型是HashMap,key是变量名,value是变量值
  • 插桩的位置是命令参数传入的 LineNumber 或 LineCode(自定义的特殊值) 之前,并使用-1来表示方法退出前的插桩点
  • 监听器的注册需要做相应的区分处理

已经做的基本测试:

  • 基础功能实现,能观测到定义的local variables,并能在expresscondition-express中使用
  • 重复插桩处理,增加了对应的LocationFilter,避免重复插桩
  • 多人同时监听使用
  • tracewatch命令的同时使用

为什么会需要使用LineCode?

主要是kotlin编译后的字节码跟源码相差甚远,如下: kotlin源码:

/*56*/    fun index(): JSONObject {
/*57*/        listOf(1,2,3,4,5,6).map {
/*58*/            listOf("a","b","c","d","e","f").map {
/*59*/                listOf(true,false,true,false,false).map {
/*60*/                    println(it)
/*61*/                    if (true) return JSONObject()
/*62*/                }
/*63*/            }
/*64*/        }
/*65*/        return ResponseBuilder().ok().data("Hello World!").build()
/*66*/    }

jad出来的样子:

       public final JSONObject index() {
           void var3_3;
           void $receiver$iv$iv;
           Iterable $receiver$iv;
/*57*/     Iterable iterable = $receiver$iv = (Iterable)CollectionsKt.listOf(1, 2, 3, 4, 5, 6);
           Collection destination$iv$iv = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10));
/*83*/     for (Object item$iv$iv : $receiver$iv$iv) {
               void $receiver$iv$iv2;
               Iterable $receiver$iv2;
               int n = ((Number)item$iv$iv).intValue();
               Collection collection = destination$iv$iv;
               boolean bl = false;
/*58*/         Iterable iterable2 = $receiver$iv2 = (Iterable)CollectionsKt.listOf("a", "b", "c", "d", "e", "f");
               Collection destination$iv$iv2 = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv2, 10));
/*86*/         for (Object item$iv$iv2 : $receiver$iv$iv2) {
                   void $receiver$iv$iv3;
                   Iterable $receiver$iv3;
                   String string = (String)item$iv$iv2;
                   Collection collection2 = destination$iv$iv2;
                   boolean bl2 = false;
/*59*/             Iterable iterable3 = $receiver$iv3 = (Iterable)CollectionsKt.listOf(true, false, true, false, false);
                   Collection destination$iv$iv3 = new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv3, 10));
                   Iterator iterator2 = $receiver$iv$iv3.iterator();
                   if (iterator2.hasNext()) {
                       void it;
                       void it2;
                       void it3;
                       Object item$iv$iv3 = iterator2.next();
                       boolean bl3 = (Boolean)item$iv$iv3;
                       Collection collection3 = destination$iv$iv3;
                       boolean bl4 = false;
/*60*/                 System.out.println((boolean)it3);
                       JSONObject jSONObject = new JSONObject();
                       return jSONObject;
                   }
/*91*/             List list = (List)destination$iv$iv3;
                   collection2.add(list);
               }
/*92*/         List list = (List)destination$iv$iv2;
               collection.add(list);
           }
/*93*/     List cfr_ignored_0 = (List)var3_3;
           return new ResponseBuilder().ok().data((Object)"Hello World!").build();
       }

问题: 反编译后,它的行号分布是乱序的,另外实践中还发现有重复行号的问题,而且反编译后与源代码大相径庭,也生成了很多的额外的零时变量,也有很多行是没有行号的,所以使用行号定位的话,是不够完善的,有些点无法进行定位插入监听!

解法: 如何能监听到所有本地变量的变化过程呢? -> 变量在何时会被改变呢? -> 赋值、作为方法参数被调用 基于此,通过筛选方法中的InsnNode,只保留 VarInsnNode 和 MethodInsnNode 作为备选插入点,然后生成类似如下的标记行并以此为变量监测的插入点. 目前格式如: 行号 + LineCode + 指令(方法调用/变量赋值 )

/*61 */ (4076-1)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-2)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-3)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-4)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-5)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (4076-6)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*61 */ (7a12-1)->  
                  invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*84 */ (17cc-1)->  
                  invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*84 */ (ec06-1)->  
                  invoke-method:java/util/ArrayList#<init>:(I)V
/*85 */ (2795-1)->  
                  invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*85 */ (b5c4-1)->  
                  invoke-method:java/util/Iterator#hasNext:()Z
/*85 */ (dce9-1)->  
                  invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*86 */ (9699-1)->  
                  invoke-method:java/lang/Number#intValue:()I
/*86 */ (81f3-1)->  
                  assign-variable:it
/*62 */ (7a12-2)->  
                  invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*87 */ (17cc-2)->  
                  invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*87 */ (ec06-2)->  
                  invoke-method:java/util/ArrayList#<init>:(I)V
/*88 */ (2795-2)->  
                  invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*88 */ (b5c4-2)->  
                  invoke-method:java/util/Iterator#hasNext:()Z
/*88 */ (dce9-2)->  
                  invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*89 */ (81f3-2)->  
                  assign-variable:it
/*63 */ (fe63-1)->  
                  invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-2)->  
                  invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-3)->  
                  invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-4)->  
                  invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (fe63-5)->  
                  invoke-method:java/lang/Boolean#valueOf:(Z)Ljava/lang/Boolean;
/*63 */ (7a12-3)->  
                  invoke-method:kotlin/collections/CollectionsKt#listOf:([Ljava/lang/Object;)Ljava/util/List;
/*90 */ (17cc-3)->  
                  invoke-method:kotlin/collections/CollectionsKt#collectionSizeOrDefault:(Ljava/lang/Iterable;I)I
/*90 */ (ec06-3)->  
                  invoke-method:java/util/ArrayList#<init>:(I)V
/*91 */ (2795-3)->  
                  invoke-method:java/lang/Iterable#iterator:()Ljava/util/Iterator;
/*91 */ (b5c4-3)->  
                  invoke-method:java/util/Iterator#hasNext:()Z
/*91 */ (dce9-3)->  
                  invoke-method:java/util/Iterator#next:()Ljava/lang/Object;
/*92 */ (e4e8-1)->  
                  invoke-method:java/lang/Boolean#booleanValue:()Z
/*92 */ (81f3-3)->  
                  assign-variable:it
/*64 */ (a406-1)->  
                  invoke-method:java/io/PrintStream#println:(Z)V
/*65 */ (7338-1)->  
                  invoke-method:com/alibaba/fastjson/JSONObject#<init>:()V
/*93 */ (088d-1)->  
                  invoke-method:java/util/Collection#add:(Ljava/lang/Object;)Z
/*94 */ (088d-2)->  
                  invoke-method:java/util/Collection#add:(Ljava/lang/Object;)Z
/*69 */ (f84c-1)->  
                  invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#<init>:()V
/*69 */ (3252-1)->  
                  invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#ok:()Lcom/seewo/study/minder/common/util/ResponseBuilder;
/*69 */ (06c3-1)->  
                  invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#data:(Ljava/lang/Object;)Lcom/seewo/study/minder/common/util/ResponseBuilder;
/*69 */ (a260-1)->  
                  invoke-method:com/seewo/study/minder/common/util/ResponseBuilder#build:()Lcom/alibaba/fastjson/JSONObject;

使用示例:

源码(部分):

...
/*8*/   public class MathGame {
...
/*21*/      public void run() throws InterruptedException {
/*22*/          try {
/*23*/              int number = random.nextInt() / 10000;
/*24*/              List<Integer> primeFactors = primeFactors(number);
/*25*/              print(number, primeFactors);
/*26*/  
/*27*/          } catch (Exception e) {
/*28*/              System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage());
/*29*/          }
/*30*/      }
...
/*63*/  }

使用行号:

$ line demo.MathGame run 25 -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2
method=demo.MathGame.run line=25
ts=2024-06-21 09:57:34.452; result=@HashMap[
    @String[primeFactors]:@ArrayList[
        @Integer[2],
        @Integer[7],
        @Integer[7],
        @Integer[991],
    ],
    @String[number]:@Integer[97118],
]

查看LineCode:

$ jad --lineCode demo.MathGame run
........
------------------------- lineCode location -------------------------
format: /*LineNumber*/ (LineCode)-> Instruction
/*23 */ (aacd-1)->  
                  invoke-method:java/util/Random#nextInt:()I
/*23 */ (5918-1)->  
                  assign-variable:e
/*24 */ (653f-1)->  
                  invoke-method:demo/MathGame#primeFactors:(I)Ljava/util/List;
/*24 */ (d961-1)->  
                  assign-variable:primeFactors
/*25 */ (416e-1)->  
                  invoke-method:demo/MathGame#print:(ILjava/util/List;)V
/*27 */ (5918-2)->  
                  assign-variable:e
/*28 */ (2455-1)->  
                  invoke-method:java/lang/StringBuilder#<init>:()V
/*28 */ (4076-1)->  
                  invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer;
/*28 */ (b6e4-1)->  
                  invoke-method:java/lang/String#format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
/*28 */ (850c-1)->  
                  invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
/*28 */ (a53d-1)->  
                  invoke-method:java/lang/Exception#getMessage:()Ljava/lang/String;
/*28 */ (850c-2)->  
                  invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
/*28 */ (f7bb-1)->  
                  invoke-method:java/lang/StringBuilder#toString:()Ljava/lang/String;
/*28 */ (2f1b-1)->  
                  invoke-method:java/io/PrintStream#println:(Ljava/lang/String;)V
Affect(row-cnt:1) cost in 103 ms.

使用LineCode:

$ line demo.MathGame run 416e-1 -x 2
Press Q or Ctrl+C to abort.
Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2
method=demo.MathGame.run line=25
ts=2024-06-21 09:57:34.452; result=@HashMap[
    @String[primeFactors]:@ArrayList[
        @Integer[2],
        @Integer[7],
        @Integer[7],
        @Integer[991],
    ],
    @String[number]:@Integer[97118],
]

其它:

  • 这边打包好的版本(arthas-bin.zip): https://ali-pro-pub.xbstatic.com/running-wechat-mp/uwiwlnvhhhwljhjxhnjvinmpyiphihhh
  • 具体使用方法可以启动后通过line -h查看
  • commit: line command implement 是line命令的具体实现
  • commit: add introduction for line command 是相关的命令说明

isadliliying avatar Jun 21 '24 08:06 isadliliying

@hengyunabc 大佬麻烦看看

isadliliying avatar Jul 05 '24 03:07 isadliliying

line -h可以正常输出,为什么观测变量时提示不支持命令呢,是不支持java8吗 image

PGMT12138 avatar Jul 09 '24 07:07 PGMT12138

@isadliliying 有时候 我们观察局部变量是观察一个变化过程,比如 方法methodX内部 局部变量 a,b, 从35到55行 a,b变量发生了哪些变化,也就是方法methodX 执行一次就打印20行的(55-35)变化结果类似于表格记录吧

lxyyouxiang123 avatar Aug 21 '24 02:08 lxyyouxiang123

image java8不行吗?

xiaoabc4 avatar Oct 12 '24 07:10 xiaoabc4

我编译你这个分支好像不通过 Screenshot of IntelliJ IDEA (2024-10-23, 14-59-51)

zhangpeilin avatar Oct 23 '24 07:10 zhangpeilin

line -h可以正常输出,为什么观测变量时提示不支持命令呢,是不支持java8吗 image

这个应该是你先启动了旧版本,然后再启动这个带line命令的版本,就会出现这个提示哈

isadliliying avatar Oct 25 '24 01:10 isadliliying

image java8不行吗?

你启动的是这个版本嘛?这个命令没有合并到主分支,正式版本里边是没有这个命令的

isadliliying avatar Oct 25 '24 01:10 isadliliying

我编译你这个分支好像不通过 Screenshot of IntelliJ IDEA (2024-10-23, 14-59-51)

excludePattern 是有个新版本的依赖库,可以检查下你的 bytekit-core 版本 <groupId>com.alibaba</groupId> <artifactId>bytekit-core</artifactId> 0.0.9

isadliliying avatar Oct 25 '24 01:10 isadliliying