Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run diff on background thread when calling Anvil.render() #141

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 133 additions & 86 deletions anvil/src/main/java/trikita/anvil/Anvil.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,10 @@

import java.lang.reflect.InvocationTargetException;
import java.lang.ref.WeakReference;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
import java.util.WeakHashMap;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
* Anvil class is a namespace for top-level static methods and interfaces. Most
Expand All @@ -32,10 +25,11 @@
*/
public final class Anvil {

private final static Map<View, Mount> mounts = new WeakHashMap<>();
private final static Map<View, Mount> mounts = Collections.synchronizedMap(new WeakHashMap<View, Mount>());
private static Mount currentMount = null;

private static Handler anvilUIHandler = null;
private static Handler anvilUIHandler = new Handler(Looper.getMainLooper());
private static ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

/** Renderable can be mounted and rendered using Anvil library. */
public interface Renderable {
Expand Down Expand Up @@ -70,7 +64,7 @@ public View fromClass(Context c, Class<? extends View> viewClass) {
public View fromXml(ViewGroup parent, int xmlId) {
return LayoutInflater.from(parent.getContext()).inflate(xmlId, parent, false);
}
};
}

public static void registerViewFactory(ViewFactory viewFactory) {
if (!viewFactories.contains(viewFactory)) {
Expand All @@ -80,12 +74,6 @@ public static void registerViewFactory(ViewFactory viewFactory) {

private Anvil() {}

private final static Runnable anvilRenderRunnable = new Runnable() {
public void run() {
Anvil.render();
}
};

public interface AttributeSetter<T> {
boolean set(View v, String name, T value, T prevValue);
}
Expand Down Expand Up @@ -124,23 +112,22 @@ public static Object get(View v, String key) {
* been changed since last rendering cycle will be actually updated in the
* views. This method can be called from any thread, so it's safe to use
* {@code Anvil.render()} in background services. */
public static void render() {
// If Anvil.render() is called on a non-UI thread, use UI Handler
if (Looper.myLooper() != Looper.getMainLooper()) {
synchronized (Anvil.class) {
if (anvilUIHandler == null) {
anvilUIHandler = new Handler(Looper.getMainLooper());
public static Future<?> render() {
return singleThreadExecutor.submit(createRenderTask());
}

private static Runnable createRenderTask() {
return new Runnable() {
@Override
public void run() {
synchronized (mounts) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this synchronized is excessive since mounts are already use synchronizedMap

Set<Mount> set = new HashSet<>(mounts.values());
for (Mount m : set) {
render(m);
}
}
}
anvilUIHandler.removeCallbacksAndMessages(null);
anvilUIHandler.post(anvilRenderRunnable);
return;
}
Set<Mount> set = new HashSet<>();
set.addAll(mounts.values());
for (Mount m : set) {
render(m);
}
};
}

/**
Expand All @@ -151,10 +138,12 @@ public static void render() {
* @param r a Renderable to mount into a View
*/
public static <T extends View> T mount(T v, Renderable r) {
Mount m = new Mount(v, r);
mounts.put(v, m);
render(v);
return v;
synchronized (mounts) {
Mount m = new Mount(v, r);
mounts.put(v, m);
render(v);
return v;
}
}

/**
Expand All @@ -167,18 +156,20 @@ public static void unmount(View v) {
}

public static void unmount(View v, boolean removeChildren) {
Mount m = mounts.get(v);
if (m != null) {
mounts.remove(v);
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;

int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
unmount(viewGroup.getChildAt(i));
}
if (removeChildren) {
viewGroup.removeViews(0, childCount);
synchronized (mounts) {
Mount m = mounts.get(v);
if (m != null) {
mounts.remove(v);
if (v instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) v;

int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
unmount(viewGroup.getChildAt(i));
}
if (removeChildren) {
viewGroup.removeViews(0, childCount);
}
}
}
}
Expand Down Expand Up @@ -215,26 +206,23 @@ public static void render(View v) {
}

static void render(Mount m) {
if (m.lock) {
return;
}
m.lock = true;
Mount prev = currentMount;
currentMount = m;
m.iterator.start();
if (m.renderable != null) {
m.renderable.view();
synchronized (m.lock) {
Mount prev = currentMount;
currentMount = m;
m.iterator.start();
if (m.renderable != null) {
m.renderable.view();
}
m.iterator.end();
currentMount = prev;
}
m.iterator.end();
currentMount = prev;
m.lock = false;
}

/** Mount describes a mount point. Mount point is a Renderable function
* attached to some ViewGroup. Mount point keeps track of the virtual layout
* declared by Renderable */
static class Mount {
private boolean lock = false;
private final Object lock = new Object();

private final WeakReference<View> rootView;
private final Renderable renderable;
Expand Down Expand Up @@ -277,23 +265,23 @@ void start(Class<? extends View> c, int layoutId, Object key) {
}
Context context = rootView.get().getContext();
if (c != null && (v == null || !v.getClass().equals(c))) {
vg.removeView(v);
removeViewFromViewGroup(v, vg);
for (ViewFactory vf : viewFactories) {
v = vf.fromClass(context, c);
if (v != null) {
set(v, "_anvil", 1);
vg.addView(v, i);
addViewToViewGroupAt(v, vg, i);
break;
}
}
} else if (c == null && (v == null || !Integer.valueOf(layoutId).equals(get(v, "_layoutId")))) {
vg.removeView(v);
removeViewFromViewGroup(v, vg);
for (ViewFactory vf : viewFactories) {
v = vf.fromXml(vg, layoutId);
if (v != null) {
set(v, "_anvil", 1);
set(v, "_layoutId", layoutId);
vg.addView(v, i);
addViewToViewGroupAt(v, vg, i);
break;
}
}
Expand All @@ -304,15 +292,54 @@ void start(Class<? extends View> c, int layoutId, Object key) {
indices.push(0);
}

private void removeViewFromViewGroup(View v, ViewGroup vg) {
if (isRunningOnBackgroundThread())
postViewRemovalOnUIThread(v, vg);
else
vg.removeView(v);
}

private boolean isRunningOnBackgroundThread() {
return Looper.myLooper() != Looper.getMainLooper();
}

private void postViewRemovalOnUIThread(final View v, final ViewGroup vg) {
anvilUIHandler.post(new Runnable() {
@Override
public void run() {
vg.removeView(v);
}
});
}

private void addViewToViewGroupAt(View v, ViewGroup vg, int i) {
if (isRunningOnBackgroundThread()) {
postViewAdditionOnUIThread(v, vg, i);
} else {
vg.addView(v, i);
}
}

private void postViewAdditionOnUIThread(final View v, final ViewGroup vg, final int i) {
anvilUIHandler.post(new Runnable() {
@Override
public void run() {
vg.addView(v, i);
}
});
}

void end() {
int index = indices.peek();
View v = views.peek();
if (v != null && v instanceof ViewGroup &&
get(v, "_layoutId") == null &&
(mounts.get(v) == null || mounts.get(v) == Mount.this)) {
ViewGroup vg = (ViewGroup) v;
if (index < vg.getChildCount()) {
removeNonAnvilViews(vg, index, vg.getChildCount() - index);
synchronized (mounts) {
if (v instanceof ViewGroup &&
get(v, "_layoutId") == null &&
(mounts.get(v) == null || mounts.get(v) == Mount.this)) {
ViewGroup vg = (ViewGroup) v;
if (index < vg.getChildCount()) {
removeNonAnvilViews(vg, index, vg.getChildCount() - index);
}
}
}
indices.pop();
Expand All @@ -321,6 +348,16 @@ void end() {
}
}

private void removeNonAnvilViews(ViewGroup vg, int start, int count) {
final int end = start + count - 1;

for (int i = end; i >= start; i--) {
View v = vg.getChildAt(i);
if (get(v, "_anvil") != null)
removeViewFromViewGroup(v, vg);
}
}

<T> void attr(String name, T value) {
View currentView = views.peek();
if (currentView == null) {
Expand All @@ -329,22 +366,32 @@ <T> void attr(String name, T value) {
@SuppressWarnings("unchecked")
T currentValue = (T) get(currentView, name);
if (currentValue == null || !currentValue.equals(value)) {
for (AttributeSetter setter : attributeSetters) {
if (setter.set(currentView, name, value, currentValue)) {
set(currentView, name, value);
return;
}
}
updateViewAttribute(name, value, currentView, currentValue);
}
}

private void removeNonAnvilViews(ViewGroup vg, int start, int count) {
final int end = start + count - 1;
private <T> void updateViewAttribute(String name, T value, View currentView, T currentValue) {
if (isRunningOnBackgroundThread())
postViewUpdateOnUIThread(name, value, currentView, currentValue);
else
setAttribute(name, value, currentView, currentValue);
}

for (int i = end; i >= start; i--) {
View v = vg.getChildAt(i);
if (get(v, "_anvil") != null) {
vg.removeView(v);
private <T> void postViewUpdateOnUIThread(final String name, final T value, final View currentView,
final T currentValue) {
anvilUIHandler.post(new Runnable() {
@Override
public void run() {
setAttribute(name, value, currentView, currentValue);
}
});
}

private <T> void setAttribute(String name, T value, View currentView, T currentValue) {
for (AttributeSetter setter : attributeSetters) {
if (setter.set(currentView, name, value, currentValue)) {
set(currentView, name, value);
return;
}
}
}
Expand Down
26 changes: 22 additions & 4 deletions anvil/src/test/java/trikita/anvil/BenchmarkTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import org.junit.Test;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import static trikita.anvil.BaseDSL.v;
import static trikita.anvil.DSL.id;
import static trikita.anvil.DSL.tag;
Expand All @@ -12,7 +15,7 @@ public class BenchmarkTest extends Utils {
private int mode;

@Test
public void testRenderBenchmark() {
public void testRenderBenchmark() throws ExecutionException {
long start;

Anvil.Renderable r = new Anvil.Renderable() {
Expand All @@ -34,7 +37,7 @@ public void view() {
Anvil.mount(container, r);
start = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
Anvil.render();
renderView(i);
}
System.out.println("render/no-changes: " + (System.currentTimeMillis() - start)*1000/N + "us");
Anvil.unmount(container, true);
Expand All @@ -43,7 +46,7 @@ public void view() {
Anvil.mount(container, r);
start = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
Anvil.render();
renderView(i);
}
System.out.println("render/small-changes: " + (System.currentTimeMillis() - start)*1000/N +"us");
Anvil.unmount(container, true);
Expand All @@ -52,7 +55,7 @@ public void view() {
Anvil.mount(container, r);
start = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
Anvil.render();
renderView(i);
}
System.out.println("render/big-changes: " + (System.currentTimeMillis() - start)*1000/N+"us");
}
Expand Down Expand Up @@ -84,4 +87,19 @@ public void view() {
}
});
}

private void renderView(int i) throws ExecutionException {
Future<?> future = Anvil.render();
if (i == N-1)
waitTaskToFinish(future);
}

private void waitTaskToFinish(Future<?> future) throws ExecutionException {
try {
future.get();
} catch (ExecutionException executionException) {
throw executionException;
} catch (Exception e) {
}
}
}
Loading