| // Copyright 2016 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package io.v.pingpongapp; |
| |
| import android.graphics.Color; |
| import android.os.Bundle; |
| import android.support.v7.app.AppCompatActivity; |
| import android.util.Log; |
| import android.view.View; |
| import android.widget.Button; |
| import android.widget.TextView; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.Futures; |
| |
| import java.util.Arrays; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.Future; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| import io.v.android.security.BlessingsManager; |
| import io.v.android.v23.V; |
| import io.v.impl.google.services.syncbase.SyncbaseServer; |
| import io.v.v23.InputChannels; |
| import io.v.v23.OptionDefs; |
| import io.v.v23.Options; |
| import io.v.v23.VIterable; |
| import io.v.v23.context.VContext; |
| import io.v.v23.rpc.Server; |
| import io.v.v23.security.BlessingPattern; |
| import io.v.v23.security.Blessings; |
| import io.v.v23.security.access.AccessList; |
| import io.v.v23.security.access.Constants; |
| import io.v.v23.security.access.Permissions; |
| import io.v.v23.services.syncbase.Id; |
| import io.v.v23.services.syncbase.SyncgroupMemberInfo; |
| import io.v.v23.services.syncbase.SyncgroupSpec; |
| import io.v.v23.syncbase.Syncbase; |
| import io.v.v23.syncbase.SyncbaseService; |
| import io.v.v23.syncbase.Database; |
| import io.v.v23.syncbase.Syncgroup; |
| import io.v.v23.syncbase.Collection; |
| import io.v.v23.syncbase.WatchChange; |
| import io.v.v23.syncbase.util.Util; |
| import io.v.v23.verror.VException; |
| |
| import static io.v.v23.VFutures.sync; |
| |
| public class MainActivity extends AppCompatActivity { |
| // Syncgroup-related names for where the mounttable is and the syncgroup name's suffix. |
| public static final String MT_PREFIX = "/ns.dev.v.io:8101/tmp/benchmark/pingpong/"; |
| public static final String SG_SUFFIX = "_sg"; |
| |
| // Syncbase-related names for the db/cx hierarchy and where to write data. |
| public static final String DB_NAME = "db"; |
| public static final String CX_NAME = "cx"; |
| |
| // Allow each asynchronous operation to take at most this amount of time. |
| private static final long TEST_TIMEOUT = 5; |
| private static final TimeUnit TEST_TIMEUNIT = TimeUnit.SECONDS; |
| |
| // Fixed parameters for the app. |
| private static final String TAG = "PingPong"; // Debug tag for Log.d |
| private static final String TEST_ID = "AndroidPingPongTest"; |
| private static final int NUM_TIMES = 100; |
| // Log success/failure once blessings are received. |
| private static final FutureCallback<Blessings> ON_BLESSINGS = new FutureCallback<Blessings>() { |
| @Override |
| public void onSuccess(Blessings b) { |
| Log.d(TAG, "Got blessings!"); |
| } |
| |
| @Override |
| public void onFailure(Throwable t) { |
| Log.d(TAG, "Failure to get blessings, nothing will work.", t); |
| } |
| }; |
| |
| // The V context tracked by the app. |
| VContext mBaseContext; |
| // Values that can be changed by the app. |
| private int mPeerID = 0; |
| private Thread mTestThread; |
| private boolean mUsingProxy = false; |
| |
| public static Permissions openPermissions() { |
| AccessList acl = new AccessList( |
| ImmutableList.of(new BlessingPattern("...")), ImmutableList.<String>of()); |
| return new Permissions(ImmutableMap.of( |
| Constants.RESOLVE.getValue(), acl, |
| Constants.DEBUG.getValue(), acl, |
| Constants.READ.getValue(), acl, |
| Constants.WRITE.getValue(), acl, |
| Constants.ADMIN.getValue(), acl)); |
| } |
| |
| private String getMountPoint(String testID, int peerID) { |
| return MT_PREFIX + testID + "/s" + peerID; |
| } |
| |
| private String getSyncgroupName(String testID) { |
| return testID + SG_SUFFIX; |
| } |
| |
| // Helper to show default text before the test starts. |
| private void setPreTestText() { |
| String suffix = mUsingProxy ? " with proxy" : " w/o proxy"; |
| String prefix = mPeerID == 0 ? "Pinger: Hello World!" : "Ponger: Hello World!"; |
| updateText(prefix + suffix); |
| } |
| |
| // Helper to change the text shown by the debug TextView. |
| private void updateText(String data) { |
| TextView tv = (TextView) findViewById(R.id.debug_text); |
| tv.setText(data); |
| } |
| |
| // Callback (from button press). Changes the text color between pinger and ponger. |
| public void pingpongToggle(View view) { |
| if (mTestThread == null) { |
| Log.d(TAG, "Toggling peer ID!"); |
| mPeerID = 1 - mPeerID; |
| setPreTestText(); |
| TextView tv = (TextView) findViewById(R.id.debug_text); |
| tv.setTextColor(mPeerID == 0 ? Color.BLACK : Color.BLUE); |
| } |
| } |
| |
| // Callback (from button press). If not pressed yet, the proxy will be added to the listen spec. |
| public void useProxy(View view) { |
| // If we don't proxy, we can't communicate across networks. |
| if (!mUsingProxy) { |
| Log.d(TAG, "Using the proxy!"); |
| try { |
| mBaseContext = V.withListenSpec(mBaseContext, |
| V.getListenSpec(mBaseContext).withProxy("proxy")); |
| mUsingProxy = true; |
| setPreTestText(); |
| ((Button) findViewById(R.id.useproxybutton)).setText(R.string.using_proxy); |
| |
| } catch (final VException e) { |
| Log.e(TAG, "Use proxy failed", e); |
| } |
| } |
| } |
| |
| // Prepares an asynchronous fetch of blessings using the user account. |
| // On some Android versions, this will open an account picker. |
| private void setUpBlessings() { |
| Log.d(TAG, "Attempting to get blessings."); |
| |
| Futures.addCallback(BlessingsManager.getBlessings(mBaseContext, this, "BlessingsKey", true), |
| ON_BLESSINGS); |
| } |
| |
| // Folder where Syncbase will store its data. |
| private String getDataDir() { |
| return getApplicationContext().getApplicationInfo().dataDir + "/" + TEST_ID; |
| } |
| |
| /** |
| * Callback (from button press). If the test hasn't run yet, starts the test. |
| * The protocol is: |
| * 1. Create app hierarchy. |
| * 2. Create syncgroup if pinger, else as ponger, join the syncgroup. |
| * 3. If pinger, write some data to the store. |
| * 4. Watch from the beginning of time. This watch loop will have the pinger write on odd watch |
| * changes. and the ponger write when it sees even watch changes. |
| * 5. After enough watches have been seen, the watch loop ends, and the statistics are printed. |
| * The start time and end time are set during the watch loop. |
| * We allow 1 round of free sync in order to initialize before the start time is recorded. |
| * |
| * @param view |
| */ |
| public void startTestPingPong(final View view) { |
| if (mTestThread == null) { |
| ((Button) findViewById(R.id.pingpongbutton)).setText(R.string.running_ping_pong); |
| |
| Log.d(TAG, "Starting Syncbase Ping Pong Test!"); |
| mTestThread = new Thread() { |
| long startTime; |
| long endTime; |
| |
| // Count starts at -2. (The first ping and first pong are not counted in the test.) |
| int count = -2; |
| |
| @Override |
| public void run() { |
| try { |
| testPingPong(); |
| } catch (ExecutionException | InterruptedException | TimeoutException | |
| VException e) { |
| Log.e(TAG, "Ping pong test failed", e); |
| mBaseContext.cancel(); |
| } |
| } |
| |
| public void testPingPong() throws InterruptedException, ExecutionException, |
| TimeoutException, VException { |
| |
| Log.d(TAG, "Preparing Syncbase Server"); |
| helpUpdateText("Preparing Syncbase Server"); |
| |
| // Create the syncbase server... |
| try { |
| mBaseContext = SyncbaseServer.withNewServer(mBaseContext, |
| new SyncbaseServer.Params() |
| .withPermissions(openPermissions()) |
| .withStorageRootDir(getDataDir()) |
| .withName(getMountPoint(TEST_ID, mPeerID))); |
| } catch (SyncbaseServer.StartException e) { |
| Log.d(TAG, e.toString()); |
| } |
| |
| Server syncbaseServer = io.v.v23.V.getServer(mBaseContext); |
| |
| |
| Log.d(TAG, "Preparing Syncbase Client"); |
| helpUpdateText("Preparing Syncbase Client"); |
| |
| Log.d(TAG, Arrays.toString(syncbaseServer.getStatus().getEndpoints())); |
| |
| // And the syncbase client, app, db, and table... |
| SyncbaseService service = Syncbase.newService( |
| "/" + syncbaseServer.getStatus().getEndpoints()[0]); |
| |
| // Note: In order for the two apps to find each other, the collection and |
| // syncgroup both use "..." as the blessing portion of the id. This must match |
| // or else watch and syncgroup join will not work, and "..." is the simplest |
| // string they can have in common. |
| final Database db = service.getDatabase(mBaseContext, DB_NAME, null); |
| final Collection cx = db.getCollection(new Id("...", CX_NAME)); |
| Log.d(TAG, "db exists?"); |
| if (!syncWithTimeout(db.exists(mBaseContext))) { |
| Log.d(TAG, "db create"); |
| syncWithTimeout(db.create(mBaseContext, |
| Util.filterPermissionsByTags(openPermissions(), |
| io.v.v23.services.syncbase.Constants.ALL_DATABASE_TAGS))); |
| } |
| Log.d(TAG, "cx exists?"); |
| if (!syncWithTimeout(cx.exists(mBaseContext))) { |
| Log.d(TAG, "cx create"); |
| syncWithTimeout(cx.create(mBaseContext, |
| Util.filterPermissionsByTags(openPermissions(), |
| io.v.v23.services.syncbase.Constants.ALL_COLLECTION_TAGS))); |
| } |
| |
| Log.d(TAG, "I am peer " + mPeerID); |
| helpUpdateText("I am peer " + mPeerID); |
| |
| // If you're peer 0, you should create the syncgroup. Otherwise, join it. |
| String sgName = getSyncgroupName(TEST_ID); |
| String mtPointName = MT_PREFIX + "/" + TEST_ID; |
| Syncgroup group = db.getSyncgroup(new Id("...", sgName)); |
| |
| SyncgroupMemberInfo memberInfo = new SyncgroupMemberInfo(); |
| memberInfo.setSyncPriority((byte) 3); |
| if (mPeerID == 0) { |
| Log.d(TAG, "Creating Syncgroup" + sgName); |
| helpUpdateText("Creating Syncgroup"); |
| |
| Permissions sgPerms = Util.filterPermissionsByTags(openPermissions(), |
| io.v.v23.services.syncbase.Constants.ALL_SYNCGROUP_TAGS); |
| |
| SyncgroupSpec spec = new SyncgroupSpec( |
| TEST_ID, null, sgPerms, |
| ImmutableList.of(cx.id()), |
| ImmutableList.of(mtPointName), false); |
| syncWithTimeout(group.create(mBaseContext, spec, memberInfo)); |
| } else { |
| Log.d(TAG, "Joining Syncgroup" + sgName); |
| helpUpdateText("Joining Syncgroup"); |
| // Join at s0 or via the neighborhood. |
| syncWithTimeout(group.join(mBaseContext, getMountPoint(TEST_ID, 0), |
| null, memberInfo)); |
| } |
| |
| // Now that we've all joined the syncgroup... |
| // We should synchronize our times. |
| Log.d(TAG, "We are now ready to time sync!"); |
| helpUpdateText("We are now ready to time sync!"); |
| |
| // 0 will send to 1, and 1 responds via watch. |
| if (mPeerID == 0) { |
| writeData(cx); |
| } |
| |
| watchForChanges(db, cx); |
| |
| Log.d(TAG, "We finished!"); |
| double delta = (endTime - startTime) / (NUM_TIMES * 1000000.); |
| Log.d(TAG, "Average Time: " + delta + " ms per ping pong iteration\n"); |
| helpUpdateText("Average Time: " + delta + "ms per ping pong iteration!"); |
| } |
| |
| // Posts an action through the view to be run on the main thread. |
| // This is the only way we're allowed to change the UI from this Thread. |
| private void helpUpdateText(final String t) { |
| view.post(new Runnable() { |
| @Override |
| public void run() { |
| updateText(t); |
| } |
| }); |
| } |
| |
| private void watchForChanges(Database db, Collection cx) { |
| try { |
| VIterable<WatchChange> watchStream = InputChannels.asIterable( |
| db.watch(mBaseContext, ImmutableList.of( |
| Util.rowPrefixPattern(cx.id(), ""))) |
| ); |
| |
| Log.d(TAG, "Starting watch"); |
| // 0 will send to 1, and vice versa. |
| for (final WatchChange wc : watchStream) { |
| String key = wc.getRowName(); |
| Integer value = (Integer)wc.getValue(); |
| Log.d(TAG, "Watch: " + key + " " + value); |
| helpUpdateText("Watch: " + key + " " + value); |
| count++; |
| |
| if (count == 0) { |
| startTime = System.nanoTime(); |
| } |
| if (count == NUM_TIMES * 2) { |
| endTime = System.nanoTime(); |
| break; |
| } |
| if ((count + 2) % 2 == mPeerID) { |
| writeData(cx); |
| } |
| } |
| Log.d(TAG, "Exiting watch"); |
| } catch (TimeoutException | VException ve) { |
| Log.e(TAG, ve.toString()); |
| } |
| } |
| |
| private void writeData(Collection cx) throws VException, TimeoutException { |
| String actualKey = "" + count; |
| Log.d(TAG, "I write " + count); |
| syncWithTimeout(cx.put(mBaseContext, actualKey, count)); |
| } |
| }; |
| mTestThread.start(); |
| } |
| } |
| |
| private <T> T syncWithTimeout(Future<T> f) throws TimeoutException, VException { |
| return sync(f, TEST_TIMEOUT, TEST_TIMEUNIT); |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.content_main); |
| |
| // Initialize Vanadium as soon as we can. |
| mBaseContext = V.init(getApplicationContext(), new Options() |
| .set(OptionDefs.LOG_VLEVEL, 0) |
| .set(OptionDefs.LOG_VMODULE, "vsync*=2")); |
| |
| // We also need (trusted) blessings in order to be able to communicate with others. |
| setUpBlessings(); |
| } |
| } |