Thursday, May 05, 2005

Memory Leak TestCase

ตัวเนื้อหาได้มาจาก blog ของ Tim Boudreau

ปัญหาที่พบบ่อยในการเขียน Swing ก็คือ Memory leak
ซึ่งเกิดการการ add listener ระหว่าง component ต่างๆ
แล้วไม่มีการ remove ออก

ใน project netbean ซึ่งเป็น swing ขนาดใหญ่
ก็มีการจัดการกับปัญหานี้ผ่านทาง testcase + INSANE
โดย testcase ของเขามี pattern ดังนี้

SomeObject o = new SomeObject();
something.doSomethingWith (o);
WeakReference ref = new WeakReference (o);
o = null;
assertGC ("Object still referenced", ref);

object o เป็น object ที่เราสนใจว่ามีการ leak
เกิดขึ้นหรือไม่ โดยสังเกตุว่ามีการสั่ง o = null ซึ่ง
เป็นการ clear referenece ที่อ้างถึงภายใน method นี้แล้ว
ดังนั้นถ้ามี gc เกิดขึ้นเมื่อไร object o ควรจะหายไปจาก Heap
ซึ่งวิธีการตรวจสอบก็คือ wrap object o ด้วย
WeakReference ก่อนแล้วค่อยส่งไปตรวจด้วย method assertGC

ใน method assertGC มี code เขียนไว้ดังนี้
    public static void assertGC(String text, java.lang.ref.Reference ref) {
ArrayList alloc = new ArrayList ();
int size = 100000;
for (int i = 0; i < 50; i++) {
if (ref.get() == null) {
return;
}
System.gc();
System.runFinalization();
try {
alloc.add (new byte[size]);
size = (int)(((double)size) * 1.3);
} catch (OutOfMemoryError error) {
size = size / 2;
}
try {
if (i % 3 == 0) Thread.sleep(321);
} catch (InterruptedException t) {
// ignore
}
}
alloc = null;
fail(text + ":\n" + findRefsFromRoot(ref.get()));
}

หลักๆ ก็คือวน loop 50 ครั้งเพื่อ check ว่า
เจ้า object ที่อยู่ใน weak reference หายไปหรือยัง
โดยแต่ละครั้งก็พยายาม force gc ให้เกิด
โดยมีการ simulate การใช้ heap ผ่านทาง
ArrayList alloc

กรณีที่ loop ครบ 50 ครั้งแล้ว
ref.get() != null
ก็แสดงว่า memory leak แล้ว
ก็ call findRefsFromRoot ซึ่งมีการทำงานดังนี้
    private static String findRefsFromRoot(final Object target) {
final Map objects = new IdentityHashMap();
boolean found = false;

Visitor vis = new Visitor() {
public void visitClass(Class cls) {}

public void visitObject(ObjectMap map, Object object) {
objects.put(object, new Entry(object));
}

public void visitArrayReference(ObjectMap map, Object from, Object to, int index) {
visitRef(from, to);
}

public void visitObjectReference(ObjectMap map, Object from, Object to, java.lang.reflect.Field ref) {
visitRef(from, to);
}

private void visitRef(Object from, Object to) {
((Entry)objects.get(from)).addOut(to);
((Entry)objects.get(to)).addIn(from);
if (to == target) throw new RuntimeException("Done");
}


public void visitStaticReference(ObjectMap map, Object to, java.lang.reflect.Field ref) {
((Entry)objects.get(to)).addStatic(ref);
if (to == target) throw new RuntimeException("Done");
}
};

try {
ScannerUtils.scanExclusivelyInAWT(ScannerUtils.skipNonStrongReferencesFilter(), vis, ScannerUtils.interestingRoots());
} catch (Exception ex) {
// found object
found = true;
}

if (found) {
return findRoots(objects, target);
} else {
return "Not found!!!";
}
}

ซึ่งเป็นการ dump เอา Reference ทั้งหมดออกมา
ผมยังไม่ได้หา javadoc ของ ScannerUtils ดู
แต่เดาว่า skipNonStrongReferencesFilter คงจะ
เป็นการ scan หาเฉพาะพวกที่อยู่ใน WeakReference เท่านั้น

ที่นี้ลองดูตัวอย่างที่ Tim เขาเขียนไว้
public class MyFrame extends javax.swing.JFrame {
MenuAction menuAction = new MenuAction();
public MyFrame() {
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
setBounds (20, 20, 300, 300);
getContentPane().addMouseListener (new MouseAdapter() {
public void mouseReleased (MouseEvent me) {
getPopupMenu().show((Component) me.getSource(), me.getX(), me.getY());
}
});
}

JPopupMenu getPopupMenu() {
JPopupMenu menu = new JPopupMenu();
menu.add (new JMenuItem (menuAction));
return menu;
}

static final class MenuAction extends AbstractAction {
public MenuAction() {
putValue (Action.NAME, "Do Something");
}
public void actionPerformed (ActionEvent ae) {
System.out.println("Action performed");
}
}
}

ผลลัพท์ที่ได้จากการ Test ออกมาหน้าตาประมาณนี้
Testcase: testLeak(javaapplication7.MyFrameTest):       FAILED
Popup menu should have been collected:
private static java.awt.Component java.awt.KeyboardFocusManager.focusOwner->
javaapplication7.MyFrame@d1e89e->
javaapplication7.MyFrame$MenuAction@f17a73->
javax.swing.event.SwingPropertyChangeSupport@3526b0->
javax.swing.event.EventListenerList@3ddcf1->
[Ljava.lang.Object;@105c1->
javax.swing.JMenuItem$1@f74864->
javax.swing.JMenuItem@110003->
javax.swing.JPopupMenu@627086

จะเห็นว่า leak ครั้งนี้ขึ้นใน JMenuItem
ของ JMenuItem ยัง hold reference ถึง MyFrame$MenuAction อยู่
โดยตัวที่ทำให้ leak ก็คือ menu.add(new JMenuItem(menuAction))
ซึ่งกรณีที่เรา new JMenuItem จะเกิดการ add listener
ระหว่าง MenuAction กับ MenuItem
ผลก็คือ PopupMenu ยังมี link จาก menuItem วิ่งไป menuAction

สรุป

โดยปกติเวลาเราสงสัยว่า memory leak หรือไม่ก็ต้องใช้
พวก Profiler ทำการใล่ trace ดู
แต่ที่ Tim เขาแสดงให้ดูก็คือ เราสามารถเขียน Testcase
เพื่อดักจับไว้ได้ก่อนเลย ทำให้เมื่อ project evolve
ไป ถ้าเกิดมีใครมาแก้อะไรแล้วทำให้ memory leak
ตัว testcase ก็จะฟ้องออกมาทันที

Related link from Roti

1 comment:

bact' said...

อันนี้น่าสนแฮะ :)