Optimize Java Exception and benchmark

bài Clean Code with Exception mình có hứa sẽ giải quyết vấn đề làm Exception chậm. Vì thế bài này sẽ đi vào sâu hơn việc Java sẽ làm gì khi có 1 Exception được throw ra và giải thích stack trace là gì… và từ đó sẽ tối ưu việc sử dụng Exception

1. Java sẽ làm gì khi có 1 Exception được throw ra?

Để hiểu rõ Java làm gì khi có 1 Exception được throw ra thì ta hãy vào xem code của class Throwable cha của Exception hen ;))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class Throwable implements Serializable {
    
    //bla bla bla
    public Throwable() {
        fillInStackTrace();
    }

    public Throwable(String message) {
        fillInStackTrace();
        detailMessage = message;
    }

    public Throwable(String message, Throwable cause) {
        fillInStackTrace();
        detailMessage = message;
        this.cause = cause;
    }

    public Throwable(Throwable cause) {
        fillInStackTrace();
        detailMessage = (cause==null ? null : cause.toString());
        this.cause = cause;
    }

    // fu fu fu
    public StackTraceElement[] getStackTrace() {
        return getOurStackTrace().clone();
    }

    private synchronized StackTraceElement[] getOurStackTrace() {
        // Initialize stack trace field with information from
        // backtrace if this is the first call to this method
        if (stackTrace == UNASSIGNED_STACK ||
            (stackTrace == null && backtrace != null) /* Out of protocol state */) {
            stackTrace = StackTraceElement.of(this, depth);
        } else if (stackTrace == null) {
            return UNASSIGNED_STACK;
        }
        return stackTrace;
    }

    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }
        return this;
    }

    private native Throwable fillInStackTrace(int dummy);
    
    // fe fe fe  
}

Đoạn code trên cho ta thấy tất cả các public constructor của Throwable đều gọi hàm fillInStackTrace(). Vậy hàm này làm gì và tại sao cần gọi nó? Để có được câu trả lời ta đi vào phần tiếp theo

2. Stack trace là gì? Vì sao cần dùng nó?

Giả sử ta có đoạn code sau và thử chạy nó

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Junk {
    public static void main(String args[]) {
        try {
            a();
        } catch(HighLevelException e) {
            e.printStackTrace();
        }
    }
    static void a() throws HighLevelException {
        try {
            b();
        } catch(MidLevelException e) {
            throw new HighLevelException(e);
        }
    }
    static void b() throws MidLevelException {
        c();
    }
    static void c() throws MidLevelException {
        try {
            d();
        } catch(LowLevelException e) {
            throw new MidLevelException(e);
        }
    }
    static void d() throws LowLevelException {
        e();
    }
    static void e() throws LowLevelException {
        throw new LowLevelException();
    }
}

class HighLevelException extends Exception {
    HighLevelException(Throwable cause) { super(cause); }
}

class MidLevelException extends Exception {
    MidLevelException(Throwable cause)  { super(cause); }
}

class LowLevelException extends Exception {
}

Kết quả sẽ có lỗi sau:

HighLevelException: MidLevelException: LowLevelException
        at Junk.a(Junk.java:13)
        at Junk.main(Junk.java:4)
Caused by: MidLevelException: LowLevelException
        at Junk.c(Junk.java:23)
        at Junk.b(Junk.java:17)
        at Junk.a(Junk.java:11)
        ... 1 more
Caused by: LowLevelException
        at Junk.e(Junk.java:30)
        at Junk.d(Junk.java:27)
        at Junk.c(Junk.java:21)
        ... 3 more

Phân tích message lỗi trên ta có diễn dịch lại sau:

  • Có exception HighLevelException được throw ra. Nguyên nhân của HighLevelExceptionMidLevelException và nguyên nhân của MidLevelExceptionLowLevelException
  • HighLevelException xuất hiện ở class Junk method a file Junk.java dòng thứ 13 và method a được gọi từ class Junk method main file Junk.java dòng thứ 4
  • MidLevelException xuất hiện ở class Junk method c file Junk.java dòng thứ 23, method c được gọi từ class Junk method b file Junk.java dòng thứ 17 và method b được gọi từ class Junk method a file Junk.java dòng thứ 11
  • LowLevelException thì tương tự như với MidLevelExceptionHighLevelException chứ mình cũng hơi đau đầu rồi đó. Tất nhiên nếu có lỗi và cần trace thì vẫn phải làm ;))

Và từ phân tích trên ta có thể vẻ lại flow mà exception bị throw ra như thế nào:

Stack trace

Hình vẻ trên của mình cũng có thể coi là một biểu diễn của stack trace. Nói rõ hơn stack trace là một danh sách có thứ tự của các lời gọi hàm và được sắp xếp theo thứ tự gọi từ các lời gọi gần đây nhất tới lời gọi đầu tiên. Nó rất hữu ích trong việc phân tích, tìm nguyên nhân và từ đó ra quyết định có cần fix lỗi hay không.

Quay lại câu hỏi ở #1. Thì ta đã có câu trả lời. Hàm fillInStackTrace() là đi collect stack trace và nó rất hữu ích phải không nào 🤟

3. Collect stack trace nhanh hay chậm?

Câu trả lời là rất chậm và để biết chậm như thế nào bạn có thể clone repo này. Còn nếu bạn làm biến thì xem #5 nha.

4. Tắt collect stack trace và optimize

Mình có viết lại NSTException và đơn giản hoá 2 hàm getStackTrace()fillInStackTrace() như đoạn code sau:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class NSTException extends Exception {

  private static final StackTraceElement[] EMPTY_STACK_TRACE = new StackTraceElement[0];

  public NSTException(String message) {
    super(message);
  }

  @Override
  public StackTraceElement[] getStackTrace() {
    return EMPTY_STACK_TRACE;
  }

  @Override
  public Throwable fillInStackTrace() {
    return this;
  }
}

Lúc này khi NSTException được khởi tạo nó sẽ không collect stack trace nữa. Cùng xem thành quả như thế nào nhé.

5. Benchmark

Mình giải thích chút xíu về cách thức benchmark. Mình có 3 method callNotThrowException, callToThrowExceptioncallToThrowNSTException dùng giả lập các tình huống:

  • callNotThrowException: tình huống bình thường, không có Exception này được throw ra
  • callToThrowException: tình huống Exception được throw ra
  • callToThrowNSTException: tình huống NSTException được throw ra. NSTException viết tắt của No Stack Trace Exception là một custom exception mình viết lại, tắt collection stack trace để tăng hiệu năng

Do khác nhau về cấu hình, OS, Java version… nên kết quả có thể khác nhau. Và đây là kết quả từ máy mình:

Benchmark                    (param)   Mode  Cnt         Score          Error  Units
Jmh.callNotThrowException          1  thrpt   25  80504640.048 ± 15390305.644  ops/s
Jmh.callToThrowException           1  thrpt   25    703653.862 ±    58119.745  ops/s
Jmh.callToThrowNSTException        1  thrpt   25  41387559.456 ±  6619635.038  ops/s

Ta chú ý 2 kết quả của Jmh.callNotThrowExceptionJmh.callToThrowException lần lượt là 80504640.048703653.862 (bỏ qua sai số ±) thì 80504640.048 / 703653.862 = 114 quả là một con số rất lớn.

Kết luận: bằng việc sử dụng NSTException để tắt collect stack trace giúp ta có những ưu điểm sau:

  • Tăng tốc ứng dụng và sử dụng bộ nhớ hiệu quả hơn
  • Làm cho method signature dễ hiểu
  • Code control bên ngoài dễ hiểu hơn khi tách biệt code bussiness và handle lỗi, mình cũng sẽ có bài về phần này

Nhưng kèm đó cũng có nhược điểm là không còn stack trace, từ đó việc trace lỗi sẽ khó khăn. Mời các bạn đón xem bài tiếp theo để giải quyết.

updatedupdated2020-11-292020-11-29
Load Comments?