electronicarts / ea-async

EA Async implements async-await methods in the JVM.
https://go.ea.com/ea-async
Other
1.38k stars 129 forks source link

debugging transformed code #7

Closed bennylut closed 6 years ago

bennylut commented 7 years ago

Hi, first I just want to say - great job!

I noticed that when I use this library I cannot debug a method that has been modified, when I try to I receive the following error: JDWP exit error JVMTI_ERROR_NONE(0): getting frame location [stepControl.c:641]

is this a limitation that you planning to resolve?

JoeHegarty commented 7 years ago

Hi! If you use the -javaagent JVM parameter as described in the README to run ea-async, you shouldn't run into any crashes, but it doesn't really solve the issue at hand. Unfortunately, debugging is still not going to work as expected (you will get odd behaviour with breakpoints and stepping through your code). This makes sense when you think about what ea-async is doing by rewriting the bytecode underneath so it's no longer matching the input source.

I've had some thoughts about potentially allowing a way to disable async just for debugging. This would work by replacing calls to await with a basic join. That would likely fix the debugging issues, but then the runtime behaviour would be significantly different as the code would block the thread rather than rewriting your method to be a state machine returning a task immediately, so it's not ideal from that point of view.

If you (or anyone else) has any thoughts on how we might approach this problem I'm certainly open to ideas!

bennylut commented 7 years ago

I see, What if you had a code transformation for async that only add bytecode to the existing method and don't completely modify the existing byte code - doesn't then the existing debug information will be kept relevant and therefore will allow debugging?

Let me try to explain myself: (and on this occasion also say sorry for the long post :) )

assume that you have the following class:

class Test {
  static CompletableFuture<Integer> doSomthingAsync() { ... }

  static CompletableFuture<Integer> transformMe(int k) {
    int sum=0; //line 1
    for (int i=0; i<k; i++)  //line 2
        sum+=await(doSomethingAsync()); //line 3
    return completedFuture(sum); //line 4
  }
}

If it will be compiled with debug information it will contain a LineNumberTable for the debugger that is in sync with the source code, if the transformation of ea-async for the transformMe method will do something like:

static CompletableFuture<Integer> transformMe(int k) {
  AsyncState$transformMe state = AsyncState.get();
  if (state == null) {
    state = new AsyncState$transformMe(k);
  }

  switch (state.state) {
    case 0: goto START; //using the goto op code 
    case 1: goto STATE_1; //using the goto op code 
  }

  START:
  int sum=0;  //line 1
  for (int i=0; i<k; i++) { //line 2
    state.$local1 = sum; 
    state.$local2 = i;
    return state.bind(doSomethingAsync(), 1)
    STATE_1:
    sum = state.$local1;
    i = state$local2;
    sum+=(Integer) state.lastAwaitResult(); //line 3
  }
  return state.complete(completedFuture(sum)); //line 4
}

Where AsyncState is the following class:

abstract class AsyncState {
  private final static ThreadLocal<AsyncState> STATE_HOLDER = new ThreadLocal<>();

  public static <T extends AsyncState> T get() {
    T res =  (T) STATE_HOLDER.get();
    STATE_HOLDER.remove();
    return res;
  }
  public static void set(AsyncState state) {STATE_HOLDER.set(state); }

  private CompletableFuture result = new CompletableFuture();
  public int state = 0;
  public Object lastResult;

  abstract void doContinue();

  CompletableFuture  bind (CompletablFuture target, int nextState) {
    target.whenComplete((v,e) -> {
        if (e == null) { 
          this.state = nextState;
          AsyncState.set(this);
          this.lastResult = v;
          doContinue();
        }else {
           result.completeExceptionally(e);
        }
    });  
    return result;
  }

  CompletableFuture complete(CompletableFuture fin) {
     fin.whenComplete((v,e) -> {
        if (e == null) { 
          result.complete(v);
        }else {
           result.completeExceptionally(e);
        }
    });  
    return result;
  }
}

And AsyncState$transformMe is a class that is generated by the agent and look something like:

public class AsyncState$transformMe  extends AsyncState {
  public int $arg1;
  public int $local1;
  public int $local2;

  public AsyncState$transformMe(int $arg1) {this.$arg1 = $arg1;}

  void doContinue() {
    transformMe($arg1);
  }  

}

Under this transformation, the original source code is still inside the same method and with a little adjustments one can align the LineNumbersTable accordingly which (theoretically at least) should keep the debugger happy.. Do you think It can work?

BTW, I am sure that it is a huge change in the code and I not suggesting you should change the whole design - It is just an idea that I hope can help..

DanielSperry commented 7 years ago

@bennylut It is usually still possible to debug the instrumented code. the line information is not lost.

The only situation where we have problems debugging it is when the instrumentation is done in runtime without the java agent. Offline instrumentation; or runtime instrumentation with the java agent; seem to debug fine.

However, your issue might be something we haven't encountered before. Which IDE are you using?

TheMulti0 commented 5 years ago

Great work on the project!

I still encounter that problem in IntelliJ, I encountered using the Gradle instrumental tool, and the runtime option Async.init();. What do you guys suggest?