Bài viết nằm trong series Java memory management & performance.
Với bài viết đầu tiên, cùng nhìn sơ lược về cách JVM thực thi các đoạn code của chúng ta như thế nào nhé.
1) WORA
Java nổi tiếng với WORA, nghĩa là Write once, run anywhere, viết một lần và chạy mọi nơi. Oh shit, wtf… làm sao lại có một ngôn ngữ thần thánh như vậy, bí ẩn phía sau nó là gì?
Lấy ví dụ thực tế, nếu mình chỉ biết tiếng Việt, liệu mình có thể đi du lịch nước ngoài được không? Làm sao mình giao tiếp được với người bản địa. Chẳng có nhẽ với mỗi một quốc gia khác nhau mình phải học ngôn ngữ của họ.
Có 3 cách cơ bản để giải quyết vấn đề trên:
- Thứ nhất, tham dự một khóa học ngoại ngữ cấp tốc để giao tiếp cơ bản được với người bản địa. Khá tốn, không khả thi.
- Thứ hai, cũng đi học ngoại ngữ nhưng là international language. Không khác cách đầu tiên mấy.
- Thứ ba, thuê luôn tour guide cho nhàn, giàu nữa thì chơi hẳn interpreter cho máu. Có vẻ khả thi nhất rồi. Vẫn hơi tốn kém, thôi dùng cái máy thông dịch (khá phổ biến hiện nay) tầm 5 – 8 củ là ngon nghẻ rồi.
Như vậy, bài toán được giải quyết đơn giản với cách thứ ba, đầu tư một lần, dùng nhiều lần. Không cần biết tất cả các loại ngôn ngữ, chỉ cần biết một và.. đi đâu cũng dùng được với chiếc máy interpreter nhỏ gọn.
Java dựa trên idea này để thực hiện WORA, viết một lần dùng mọi nơi. Interpreter lúc này là JVM và compiler. Các ngôn ngữ của từng quốc gia là các hệ điều hành như Windows, MacOS, Linux… và ngôn ngữ chúng ta nói là các đoạn code Java.
Quá trình để một đoạn code Java được thực thi với WORA diễn ra như sau:
- Mua một cái máy interpreter trước, hay nói cách khác là cài đặt JDK (Java development kit). Nếu chưa rõ các bạn có thể tìm đọc thêm về JDK, JRE và JVM nhé.
- Sau đó, nói vào máy thông dịch để nó ghi nhận thông tin. Tương ứng với việc viết code trong file .java.
- Lúc này máy thực hiện chuyển ngôn ngữ của chúng ta sang dạng ngôn ngữ máy có thể hiểu được. Cụ thể đó là chuyển thông tin từ dạng âm thanh sang dạng tín hiệu điện từ để tiến hành phân tích. Với Java, ta phải làm việc này thủ công, đó là thực hiện biên dịch các file .java sang file .class chứa các bytecode là ngôn ngữ của JVM để được thực thi.
- Cuối cùng, interpreter dịch sang ngôn ngữ bản địa và phát âm thanh. Tương ứng với JVM dịch bytecode sang machine code phù hợp với từng OS khác nhau để thực thi.
Trong thực tế, ứng dụng bao gồm rất nhiều file, sau khi compile ta sẽ nén nó thành một file duy nhất cho gọn, thường là .jar hoặc .war. Đem file jar này đi chạy trên OS nào cũng được, với điều kiện đã có JRE. Write once, run anywhere là như vậy.
2) JIT Compiler
Bỏ qua phần compile code, để start một Java application với jar file, ta sử dụng câu lệnh:
$ java -jar file-name.jar
Sau khi chạy, một JVM mới được tạo ra và thực thi trên đó. Như vậy, mỗi một application chạy trên một JVM riêng biệt, nếu có được hỏi phỏng vấn thì cứ mạnh dạn trả lời nhé .
JVM có nhiệm vụ thông dịch các đoạn code sang mã máy để OS thực thi. Như vậy, có thể nói Java là interpreter language. Nếu bàn về tốc độ thực thi thì không có cửa so với các ngôn ngữ compiled language như C, C++ hay C#…
Các ngôn ngữ như C hay C# được trực tiếp biên dịch (compile) ra mã máy nên khi thực thi sẽ nhanh hơn do không tốn công chuyển đổi (interpret) từ bytecode sang native code như Java. Do đó, với compiled language, mỗi khi chạy trên OS khác nhau cần compile lại code để phù hợp với OS đó, không có WORA như Java, bù lại tốc độ thực thi nhanh hơn.
Tuy nhiên nó chỉ là bề nổi của tảng băng chìm, các kĩ sư của Sun/Oracle đã thiết kế JVM với hàng loạt tính năng và thuật toán phức tạp để làm chương trình hoạt động hiệu quả hơn là việc đơn thuần sử dụng interpreter.
JVM sẽ monitor xem đoạn code nào được thực thi nhiều lần. Đoạn code đó có thể là method, một phần của method hoặc một vòng lặp (loop). Sau khi tìm được block of code đó, thay vì mỗi lần thực thi phải interpret sang machine code thì nó được compile sang machine code luôn cho nhanh. Tất nhiên sau khi compile phải có nơi lưu trữ lại native code đó, là code cache. Như vậy, tại một thời điểm, một vài phần của chương trình sẽ không chạy trong trạng thái interpretive nữa mà là thực thi luôn native code.
Việc interpret từ bytecode sang native code hoặc quyết định compile và lưu trữ lại native code đó hay không là nhiệm vụ của Just-in-time Compiler, ngắn gọn hơn là JIT Compiler.
Machine code hay nói cách khác là native code là những đoạn code mà OS có thể hiểu ngay được để thực thi, không cần thông qua interpreter. JIT Compiler là trái tim của JVM, có nhiệm vụ thông dịch bytecode sang native code và giúp cho Java application chạy nhanh hơn. Ngoài ra, nó cũng là lý do vì sao khi benchmark những đoạn code chạy lần đầu luôn chậm hơn các lần sau đó.
Thực ra không cần đi quá sâu vào phần này nếu bạn không cần optimize application. Nên nếu muốn optimize application ta cần hiểu rõ hơn về nó. Let’s continue .
Những đoạn code được chạy thường xuyên (hot code) sẽ được JIT Compilercompile thành native code để tăng tốc độ thực thi. Tương tự, method nào cả ngày chỉ chạy có một vài lần thì.. thôi mỗi lần chạy là một lần interpret cũng được, để dành memory lưu trữ cho các đoạn code khác cần thiết hơn.
Việc compile từ Java bytecode sang native code được thực hiện trên thread độc lập với quá trình interpreter. Bản chất JVM cũng là một application, và là multi-thread application. Một thread thực hiện interpretbytecode sang native code, thread khác thực thi native code đó, và một thread compile bytecode sang native code lưu trữ trên code cache. Do vậy, việc compile sang native code không có bất kì ảnh hưởng gì đến quá trình run application. Nếu chưa có sẵn native code, JVM vẫn sử dụng interpreter để thực thi code. Và khi native code đã sẵn sàng, JVM sẽ chuyển sang dùng luôn, chả tội gì phải interpret nữa.
3) Practice
Đến giờ thực hành rồi, cùng tìm hiểu xem dòng code nào được compile sang native code. Bài toán đơn giản như sau, in ra 10 số nguyên tố đầu tiên. Tạo file Application.java và code thôi:
importjava.util.ArrayList;importjava.util.List;classPrimeNumberGenerator{privatebooleanisPrime(int n){finaldouble result =Math.sqrt(n);for(int i =2; i <= result; i++){if(n % i ==0){returnfalse;}}returntrue;}privateintgetNextPrimeAbove(int previous){int number = previous +1;while(!isPrime(number)){++number;}return number;}publicvoidgenerateNumbers(int count){finalList<Integer> primes =newArrayList<>();
primes.add(2);int next =2;while(primes.size()<= count){
next =getNextPrimeAbove(next);
primes.add(next);}System.out.println(primes);}}publicclassApplication{publicstaticvoidmain(String[] args){finalPrimeNumberGenerator generator =newPrimeNumberGenerator();
generator.generateNumbers(10);}}
Với Java 8, ta cần thực hiện compile sang bytecode trước khi chạy ứng dụng, tức là cần 2 câu lệnh. Tuy nhiên với Java 11, chỉ cần 1 câu lệnh, nó sẽ thực hiện compile on the fly luôn (chỉ thực hiện được với chương trình có 1 class duy nhất).
Hiện tại mình đang sử dụng Java 11 nhé.
$ java -version
java version "11.0.11"2021-04-20 LTS
Java(TM) SE Runtime Environment 18.9(build 11.0.11+9-LTS-194)
Java HotSpot(TM)64-Bit Server VM 18.9(build 11.0.11+9-LTS-194, mixed mode)
Nguy hiểm tí thôi, mình thực hiện luôn trên IDE cho nhanh. Hoặc dùng command line như bên dưới cho nguy hiểm.
$ javac Application.java
$ java Application
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
Thêm JVM options khi chạy -XX:+PrintCompilation, nó sẽ in thêm các thông tin về quá trình code compilation. Nói qua về JVM options trước, nó là các tham số.. nâng cao trong quá trình thực thi code. Phần lớn JVM options tuân theo format chia làm 3 phần:
- Phần đầu tiên -XX:
- Phần tiếp theo là dấu + hoặc – nhằm mục đích bật/tắt các options.
- Phần cuối là tên của options dưới dạng CamelCase, viết hoa các chữ cái đầu tiên của mỗi từ.
JVM options sẽ được thêm ngay sau keywork java khi thực thi chương trình, ví dụ:
$ java -XX:+PrintCompilation Application
Sau khi thêm -XX:+PrintCompilation, kết quả là:
2713 java.lang.StringLatin1::hashCode (42 bytes)2823 java.lang.Object::<init>(1 bytes)2833 java.lang.String::hashCode (49 bytes)2853 java.lang.String::isLatin1 (19 bytes)2843 java.lang.String::coder (15 bytes)2863 java.util.ImmutableCollections$SetN::probe (56 bytes)2873 java.lang.Math::floorMod (10 bytes)2983 java.lang.Math::floorDiv (22 bytes)29103 java.lang.StringLatin1::equals (36 bytes)29113 java.util.ImmutableCollections$SetN::hashCode (46 bytes)2993 java.lang.String::equals (65 bytes)30123 java.util.ImmutableCollections::emptySet (4 bytes)30133 java.util.Set::of (4 bytes)30154 java.lang.StringLatin1::hashCode (42 bytes)30143 java.util.Objects::equals (23 bytes)30163 java.lang.module.ModuleDescriptor$Exports::hashCode (38 bytes)31193 java.util.Objects::requireNonNull (14 bytes)31173 java.util.AbstractCollection::<init>(5 bytes)31183 java.util.ImmutableCollections$AbstractImmutableCollection::<init>(5 bytes)31203 java.util.ImmutableCollections$AbstractImmutableSet::<init>(5 bytes)31223 java.util.Set::of (66 bytes)32231 java.lang.module.ModuleDescriptor::name (5 bytes)32241 java.lang.module.ModuleReference::descriptor (5 bytes)3213 java.lang.StringLatin1::hashCode (42 bytes) made not entrant
33214 java.lang.Object::<init>(1 bytes)3323 java.lang.Object::<init>(1 bytes) made not entrant
36253 java.lang.String::charAt (25 bytes)36263 java.lang.StringLatin1::charAt (28 bytes)37273 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)37283 jdk.internal.misc.Unsafe::getObjectAcquire (7 bytes)3730 n 0 jdk.internal.misc.Unsafe::getObjectVolatile (native)38293 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes)38313 java.util.concurrent.ConcurrentHashMap::spread (10 bytes)38333 java.util.ImmutableCollections$SetN$SetNIterator::next (47 bytes)38323 java.util.ImmutableCollections$SetN$SetNIterator::nextIndex (56 bytes)38341 java.util.KeyValueHolder::getKey (5 bytes)39351 java.util.KeyValueHolder::getValue (5 bytes)39363 java.util.ImmutableCollections$MapN::probe (60 bytes)39383 java.util.ImmutableCollections$MapN::get (35 bytes)39393 java.util.HashMap::hash (20 bytes)39373 java.util.KeyValueHolder::<init>(21 bytes)40413 java.util.concurrent.ConcurrentHashMap::addCount (289 bytes)4042 n 0 jdk.internal.misc.Unsafe::compareAndSetLong (native)4045 n 0 jdk.internal.misc.Unsafe::compareAndSetObject (native)4043!3 java.util.concurrent.ConcurrentHashMap::putVal (432 bytes)42443 java.util.concurrent.ConcurrentHashMap$Node::<init>(20 bytes)42483 java.util.concurrent.ConcurrentHashMap::putIfAbsent (8 bytes)42463 java.lang.String::length (11 bytes)4250 n 0 java.lang.System::arraycopy (native)(static)42473 java.util.concurrent.ConcurrentHashMap::casTabAt (21 bytes)43401 java.lang.module.ResolvedModule::reference (5 bytes)43491 java.util.ImmutableCollections$SetN::size (5 bytes)43523 java.util.HashMap::getNode (148 bytes)44543 java.util.HashMap::putVal (300 bytes)4459 n 0 java.lang.Object::hashCode (native)45533 java.util.HashMap::put (13 bytes)45553 java.util.HashMap$Node::<init>(26 bytes)45563 java.util.HashMap::newNode (13 bytes)45573 java.util.HashMap::afterNodeInsertion (1 bytes)46601 java.lang.module.ModuleDescriptor$Exports::source (5 bytes)46513 java.util.HashMap::get (23 bytes)46613 java.util.ImmutableCollections$Set12$1::hasNext (13 bytes)46643 java.util.ImmutableCollections$Set12$1::next (92 bytes)46581 java.lang.module.ModuleDescriptor::isAutomatic (5 bytes)46621 java.lang.module.ResolvedModule::configuration (5 bytes)46631 java.lang.module.ModuleDescriptor$Exports::targets (5 bytes)47653 java.util.Map::entry (10 bytes)47663 java.util.HashSet::add (20 bytes)47671 java.lang.module.ModuleDescriptor::isOpen (5 bytes)48684 java.lang.String::hashCode (49 bytes)48693 java.util.HashMap::resize (356 bytes)4876 n 0 java.lang.Module::addExportsToAllUnnamed0 (native)(static)49713 jdk.internal.module.ModuleBootstrap$2::hasNext (30 bytes)49703 java.util.ImmutableCollections$Set12::size (13 bytes)49743 jdk.internal.module.ModuleBootstrap$2::next (52 bytes)49753 java.util.HashMap::putIfAbsent (13 bytes)49771 java.lang.Module::getDescriptor (5 bytes)50783 java.lang.CharacterDataLatin1::getProperties (11 bytes)5133 java.lang.String::hashCode (49 bytes) made not entrant
51734 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes)51293 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes) made not entrant
51724 java.util.HashMap::afterNodeInsertion (1 bytes)51573 java.util.HashMap::afterNodeInsertion (1 bytes) made not entrant
52793 java.lang.StringLatin1::indexOf (61 bytes)61684 java.lang.String::hashCode (49 bytes) made not entrant
62803 java.lang.AbstractStringBuilder::ensureCapacityInternal (39 bytes)[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31]
Vãi nhái thật , thế này ai chơi. Hít mấy hơi đọc tiếp xem quá trình compilation diễn ra thế nào.
Nhìn sơ qua cách tổ chức thông tin compilation chia làm các column:
- Với column đầu tiên là các con số tăng dần 27, 28, 29… Nó là số đo thời gian biểu diễn dưới đơn vị miliseconds kể từ khi JVM bắt đầu chạy, chỉ có tăng chứ không giảm.
- Column tiếp theo là trình tự compile các method hoặc block of code. Nhưng tại sao chúng không sắp xếp theo thứ tự, đơn giản là do có những đoạn code gọi các method được định nghĩa ở các vị trí khác nhau, file khác nhau. Ngoài ra còn do compilation time. Ta chưa cần quan tâm đến column này lắm.
- Column tiếp theo đa số là các empty value, tuy nhiên có 2 giá trị ở đây là % và n: s là synchronized, n là native code, % là On Stack Replacement compilation. Tất cả có 5 giá trị chứ không chỉ có 2, discuss về nó sau nhé.
- Tiếp theo là column có đoạn giá trị từ 0 đến 4, nói về loại compilation. 0 là không cần compile, các giá trị 1 đến 4 là code compilation level theo chiều sâu. Nhìn lại dòng có cột thứ ba giá trị n, vì là native code nên không cần compile nữa, do đó nhận giá trị 0. Level càng cao thì quá trình compile càng nhiều bước, time càng lớn.
- Column tiếp theo là method name.
- Theo sau là size của đoạn bytecode đó.
Bạn có nhận ra điều gì kì lạ không, gợi ý ở column cuối cùng.
… waiting …
Đó là.. không thấy code của mình viết ở đâu nhỉ, không có bất kì một dòng nào ngoài System.out.println() cuối cùng .
Chạy lại với và in ra nhiều số nguyên tố hơn, 5000 cho máu, bỏ luôn System.out.println() cho đỡ vướng mắt.
2713 java.lang.StringLatin1::hashCode (42 bytes)2723 java.lang.Object::<init>(1 bytes)2733 java.lang.String::hashCode (49 bytes)2853 java.lang.String::isLatin1 (19 bytes)2843 java.lang.String::coder (15 bytes)2863 java.util.ImmutableCollections$SetN::probe (56 bytes)2873 java.lang.Math::floorMod (10 bytes)2883 java.lang.Math::floorDiv (22 bytes)29103 java.lang.StringLatin1::equals (36 bytes)29113 java.util.ImmutableCollections$SetN::hashCode (46 bytes)2993 java.lang.String::equals (65 bytes)29123 java.util.ImmutableCollections::emptySet (4 bytes)29154 java.lang.StringLatin1::hashCode (42 bytes)30133 java.util.Set::of (4 bytes)30143 java.util.Objects::requireNonNull (14 bytes)30163 java.lang.module.ModuleDescriptor$Exports::hashCode (38 bytes)30173 java.util.AbstractCollection::<init>(5 bytes)30183 java.util.ImmutableCollections$AbstractImmutableSet::<init>(5 bytes)30203 java.util.Set::of (66 bytes)31211 java.lang.module.ModuleDescriptor::name (5 bytes)31221 java.lang.module.ModuleReference::descriptor (5 bytes)3213 java.lang.StringLatin1::hashCode (42 bytes) made not entrant
32194 java.lang.Object::<init>(1 bytes)3323 java.lang.Object::<init>(1 bytes) made not entrant
34233 java.lang.String::charAt (25 bytes)35243 java.lang.StringLatin1::charAt (28 bytes)36253 java.util.concurrent.ConcurrentHashMap::tabAt (22 bytes)3628 n 0 jdk.internal.misc.Unsafe::getObjectVolatile (native)36263 jdk.internal.misc.Unsafe::getObjectAcquire (7 bytes)36293 java.util.ImmutableCollections$SetN$SetNIterator::nextIndex (56 bytes)37273 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes)37301 java.util.KeyValueHolder::getKey (5 bytes)38323 java.util.ImmutableCollections$MapN::probe (60 bytes)38353 java.util.ImmutableCollections$SetN$SetNIterator::next (47 bytes)38311 java.util.KeyValueHolder::getValue (5 bytes)38343 java.util.Objects::equals (23 bytes)38333 java.util.KeyValueHolder::<init>(21 bytes)39363 java.util.concurrent.ConcurrentHashMap::addCount (289 bytes)3939 n 0 jdk.internal.misc.Unsafe::compareAndSetLong (native)3942 n 0 jdk.internal.misc.Unsafe::compareAndSetObject (native)3940!3 java.util.concurrent.ConcurrentHashMap::putVal (432 bytes)41453 java.util.concurrent.ConcurrentHashMap::putIfAbsent (8 bytes)41383 java.util.concurrent.ConcurrentHashMap::spread (10 bytes)41413 java.util.concurrent.ConcurrentHashMap$Node::<init>(20 bytes)41443 java.lang.String::length (11 bytes)41433 java.util.concurrent.ConcurrentHashMap::casTabAt (21 bytes)42463 java.util.HashMap::hash (20 bytes)42371 java.lang.module.ResolvedModule::reference (5 bytes)4248 n 0 java.lang.System::arraycopy (native)(static)42471 java.util.ImmutableCollections$SetN::size (5 bytes)42493 java.util.HashMap::get (23 bytes)43513 java.util.HashMap::getNode (148 bytes)43533 java.util.HashMap::putVal (300 bytes)4359 n 0 java.lang.Object::hashCode (native)44523 java.util.HashMap::put (13 bytes)44543 java.util.HashMap$Node::<init>(26 bytes)44553 java.util.HashMap::newNode (13 bytes)44573 java.util.HashMap::afterNodeInsertion (1 bytes)45563 java.util.ImmutableCollections$Set12$1::hasNext (13 bytes)45601 java.lang.module.ModuleDescriptor$Exports::source (5 bytes)45503 java.lang.module.ResolvedModule::name (11 bytes)45581 java.lang.module.ModuleDescriptor::isAutomatic (5 bytes)45611 java.lang.module.ResolvedModule::configuration (5 bytes)45621 java.lang.module.ModuleDescriptor$Exports::targets (5 bytes)46633 java.util.Map::entry (10 bytes)46643 java.util.HashSet::add (20 bytes)46651 java.lang.module.ModuleDescriptor::isOpen (5 bytes)46664 java.lang.String::hashCode (49 bytes)47673 java.util.ImmutableCollections$Set12::size (13 bytes)47693 jdk.internal.module.ModuleBootstrap$2::hasNext (30 bytes)47703 java.util.HashMap::resize (356 bytes)4775 n 0 java.lang.Module::addExportsToAllUnnamed0 (native)(static)48713 jdk.internal.module.ModuleBootstrap$2::next (52 bytes)48743 java.util.HashMap::putIfAbsent (13 bytes)49763 java.util.ImmutableCollections$MapN::get (35 bytes)49731 java.lang.Module::getDescriptor (5 bytes)4933 java.lang.String::hashCode (49 bytes) made not entrant
49684 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes)50773 java.lang.CharacterDataLatin1::getProperties (11 bytes)50273 java.util.ImmutableCollections$SetN$SetNIterator::hasNext (13 bytes) made not entrant
50724 java.util.HashMap::afterNodeInsertion (1 bytes)50573 java.util.HashMap::afterNodeInsertion (1 bytes) made not entrant
51783 java.lang.StringLatin1::indexOf (61 bytes)56664 java.lang.String::hashCode (49 bytes) made not entrant
57793 java.lang.AbstractStringBuilder::ensureCapacityInternal (39 bytes)59803 PrimeNumberGenerator::isPrime (34 bytes)59823 java.lang.Number::<init>(5 bytes)59833 java.lang.Integer::<init>(10 bytes)59874 PrimeNumberGenerator::isPrime (34 bytes)59863 PrimeNumberGenerator::getNextPrimeAbove (20 bytes)59843 java.util.ArrayList::add (25 bytes)60811 java.util.ArrayList::size (5 bytes)60853 java.util.ArrayList::add (23 bytes)60803 PrimeNumberGenerator::isPrime (34 bytes) made not entrant
60883 java.lang.Integer::valueOf (32 bytes)6089 % 4 PrimeNumberGenerator::isPrime @ 9(34 bytes)61863 PrimeNumberGenerator::getNextPrimeAbove (20 bytes) made not entrant
61903 PrimeNumberGenerator::getNextPrimeAbove (20 bytes)62914 PrimeNumberGenerator::getNextPrimeAbove (20 bytes)63903 PrimeNumberGenerator::getNextPrimeAbove (20 bytes) made not entrant
Đã thấy có chút khác biệt, colum thứ ba có thêm giá trị ! và column cuối đã xuất hiện những dòng code chúng ta viết.
Chú ý vào dòng có giá trị %, method isPrime() được gọi rất nhiều lần và được đặt vào code cache, chính là nơi lưu trữ native code từ quá trình JIT Compiler. Mình sẽ nói cụ thể hơn về nó ở phần sau.
Nếu đọc đến đây thì xin chúc mừng, bạn khá kiên nhẫn đấy . Đón chờ bài tiếp theo về Code cache và Compiler nhé.
Reference
Reference in series https://viblo.asia/s/java-memory-management-performance-vElaB80m5kw
© Dat Bui
Nguồn: viblo.asia