You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@netbeans.apache.org by jt...@apache.org on 2019/05/04 11:05:08 UTC

[netbeans-html4j] 01/07: Assert that unused FX presenters can be GCed

This is an automated email from the ASF dual-hosted git repository.

jtulach pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/netbeans-html4j.git

commit a458fd4bb91836518d9a6a831eb34ff2ea9b888d
Author: Jaroslav Tulach <ja...@apidesign.org>
AuthorDate: Sat May 4 07:30:13 2019 +0200

    Assert that unused FX presenters can be GCed
---
 .../html/boot/impl/JavaScriptProcesor.java         |   7 +-
 .../main/java/org/netbeans/html/boot/spi/Fn.java   | 127 ++++++++++++---------
 ko4j/pom.xml                                       |   6 +
 .../java/org/netbeans/html/ko4j/CacheObjs.java     |   8 +-
 .../main/java/org/netbeans/html/ko4j/Knockout.java |  15 ++-
 .../main/java/org/netbeans/html/ko4j/MapObjs.java  |  50 +++++---
 .../org/netbeans/html/ko4j/DoubleViewTest.java     |  41 ++++++-
 7 files changed, 174 insertions(+), 80 deletions(-)

diff --git a/boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java b/boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
index 7e13130..3f259fa 100644
--- a/boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
+++ b/boot/src/main/java/org/netbeans/html/boot/impl/JavaScriptProcesor.java
@@ -400,16 +400,16 @@ public final class JavaScriptProcesor extends AbstractProcessor {
             source.append("package ").append(pkgName).append(";\n");
             source.append("public final class $JsCallbacks$ {\n");
             source.append("  static final $JsCallbacks$ VM = new $JsCallbacks$(null);\n");
-            source.append("  private final org.netbeans.html.boot.spi.Fn.Presenter p;\n");
+            source.append("  private final java.lang.ref.Reference<org.netbeans.html.boot.spi.Fn.Presenter> ref;\n");
             source.append("  private $JsCallbacks$ next;\n");
             source.append("  private $JsCallbacks$(org.netbeans.html.boot.spi.Fn.Presenter p) {\n");
-            source.append("    this.p = p;\n");
+            source.append("    this.ref = new java.lang.ref.WeakReference<org.netbeans.html.boot.spi.Fn.Presenter>(p);\n");
             source.append("  }\n");
             source.append("  synchronized final $JsCallbacks$ current() {\n");
             source.append("    org.netbeans.html.boot.spi.Fn.Presenter now = org.netbeans.html.boot.spi.Fn.activePresenter();\n");
             source.append("    $JsCallbacks$ thiz = this;\n");
             source.append("    for (;;) {\n");
-            source.append("      if (now == thiz.p) return thiz;\n");
+            source.append("      if (now == thiz.ref.get()) return thiz;\n");
             source.append("      if (thiz.next == null) {\n");
             source.append("        return thiz.next = new $JsCallbacks$(now);\n");
             source.append("      }\n");
@@ -486,6 +486,7 @@ public final class JavaScriptProcesor extends AbstractProcessor {
             sep = ", ";
         }
         source.append(") throws Throwable {\n");
+        source.append("    org.netbeans.html.boot.spi.Fn.Presenter p = ref.get(); \n");
         source.append(convert);
         if (useTryResources()) {
             source.append("    try (java.io.Closeable a = org.netbeans.html.boot.spi.Fn.activate(p)) { \n");
diff --git a/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java b/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java
index e70ad91..3dc4e30 100644
--- a/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java
+++ b/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java
@@ -23,6 +23,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import java.net.URL;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -38,8 +40,7 @@ import org.netbeans.html.boot.impl.FnContext;
  * @author Jaroslav Tulach
  */
 public abstract class Fn {
-    private static Map<String, Set<Presenter>> LOADED;
-    private final Presenter presenter;
+    private final Reference<Presenter> presenter;
     
     /**
      * @deprecated Ineffective as of 0.6. 
@@ -57,7 +58,7 @@ public abstract class Fn {
      * @since 0.6 
      */
     protected Fn(Presenter presenter) {
-        this.presenter = presenter;
+        this.presenter = new WeakReference<Presenter>(presenter);
     }
 
     /** True, if currently active presenter is the same as presenter this
@@ -66,7 +67,7 @@ public abstract class Fn {
      * @return true, if proper presenter is used
      */
     public final boolean isValid() {
-        return presenter != null && FnContext.currentPresenter(false) == presenter;
+        return FnContext.currentPresenter(false) == presenter.get();
     }
     
     /** Helper method to check if the provided instance is valid function.
@@ -144,52 +145,7 @@ public abstract class Fn {
         if (fn == null) {
             return null;
         }
-        return new Fn(fn.presenter()) {
-            @Override
-            public Object invoke(Object thiz, Object... args) throws Exception {
-                loadResource();
-                return fn.invoke(thiz, args);
-            }
-
-            @Override
-            public void invokeLater(Object thiz, Object... args) throws Exception {
-                loadResource();
-                fn.invokeLater(thiz, args);
-            }
-            
-            private void loadResource() throws Exception {
-                Presenter p = presenter();
-                if (p == null) {
-                    p = FnContext.currentPresenter(false);
-                }
-                if (p != null) {
-                    if (LOADED == null) {
-                        LOADED = new HashMap<String, Set<Presenter>>();
-                    }
-                    Set<Presenter> there = LOADED.get(resource);
-                    if (there == null) {
-                        there = new HashSet<Presenter>();
-                        LOADED.put(resource, there);
-                    }
-                    if (there.add(p)) {
-                        final ClassLoader l = caller.getClassLoader();
-                        InputStream is = l.getResourceAsStream(resource);
-                        if (is == null && resource.startsWith("/")) {
-                            is = l.getResourceAsStream(resource.substring(1));
-                        }
-                        if (is == null) {
-                            throw new IOException("Cannot find " + resource + " in " + l);
-                        }
-                        try {
-                            InputStreamReader r = new InputStreamReader(is, "UTF-8");
-                            p.loadScript(r);
-                        } finally {
-                            is.close();
-                        }
-                    }
-                }
-            }
-        };
+        return new Preload(fn.presenter(), fn, resource, caller);
     }
 
     
@@ -251,7 +207,7 @@ public abstract class Fn {
      * @since 0.7
      */
     protected final Presenter presenter() {
-        return presenter;
+        return presenter.get();
     }
     
     /** The representation of a <em>presenter</em> - usually a browser window.
@@ -363,4 +319,73 @@ public abstract class Fn {
          */
         public Fn defineFn(String code, String[] names, boolean[] keepAlive);
     }
+
+    private static class Preload extends Fn {
+        private static Map<String, Set<Reference<Presenter>>> LOADED;
+        private final Fn fn;
+        private final String resource;
+        private final Class<?> caller;
+
+        Preload(Presenter presenter, Fn fn, String resource, Class<?> caller) {
+            super(presenter);
+            this.fn = fn;
+            this.resource = resource;
+            this.caller = caller;
+        }
+
+        @Override
+        public Object invoke(Object thiz, Object... args) throws Exception {
+            loadResource();
+            return fn.invoke(thiz, args);
+        }
+
+        @Override
+        public void invokeLater(Object thiz, Object... args) throws Exception {
+            loadResource();
+            fn.invokeLater(thiz, args);
+        }
+
+        private void loadResource() throws Exception {
+            Reference<Presenter> ref = super.presenter;
+            if (ref == null) {
+                ref = new WeakReference<Fn.Presenter>(FnContext.currentPresenter(false));
+            }
+            Fn.Presenter realPresenter = ref == null ? null : ref.get();
+            if (realPresenter != null) {
+                if (LOADED == null) {
+                    LOADED = new HashMap<String, Set<Reference<Presenter>>>();
+                }
+                Set<Reference<Presenter>> there = LOADED.get(resource);
+                if (there == null) {
+                    there = new HashSet<Reference<Fn.Presenter>>();
+                    LOADED.put(resource, there);
+                }
+                if (addNewRef(there, ref)) {
+                    final ClassLoader l = caller.getClassLoader();
+                    InputStream is = l.getResourceAsStream(resource);
+                    if (is == null && resource.startsWith("/")) {
+                        is = l.getResourceAsStream(resource.substring(1));
+                    }
+                    if (is == null) {
+                        throw new IOException("Cannot find " + resource + " in " + l);
+                    }
+                    try {
+                        InputStreamReader r = new InputStreamReader(is, "UTF-8");
+                        realPresenter.loadScript(r);
+                    } finally {
+                        is.close();
+                    }
+                }
+            }
+        }
+
+        private static synchronized boolean addNewRef(Set<Reference<Presenter>> set, Reference<Presenter> ref) {
+            for (Reference<Presenter> r : set) {
+                if (r.get() == ref.get()) {
+                    return false;
+                }
+            }
+            return set.add(ref);
+        }
+    }
 }
diff --git a/ko4j/pom.xml b/ko4j/pom.xml
index fbe56bb..1d8b9d1 100644
--- a/ko4j/pom.xml
+++ b/ko4j/pom.xml
@@ -130,6 +130,12 @@
         <artifactId>javax.servlet-api</artifactId>
         <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.netbeans.api</groupId>
+      <artifactId>org-netbeans-modules-nbjunit</artifactId>
+      <version>${netbeans.version}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
     <description>Binds net.java.html.json APIs together with knockout.js</description>
 </project>
diff --git a/ko4j/src/main/java/org/netbeans/html/ko4j/CacheObjs.java b/ko4j/src/main/java/org/netbeans/html/ko4j/CacheObjs.java
index 4da77c2..74fe85d 100644
--- a/ko4j/src/main/java/org/netbeans/html/ko4j/CacheObjs.java
+++ b/ko4j/src/main/java/org/netbeans/html/ko4j/CacheObjs.java
@@ -19,24 +19,26 @@
 
 package org.netbeans.html.ko4j;
 
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import org.netbeans.html.boot.spi.Fn;
 
 final class CacheObjs {
     /* both @GuardedBy CacheObjs.class */
     private static CacheObjs[] list = new CacheObjs[16];
     private static int listAt = 0;
-    private final Fn.Presenter ref;
+    private final Reference<Fn.Presenter> ref;
 
     /* both @GuardedBy presenter single threaded access */
     private Object[] jsObjects;
     private int jsIndex;
 
     private CacheObjs(Fn.Presenter p) {
-        this.ref = p;
+        this.ref = new WeakReference<Fn.Presenter>(p);
     }
 
     Fn.Presenter get() {
-        return ref;
+        return ref.get();
     }
 
     static synchronized CacheObjs find(Fn.Presenter key) {
diff --git a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
index 912d12d..e95ff87 100644
--- a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
+++ b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java
@@ -20,6 +20,7 @@ package org.netbeans.html.ko4j;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.lang.ref.Reference;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.Executor;
@@ -150,10 +151,22 @@ final class Knockout  {
         funcs[index].call(data, ev);
     }
 
+    private static Fn.Presenter getPresenter(Object obj) {
+        if (obj instanceof Fn.Presenter) {
+            return (Fn.Presenter) obj;
+        } else {
+            if (obj == null) {
+                return null;
+            } else {
+                return (Fn.Presenter) ((Reference<?>)obj).get();
+            }
+        }
+    }
+
     final void valueHasMutated(final String propertyName, Object oldValue, Object newValue) {
         Object[] all = MapObjs.toArray(objs);
         for (int i = 0; i < all.length; i += 2) {
-            Fn.Presenter p = (Fn.Presenter) all[i];
+            Fn.Presenter p = getPresenter(all[i]);
             final Object o = all[i + 1];
             if (p != Fn.activePresenter()) {
                 if (p instanceof Executor) {
diff --git a/ko4j/src/main/java/org/netbeans/html/ko4j/MapObjs.java b/ko4j/src/main/java/org/netbeans/html/ko4j/MapObjs.java
index 1f3a998..bf9d998 100644
--- a/ko4j/src/main/java/org/netbeans/html/ko4j/MapObjs.java
+++ b/ko4j/src/main/java/org/netbeans/html/ko4j/MapObjs.java
@@ -19,12 +19,14 @@
 
 package org.netbeans.html.ko4j;
 
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import java.util.List;
 import net.java.html.json.Models;
 import org.netbeans.html.boot.spi.Fn;
 
 final class MapObjs {
-    private static Object onlyPresenter;
+    private static Reference<Fn.Presenter> onlyPresenter;
     private static boolean usePresenter;
 
     static {
@@ -32,7 +34,7 @@ final class MapObjs {
     }
 
     static void reset() {
-        onlyPresenter = null;
+        setOnlyPresenter(null);
         usePresenter = true;
     }
 
@@ -43,33 +45,33 @@ final class MapObjs {
     }
 
 
-    static Object put(Object now, Fn.Presenter key, Object js) {
+    synchronized static Object put(Object now, Fn.Presenter key, Object js) {
         if (now instanceof MapObjs) {
             return ((MapObjs)now).put(key, js);
         } else {
             if (usePresenter) {
-                if (onlyPresenter == null) {
-                    onlyPresenter = key;
+                if (getOnlyPresenter() == null) {
+                    setOnlyPresenter(key);
                     return js;
-                } else if (onlyPresenter == key) {
+                } else if (getOnlyPresenter() == key) {
                     return js;
                 } else {
                     usePresenter = false;
                 }
             }
             if (now == null) {
-                return new MapObjs(key, js);
+                return new MapObjs(new WeakReference<Fn.Presenter>(key), js);
             } else {
-                return new MapObjs(onlyPresenter, now, key, js);
+                return new MapObjs(onlyPresenter, now, new WeakReference<Fn.Presenter>(key), js);
             }
         }
     }
 
-    static Object get(Object now, Fn.Presenter key) {
+    synchronized static Object get(Object now, Fn.Presenter key) {
         if (now instanceof MapObjs) {
             return ((MapObjs)now).get(key);
         }
-        return key == onlyPresenter ? now : null;
+        return key == getOnlyPresenter() ? now : null;
     }
 
     static Object[] remove(Object now, Fn.Presenter key) {
@@ -79,28 +81,36 @@ final class MapObjs {
         return new Object[] { now, null };
     }
 
-    static Object[] toArray(Object now) {
+    synchronized static Object[] toArray(Object now) {
         if (now instanceof MapObjs) {
             return ((MapObjs) now).all.toArray();
         }
-        return new Object[] { onlyPresenter, now };
+        return new Object[] { getOnlyPresenter(), now };
     }
 
     private Object put(Fn.Presenter key, Object js) {
         for (int i = 0; i < all.size(); i += 2) {
-            if (all.get(i) == key) {
+            if (isSameKey(i, key)) {
                 all.set(i + 1, js);
                 return this;
             }
         }
-        all.add(key);
+        all.add(new WeakReference<Fn.Presenter>(key));
         all.add(js);
         return this;
     }
 
+    boolean isSameKey(int index, Fn.Presenter key) {
+        Object at = all.get(index);
+        if (at instanceof Reference<?>) {
+            at = ((Reference<?>)at).get();
+        }
+        return at == key;
+    }
+
     private Object get(Fn.Presenter key) {
         for (int i = 0; i < all.size(); i += 2) {
-            if (all.get(i) == key) {
+            if (isSameKey(i, key)) {
                 return all.get(i + 1);
             }
         }
@@ -109,10 +119,18 @@ final class MapObjs {
 
     private Object[] remove(Fn.Presenter key) {
         for (int i = 0; i < all.size(); i += 2) {
-            if (all.get(i) == key) {
+            if (isSameKey(i, key)) {
                 return new Object[] { all.get(i + 1), this };
             }
         }
         return new Object[] { null, this };
     }
+
+    private static Fn.Presenter getOnlyPresenter() {
+        return onlyPresenter.get();
+    }
+
+    private static void setOnlyPresenter(Fn.Presenter aOnlyPresenter) {
+        onlyPresenter = new WeakReference<Fn.Presenter>(aOnlyPresenter);
+    }
 }
diff --git a/ko4j/src/test/java/org/netbeans/html/ko4j/DoubleViewTest.java b/ko4j/src/test/java/org/netbeans/html/ko4j/DoubleViewTest.java
index d2ed578..9f49d84 100644
--- a/ko4j/src/test/java/org/netbeans/html/ko4j/DoubleViewTest.java
+++ b/ko4j/src/test/java/org/netbeans/html/ko4j/DoubleViewTest.java
@@ -19,11 +19,15 @@
 package org.netbeans.html.ko4j;
 
 import java.awt.FlowLayout;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
 import java.net.URL;
 import java.util.concurrent.CountDownLatch;
 import javafx.application.Platform;
 import javafx.embed.swing.JFXPanel;
+import javafx.scene.Parent;
 import javafx.scene.Scene;
+import javafx.scene.control.Label;
 import javafx.scene.layout.BorderPane;
 import javafx.scene.web.WebView;
 import javax.swing.JFrame;
@@ -31,6 +35,7 @@ import net.java.html.boot.fx.FXBrowsers;
 import net.java.html.json.Function;
 import net.java.html.json.Model;
 import net.java.html.json.Property;
+import org.netbeans.junit.NbTestCase;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
 import org.testng.annotations.AfterMethod;
@@ -57,6 +62,7 @@ public class DoubleViewTest {
     private WebView view2;
 
     private static final StringBuffer LOG = new StringBuffer();
+    private JFrame frame;
 
     @BeforeMethod
     public void initializeViews() throws Exception {
@@ -104,12 +110,12 @@ public class DoubleViewTest {
         view1 = displayWebView(panel);
         view2 = displayWebView(p2);
 
-        JFrame f = new JFrame();
-        f.getContentPane().setLayout(new FlowLayout());
-        f.getContentPane().add(panel);
-        f.getContentPane().add(p2);
-        f.pack();
-        f.setVisible(true);
+        frame = new JFrame();
+        frame.getContentPane().setLayout(new FlowLayout());
+        frame.getContentPane().add(panel);
+        frame.getContentPane().add(p2);
+        frame.pack();
+        frame.setVisible(true);
     }
 
     private WebView displayWebView(JFXPanel panel) {
@@ -152,6 +158,29 @@ public class DoubleViewTest {
 
     @AfterMethod
     public void waitABit() throws Exception {
+        final CountDownLatch cdl = new CountDownLatch(1);
+        Platform.runLater(() -> {
+            Parent p1 = view1.getParent();
+            ((BorderPane)p1).setCenter(new Label("Searching for GC root"));
+            Parent p2 = view2.getParent();
+            ((BorderPane)p2).setCenter(new Label("Searching for GC root"));
+            cdl.countDown();
+        });
+        cdl.await();
+
+        Reference<?> r1 = new WeakReference<>(view1);
+        Reference<?> r2 = new WeakReference<>(view2);
+
+        view1 = null;
+        view2 = null;
+
+        NbTestCase.assertGC("Clearing reference 1", r1);
+        NbTestCase.assertGC("Clearing reference 2", r2);
+
+        Platform.runLater(() -> {
+            Platform.setImplicitExit(false);
+            frame.dispose();
+        });
     }
 
     private void assertMessages(String msg, WebView v, String expected) {


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists