Throttle Calls to MockForm.refresh()

Limit the number of calls to MockForm.refresh(). This lets a project
with a lot of designer objects load when otherwise the exponential time
and space consumption of repeated and nested calls to refresh() would
cause it to fail to load.

This implementation throttles the calls “locally.” The idea is to limit
the number of calls to 1 every 2 seconds. An alternative implementation
would be to set a flag while a project is loading (which is when this is
a problem) and inhibit all calls to refresh while this flag is set. Then
when the project is no longer loading, clear the flag and do one final
call to refresh. However this would be tricky because loading is
asynchronous and knowing when it is completely finished is
complicated. Such an implementation would be spread over several
modules, and add complexity to the codebase. This change is simpler
because it is limited to one module.

We also add a logging call Ode.CLog to Ode that logs to the developer
console instead of the OdeLog module which logs to the special debug
tab. There are situations where logging to the console is desirable,
like when the problem you are attempting to debug leaves App Inventor
locked up so you cannot see the debug tab!

Change-Id: I5fb9c832704e87c6e73903dc3a75ceb79df5f7f3
parent 08ad4ee1
......@@ -2284,4 +2284,24 @@ public class Ode implements EntryPoint {
}
}-*/;
private static native boolean finish(String userId) /*-{
var delete_cookie = function(name) {
document.cookie = name + '=;Path=/;expires=Thu, 01 Jan 1970 00:00:01 GMT;';
};
var retval = {
"type": "closeApp",
"uuid" : userId }
if (top.opener) {
delete_cookie("AppInventor"); // This ends our authentication
top.opener.postMessage(retval, "*");
return true;
} else {
return false;
}
}-*/;
public static native void CLog(String message) /*-{
console.log(message);
}-*/;
}
......@@ -926,12 +926,27 @@ public abstract class MockComponent extends Composite implements PropertyChangeL
* Refreshes the form.
*
* <p>This method should be called whenever a property that affects the size
* of the component is changed.
* of the component is changed. It calls refreshForm(false) which permits
* throttling.
*/
final void refreshForm() {
refreshForm(false);
}
/*
* Refresh the current form. If force is true, we bypass the
* throttling code. This is needed by MockImageBase because it
* *must* refresh the form before resizing loaded images.
*
*/
final void refreshForm(boolean force) {
if (isAttached()) {
if (getContainer() != null || isForm()) {
getForm().refresh();
if (force) {
getForm().doRefresh();
} else {
getForm().refresh();
}
}
}
}
......
......@@ -6,6 +6,7 @@
package com.google.appinventor.client.editor.simple.components;
import com.google.appinventor.client.Ode;
import static com.google.appinventor.client.Ode.MESSAGES;
import java.util.HashMap;
......@@ -20,10 +21,14 @@ import com.google.appinventor.client.output.OdeLog;
import com.google.appinventor.client.properties.BadPropertyEditorException;
import com.google.appinventor.client.widgets.properties.EditableProperties;
import com.google.appinventor.shared.settings.SettingsConstants;
import com.google.gwt.core.client.Duration;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.DockPanel;
......@@ -549,8 +554,71 @@ public final class MockForm extends MockContainer {
/**
* Forces a re-layout of the child components of the container.
*
* Each components onPropertyChange listener calls us. This is
* reasonable during interactive editing because we have to make
* sure the screen reflects what the user is doing. However during
* project load we will be called many times, when really we should
* only be called after the project's UI is really finished loading.
*
* We could add a bunch of complicated code to inhibit refreshes
* until we know the project's UI is loaded and stable. However that
* is a change that will be spread over several modules, making it
* hard to understand what is going on.
*
* Instead, I am opting to keep this change self contained within
* this module. The idea is to see how quickly we are being
* called. If we receive a call which is close in time (within
* seconds) of a previous call, we set a timer to fire in the
* reasonable future (say 2 seconds). While this timer is counting
* down, we ignore any other calls to refresh. Whatever refreshing
* they would do will be handled by the call done when the timer
* fires. This approach does not reduce the number of calls to
* refresh during project loading to 1. But it significantly reduces
* the number of calls and gets us out of the exponential explosion
* in time and memory that we see with projects with hundreds of
* design elements (yes, people do that, and I have seen at least
* one project that was this big and reasonable!). -Jeff Schiller
* (jis@mit.edu).
*
*/
private Duration lastRefresh = new Duration();
private boolean refreshPending = false;
public final void refresh() {
Ode.CLog("MockForm: refresh() called.");
/* We refresh less then two seconds ago! */
if (lastRefresh.elapsedMillis() < 2000) {
if (!refreshPending) {
Ode.CLog("MockForm: refresh() called < 2 seconds ago, setting up timer.");
refreshPending = true;
Timer t = new Timer() {
@Override
public void run() {
refreshPending = false;
doRefresh();
}
};
t.schedule(2000); // Two Seconds
} else {
Ode.CLog("MockForm: refresh() while timer running, IGNORING!");
}
} else {
lastRefresh = new Duration();
doRefresh();
}
}
/*
* Do the actual refresh.
*
* This method is public because it is called directly from MockComponent for refreshes
* which bypass throttling.
*
*/
public final void doRefresh() {
Ode.CLog("MockForm: doRefresh() called");
Map<MockComponent, LayoutInfo> layoutInfoMap = new HashMap<MockComponent, LayoutInfo>();
collectLayoutInfos(layoutInfoMap, this);
......@@ -564,6 +632,7 @@ public final class MockForm extends MockContainer {
layoutInfo.cleanUp();
}
layoutInfoMap.clear();
Ode.CLog("MockForm: doRefresh() done.");
}
/*
......
......@@ -45,13 +45,13 @@ abstract class MockImageBase extends MockVisibleComponent {
if (picturePropValue != null && !picturePropValue.isEmpty()) {
OdeLog.elog("Error occurred while loading image " + picturePropValue);
}
refreshForm();
refreshForm(true);
}
});
image.addLoadHandler(new LoadHandler() {
@Override
public void onLoad(LoadEvent event) {
refreshForm();
refreshForm(true);
resizeImage(); // resize after the new image occupies the form
}
});
......
......@@ -53,6 +53,7 @@ public final class Project {
public void loadProjectNodes() {
if (projectRoot == null && !loadingInProgress) {
loadingInProgress = true;
Ode.CLog("Project.loadProjectNodes(): loadingInProgress = true");
if (settings == null) {
settings = new ProjectSettings(Project.this);
......@@ -68,6 +69,7 @@ public final class Project {
public void onSuccess(ProjectRootNode result) {
projectRoot = result;
Ode.CLog("Project.loadProjectNodes(): loadingInProgress = false");
loadingInProgress = false;
fireProjectLoaded();
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment