Trong bài viết này, thông qua một số ví dụ nhỏ, tôi muốn đề cập đến việc sử dụng các công cụ Profiler (tạm dịch là công cụ đo đạc hiệu năng) để cải thiện tốc độ của từng phần mã lệnh trong các phương thức, từ đó từng bước nâng cao tốc độ của toàn bộ chương trình.

Yêu cầu của ví dụ minh họa cho bài viết này là phát triển một hàm kiểm tra một chuỗi đầu vào có phải là địa chỉ email hợp lệ hay không? Netbeans được sử dụng để viết mã lệnh do có sẵn Profiler tích hợp bên trong. Nếu các bạn sử dụng các công cụ khác như Eclipse hoặc IntelliJ IDEA có thể cài đặt tích hợp thêm VisualVM vào các môi trường phát triển này.

Phát triển chức năng theo TDD

Để lập trình chức năng đặt ra ở trên, chúng ta sẽ tạo một lớp đối tượng có tên là ValidationUtil, trong đó có một hàm verifyEmail(s) có chức năng kiểm tra chuỗi đầu vào s có phải là một địa chỉ email hợp lệ hay không?

public class ValidationUtil {
    public static boolean verifyEmail(String email) {
		throw new UnsupportedOperationException();
    }
}

Tạo lớp đối tượng ValidationUtilTest trong thư mục src/test/java, trong đó viết các hàm kiểm thử cho verifyEmail():

public class ValidationUtilTest {

    @Test
    public void test_valid_emails() {
        String[] sample_valid_emails = new String[] {
            "abc@example.com", "abc01@example.com", "a@example.com",
            "too_long_email_address@sub.example.com"};

        for(String sample:sample_valid_emails) {
			Assert.assertTrue(ValidationUtil.verifyEmail(sample));
		}
    }
    
    @Test
    public void test_invalid_emails() {
        String[] sample_invalid_emails = new String[] {
            "", "#ABCDEF$", "Wrong Email", "someone@abc",
            "domain.only.vn", "without#at.abc"};

        for(String sample:sample_invalid_emails) {
            Assert.assertFalse(ValidationUtil.verifyEmail(sample));
        }
    }
}

Chạy kiểm thử ValidationUtilTest, đương nhiên kết quả fail đối với cả hai hàm test. Lập trình thật nhanh chưa cần tối ưu chức năng của hàm verifyEmail() để pass qua được các bộ test:

public class ValidationUtil {
    
    private static final String EMAIL_PATTERN_STRING =
		"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
		+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";
   
    public static boolean verifyEmail(String email) {
        if (email == null) return false;
        return email.matches(EMAIL_PATTERN_STRING);
    }
}

Chạy kiểm thử lại ValidationUtilTest, dựa trên kết quả test để tùy chỉnh mã lệnh cho đến khi pass qua bộ test.

Áp dụng profiling để cải tiến tốc độ

Nếu chỉ dừng lại tại đây, chúng ta đã phát triển xong chức năng kiểm tra tính hợp lệ của địa chỉ email, đáp ứng được bộ Test đặt ra. Tuy nhiên, do phát triển nhanh để pass qua bộ test, chúng ta chưa cân nhắc đến tốc độ. Sau khi đã đảm bảo pass qua Test, đây là thời điểm xem xét đến hiệu năng của đoạn mã lệnh vừa tạo ra.

Trong trường hợp ví dụ của chúng ta, có thể dễ dàng nhận ra hàm String.matches() không thực sự hiệu quả nếu được sử dụng với tần suất lớn, do mỗi lần gọi hàm chương trình đều tạo ra ngầm bên trong một đối tượng Pattern để biên dịch mẫu EMAIL_PATTERN_STRING, quá trình này khá tốn kém thời gian. Với nhận định trên, ta có thể cải tiến bằng cách tạo đối tượng Pattern để biên dịch mẫu đối sánh EMAIL_PATTERN_STRING một lần từ đầu trong lớp ValidationUtil, sau đó mỗi lần kiểm tra không cần tạo lại nữa.

Để tiện cho việc so sánh, chúng ta tạo thêm một hàm verifyEmail2() chứa đoạn mã cải tiến:

public class ValidationUtil {
    
    private static final String EMAIL_PATTERN_STRING =
		"^[_A-Za-z0-9-\\+]+(\\.[_A-Za-z0-9-]+)*@"
		+ "[A-Za-z0-9-]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";

    public static boolean verifyEmail(String email) {
        if (email == null) return false;
        return email.matches(EMAIL_PATTERN_STRING);
    }

    private static final Pattern EMAIL_PATTERN =
            Pattern.compile(EMAIL_PATTERN_STRING);
    
    public static boolean verifyEmail2(String email) {
        if (email == null) return false;
        Matcher matcher = EMAIL_PATTERN.matcher(email);
        return matcher.matches();
    }
}

Trong ValidationUtilTest bổ sung thêm các đoạn mã lệnh kiểm thử hàm verifyEmail2():

public class ValidationUtilTest {

    @Test
    public void test_valid_emails() {
        String[] sample_valid_emails = new String[] {
            "abc@example.com", "abc01@example.com", "a@example.com",
            "too_long_email_address@sub.example.com"};

        for(int i=0; i<1000; i++) {
            for(String sample:sample_valid_emails) {
                Assert.assertTrue( ValidationUtil.verifyEmail(sample) );
            }
        }
        
        for(int i=0; i<1000; i++) {
            for(String sample:sample_valid_emails) {
                Assert.assertTrue( ValidationUtil.verifyEmail2(sample) );
            }
        }
    }
    
    @Test
    public void test_invalid_emails() {
        String[] sample_invalid_emails = new String[] {
            "", "#ABCDEF$", "Wrong Email",
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567"};

        for(String sample:sample_invalid_emails) {
            Assert.assertFalse( ValidationUtil.verifyEmail(sample) );
        }

        for(String sample:sample_invalid_emails) {
            Assert.assertFalse( ValidationUtil.verifyEmail2(sample) );
        }
    }
}

Sau khi chạy kiểm thử lại ValidationUtilTest để đảm bảo verifyEmail2() pass qua bộ test, chúng ta chạy Profile cho file ValidationUtilTest như sau:

Hình 1. Kích hoạt Profiler để phân tích tệp Test

Chọn chức năng cần đo đạc là CPU. Profiler sẽ yêu cầu thiết lập tùy chọn, hãy chọn Advanced > Edit Profiling Roots để xác định các phương thức cần đo đạc tốc độ như trong hình sau:

Hình 2. Chọn các phương thức cần thực hiện đo đạc

Sau khi đã thiết lập xong tùy chọn, click chuột vào Run để chạy profiling. Khi kết thúc, Profiler hiển thị hộp thoại xác nhận có muốn ghi lại kết quả đã đo đạc không? hãy chọn Yes:

Hình 3. Xác nhận ghi lại kết quả đo đạc

Profiler sẽ hiển thị cửa sổ kết quả như sau:

Hình 4. Kết quả đo đạc tốc độ thực thi của các phương thức được chọn

Nhìn vào kết quả đo đạc, ta có thể dễ dàng thấy cả 2 hàm verifyEmail()verifyEmail2() đều thực thi 4004 lần, tuy nhiên tổng thời gian thực thi của verifyEmail() là 346ms, chiếm 70.9% tổng thời gian thực thi ứng dụng, trong khi hàm verifyEmail2() chỉ mất tổng thời gian thực thi là 24.8ms, chiếm 5.1%. Như vậy, chúng ta có đủ cơ sở để thay hàm verifyEmail() bởi hàm verifyEmail2(). Việc này khá đơn giản, chỉ cần hoán đổi tên của 2 hàm trên.

Kết luận

Qua ví dụ trên, chúng ta có thể thấy Profiler là công cụ đắc lực trong việc đo đạc hiệu năng của các hàm, làm cơ sở để chọn lựa hàm có hiệu năng tốt hơn. Hơn nữa, kết hợp với Unit Testing sẽ giúp chúng ta cải thiện dần từng bước tốc độ từng phần của ứng dụng, từ đó góp phần cải tiến toàn bộ chương trình.

Các bạn có thể tải mã nguồn đầy đủ của ví dụ minh họa để chạy thử.

Comments