# HG changeset patch # User Jaroslav Tulach # Date 1418379760 -3600 # Node ID 2ee22312e414890b624ca1145bbfd3d52e941343 # Parent 01c8dda1f8887866b73f05a96a6e87323b927d1f Giving API users better control over GC aspects of their objects diff -r 01c8dda1f888 -r 2ee22312e414 boot/src/main/java/net/java/html/js/JavaScriptBody.java --- a/boot/src/main/java/net/java/html/js/JavaScriptBody.java Tue Dec 09 21:32:09 2014 +0100 +++ b/boot/src/main/java/net/java/html/js/JavaScriptBody.java Fri Dec 12 11:22:40 2014 +0100 @@ -120,4 +120,33 @@ * of the JavaScript snippet */ public boolean wait4js() default true; + + /** Controls garbage collection behavior of method parameters. + * In general JavaScript garbage + * collection system makes it close to impossible to find out whether + * an object is supposed to be still used or not. Some systems have + * an external hooks to find that out (like JavaFX WebView), + * in some systems this information is not important (like the + * Bck2Brwsr VM running + * all in JavaScript), but other execution systems just can't find that + * out. To prevent memory leaks on such systems and help them manage + * memory more effectively, those who define JavaScript interfacing + * methods may indicate whether the non-primitive parameters passed + * in should be hold only for the time of method invocation or + * for the whole application lifetime. + *

+ * The default value is true as that is compatible with + * previous behavior and also prevents unwanted surprises when something + * garbage collects pre-maturaly. Framework developers are however + * encouraged to use keepAlive=false as much as possible. + * + * @return whether Java objects passed as parameters of the method + * should be made guaranteed to be available JavaScript + * even after the method invocation is over (e.g. prevent them to be + * garbage collected in Java until it is known they are not needed + * from JavaScript at all). + * + * @since 1.1 + */ + public boolean keepAlive() default true; } diff -r 01c8dda1f888 -r 2ee22312e414 boot/src/main/java/org/netbeans/html/boot/impl/FnUtils.java --- a/boot/src/main/java/org/netbeans/html/boot/impl/FnUtils.java Tue Dec 09 21:32:09 2014 +0100 +++ b/boot/src/main/java/org/netbeans/html/boot/impl/FnUtils.java Fri Dec 12 11:22:40 2014 +0100 @@ -296,6 +296,7 @@ // init Fn super.visitInsn(Opcodes.POP); super.visitLdcInsn(Type.getObjectType(FindInClass.this.name)); + super.visitInsn(fia.keepAlive ? Opcodes.ICONST_1 : Opcodes.ICONST_0); super.visitLdcInsn(body); super.visitIntInsn(Opcodes.SIPUSH, args.size()); super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); @@ -311,7 +312,7 @@ } super.visitMethodInsn(Opcodes.INVOKESTATIC, "org/netbeans/html/boot/spi/Fn", "define", - "(Ljava/lang/Class;Ljava/lang/String;[Ljava/lang/String;)Lorg/netbeans/html/boot/spi/Fn;" + "(Ljava/lang/Class;ZLjava/lang/String;[Ljava/lang/String;)Lorg/netbeans/html/boot/spi/Fn;" ); Label noPresenter = new Label(); super.visitInsn(Opcodes.DUP); @@ -526,6 +527,7 @@ String body; boolean javacall = false; boolean wait4js = true; + boolean keepAlive = false; public FindInAnno() { super(Opcodes.ASM4); @@ -545,7 +547,11 @@ wait4js = (Boolean) value; return; } - assert name.equals("body"); + if (name.equals("keepAlive")) { // NOI18N + keepAlive = (Boolean) value; + return; + } + assert name.equals("body"); // NOI18N body = (String) value; } diff -r 01c8dda1f888 -r 2ee22312e414 boot/src/main/java/org/netbeans/html/boot/spi/Fn.java --- a/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java Tue Dec 09 21:32:09 2014 +0100 +++ b/boot/src/main/java/org/netbeans/html/boot/spi/Fn.java Fri Dec 12 11:22:40 2014 +0100 @@ -115,8 +115,39 @@ * @since 0.7 */ public static Fn define(Class caller, String code, String... names) { + return define(caller, false, code, names); + } + + /** Helper method to find current presenter and ask it to define new + * function. + * + * @param caller the class who wishes to define the function + * @param keepParametersAlive whether Java parameters should survive in JavaScript + * after the method invocation is over + * @param code the body of the function (can reference this and names variables) + * @param names names of individual parameters + * @return the function object that can be {@link Fn#invoke(java.lang.Object, java.lang.Object...) invoked} + * - can return null if there is {@link #activePresenter() no presenter} + * @since 1.1 + */ + public static Fn define(Class caller, boolean keepParametersAlive, String code, String... names) { final Presenter p = FnContext.currentPresenter(false); - return p == null ? null : p.defineFn(code, names); + if (p == null) { + return null; + } + if (p instanceof KeepAlive) { + boolean[] arr; + if (!keepParametersAlive) { + arr = new boolean[names.length]; + for (int i = 0; i < arr.length; i++) { + arr[i] = false; + } + } else { + arr = null; + } + return ((KeepAlive)p).defineFn(code, names, arr); + } + return p.defineFn(code, names); } private static final Map> LOADED = new HashMap>(); @@ -244,7 +275,7 @@ protected final Presenter presenter() { return presenter; } - + /** The representation of a presenter - usually a browser window. * Should be provided by a library included in the application and registered * in META-INF/services, for example with @@ -329,4 +360,29 @@ */ public Object toJava(Object js); } + + /** Additional interface to {@link Presenter} to control more precisely + * garbage collection behavior of individual parameters. See + * {@link JavaScriptBody#keepAlive()} attribute for description of the + * actual behavior of the interface. + * + * @since 1.1 + */ + public interface KeepAlive { + /** Creates new function with given parameter names and provided body. + * + * @param code the body of the function. Can refer to variables named + * as names + * @param names names of parameters of the function - these will be + * available when the code body executes + * @param keepAlive array of booleans describing for each parameter + * whether it should be kept alive or not. Length of the array + * must be the same as length of names array. The + * array may be null to signal that all parameters + * should be kept alive. + * + * @return function that can be later invoked + */ + public Fn defineFn(String code, String[] names, boolean[] keepAlive); + } } diff -r 01c8dda1f888 -r 2ee22312e414 boot/src/test/java/org/netbeans/html/boot/impl/JsMethods.java --- a/boot/src/test/java/org/netbeans/html/boot/impl/JsMethods.java Tue Dec 09 21:32:09 2014 +0100 +++ b/boot/src/test/java/org/netbeans/html/boot/impl/JsMethods.java Fri Dec 12 11:22:40 2014 +0100 @@ -134,4 +134,7 @@ public static int plusOrMul(int x, int y) { return x * y; } + + @JavaScriptBody(args = { "x" }, keepAlive = false, body = "throw 'Do not call me!'") + public static native boolean checkAllowGC(Object x); } diff -r 01c8dda1f888 -r 2ee22312e414 boot/src/test/java/org/netbeans/html/boot/impl/KeepAliveTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/boot/src/test/java/org/netbeans/html/boot/impl/KeepAliveTest.java Fri Dec 12 11:22:40 2014 +0100 @@ -0,0 +1,114 @@ +/** + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2013-2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Oracle. Portions Copyright 2013-2014 Oracle. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package org.netbeans.html.boot.impl; + +import java.io.Closeable; +import java.io.Reader; +import java.net.URL; +import java.util.Collection; +import org.netbeans.html.boot.spi.Fn; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +public class KeepAliveTest implements Fn.Presenter, Fn.KeepAlive, FindResources { + private Class jsMethods; + @Test public void keepAliveIsSetToFalse() throws Exception { + Closeable c = Fn.activate(this); + boolean ret = (Boolean)jsMethods.getMethod("checkAllowGC", Object.class).invoke(null, this); + c.close(); + assertFalse(ret, "keepAlive returns false when the presenter is invoked"); + } + + @Test public void keepAliveIsPropagated() throws Exception { + Closeable c = Fn.activate(this); + boolean ret = (Boolean)jsMethods.getMethod("truth").invoke(null); + c.close(); + assertTrue(ret, "keepAlive returns true when the presenter is invoked"); + } + + @BeforeMethod + public void initClass() throws ClassNotFoundException { + ClassLoader loader = FnUtils.newLoader(this, this, KeepAliveTest.class.getClassLoader().getParent()); + jsMethods = loader.loadClass(JsMethods.class.getName()); + } + + @Override + public Fn defineFn(String code, String[] names, final boolean[] keepAlive) { + return new Fn(this) { + @Override + public Object invoke(Object thiz, Object... args) throws Exception { + boolean res = true; + if (keepAlive != null) { + for (int i = 0; i < keepAlive.length; i++) { + res &= keepAlive[i]; + } + } + return res; + } + }; + } + + @Override + public Fn defineFn(String code, String... names) { + throw new UnsupportedOperationException("Never called!"); + } + + @Override + public void displayPage(URL page, Runnable onPageLoad) { + throw new UnsupportedOperationException(); + } + + @Override + public void loadScript(Reader code) throws Exception { + } + + @Override + public void findResources(String path, Collection results, boolean oneIsEnough) { + URL u = ClassLoader.getSystemClassLoader().getResource(path); + if (u != null) { + results.add(u); + } + } +} diff -r 01c8dda1f888 -r 2ee22312e414 json-tck/src/main/java/net/java/html/js/tests/Bodies.java --- a/json-tck/src/main/java/net/java/html/js/tests/Bodies.java Tue Dec 09 21:32:09 2014 +0100 +++ b/json-tck/src/main/java/net/java/html/js/tests/Bodies.java Fri Dec 12 11:22:40 2014 +0100 @@ -61,7 +61,7 @@ @JavaScriptBody(args = {"r"}, javacall = true, body = "r.@java.lang.Runnable::run()();") static native void callback(Runnable r); - @JavaScriptBody(args = {"r"}, wait4js = false, javacall = true, body = "r.@java.lang.Runnable::run()();") + @JavaScriptBody(args = {"r"}, wait4js = false, keepAlive = false, javacall = true, body = "r.@java.lang.Runnable::run()();") static native void asyncCallback(Runnable r); @JavaScriptBody(args = {"c"}, javacall = true, body = "return c.@java.util.concurrent.Callable::call()();") @@ -88,10 +88,10 @@ @JavaScriptBody(args = "o", body = "return o.x;") public static native Object readX(Object o); - @JavaScriptBody(args = { "o", "x" }, body = "o.x = x;") + @JavaScriptBody(args = { "o", "x" }, keepAlive = false, body = "o.x = x;") public static native Object setX(Object o, Object x); - @JavaScriptBody(args = { "c" }, javacall = true, body = + @JavaScriptBody(args = { "c" }, keepAlive = false, javacall = true, body = "return c.@net.java.html.js.tests.Sum::sum(II)(40, 2);" ) public static native int sumIndirect(Sum c); diff -r 01c8dda1f888 -r 2ee22312e414 json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java --- a/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java Tue Dec 09 21:32:09 2014 +0100 +++ b/json-tck/src/main/java/net/java/html/js/tests/GCBodyTest.java Fri Dec 12 11:22:40 2014 +0100 @@ -52,8 +52,9 @@ */ public class GCBodyTest { Reference ref; + int[] arr; - @KOTest public void callbackWithParameters() throws InterruptedException { + @KOTest public void callbackInterfaceCanDisappear() throws InterruptedException { if (ref != null) { assertGC(ref, "Can disappear!"); return; @@ -66,24 +67,60 @@ assertGC(ref, "Can disappear!"); } + private Object assignInst() { + Object obj = Bodies.instance(0); + Object s = new EmptyInstance(); + Bodies.setX(obj, s); + assert s == Bodies.readX(obj); + ref = new WeakReference(s); + return obj; +} + @KOTest public void holdObjectAndReleaseObject() throws InterruptedException { if (ref != null) { assertGC(ref, "Can disappear!"); return; } - Sum s = new Sum(); - Object obj = Bodies.instance(0); - Bodies.setX(obj, s); - ref = new WeakReference(s); - s = null; - assertNotGC(ref, "Cannot disappear!"); + Object obj = assignInst(); + assert ref != null; Bodies.setX(obj, null); obj = null; assertGC(ref, "Can disappear!"); } + + private static Reference sendRunnable(final int[] arr) { + Runnable r = new Runnable() { + @Override + public void run() { + arr[0]++; + } + }; + Bodies.asyncCallback(r); + return new WeakReference(r); + } + + private static class EmptyInstance { + } + + @KOTest public void parametersNeedToRemainInAsyncMode() throws InterruptedException { + if (ref != null) { + if (arr[0] != 1) { + throw new InterruptedException(); + } + assertGC(ref, "Now the runnable can disappear"); + return; + } + arr = new int[] { 0 }; + ref = sendRunnable(arr); + if (arr[0] == 1) { + return; + } + assertNotGC(ref, false, "The runnable should not be GCed"); + throw new InterruptedException(); + } private static void assertGC(Reference ref, String msg) throws InterruptedException { for (int i = 0; i < 100; i++) { @@ -109,12 +146,14 @@ return ref.get() == null; } - private static void assertNotGC(Reference ref, String msg) throws InterruptedException { + private static void assertNotGC(Reference ref, boolean clearJS, String msg) throws InterruptedException { for (int i = 0; i < 10; i++) { if (ref.get() == null) { throw new IllegalStateException(msg); } - int size = Bodies.gc(Math.pow(2.0, i)); + if (clearJS) { + Bodies.gc(Math.pow(2.0, i)); + } try { System.gc(); System.runFinalization(); diff -r 01c8dda1f888 -r 2ee22312e414 ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java --- a/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java Tue Dec 09 21:32:09 2014 +0100 +++ b/ko4j/src/main/java/org/netbeans/html/ko4j/Knockout.java Fri Dec 12 11:22:40 2014 +0100 @@ -147,6 +147,7 @@ @JavaScriptBody( javacall = true, + keepAlive = false, wait4js = false, args = { "thiz", "ret", "propNames", "propReadOnly", "propValues", "funcNames" }, body =