diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/CollectTraceAction.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/CollectTraceAction.java | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/CollectTraceAction.java b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/CollectTraceAction.java new file mode 100644 index 000000000..c13ab461b --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.gldebugger/src/com/android/ide/eclipse/gltrace/CollectTraceAction.java @@ -0,0 +1,428 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.gltrace; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.Client; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace; +import com.android.ddmlib.IShellOutputReceiver; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.android.ide.eclipse.gltrace.editors.GLFunctionTraceViewer; +import com.google.common.io.Closeables; +import com.google.common.util.concurrent.SimpleTimeLimiter; + +import org.eclipse.core.filesystem.EFS; +import org.eclipse.core.filesystem.IFileStore; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.action.IAction; +import org.eclipse.jface.dialogs.MessageDialog; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IEditorReference; +import org.eclipse.ui.IURIEditorInput; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.IWorkbenchWindowActionDelegate; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.WorkbenchException; +import org.eclipse.ui.ide.IDE; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.util.concurrent.Callable; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +public class CollectTraceAction implements IWorkbenchWindowActionDelegate { + /** Abstract Unix Domain Socket Name used by the gltrace device code. */ + private static final String GLTRACE_UDS = "gltrace"; //$NON-NLS-1$ + + /** Local port that is forwarded to the device's {@link #GLTRACE_UDS} socket. */ + private static final int LOCAL_FORWARDED_PORT = 6039; + + /** Activity name to use for a system activity that has already been launched. */ + private static final String SYSTEM_APP = "system"; //$NON-NLS-1$ + + /** Time to wait for the application to launch (seconds) */ + private static final int LAUNCH_TIMEOUT = 15; + + /** Time to wait for the application to die (seconds) */ + private static final int KILL_TIMEOUT = 5; + + private static final int MIN_API_LEVEL = 16; + + @Override + public void run(IAction action) { + connectToDevice(); + } + + @Override + public void selectionChanged(IAction action, ISelection selection) { + } + + @Override + public void dispose() { + } + + @Override + public void init(IWorkbenchWindow window) { + } + + private void connectToDevice() { + Shell shell = Display.getDefault().getActiveShell(); + GLTraceOptionsDialog dlg = new GLTraceOptionsDialog(shell); + if (dlg.open() != Window.OK) { + return; + } + + TraceOptions traceOptions = dlg.getTraceOptions(); + + IDevice device = getDevice(traceOptions.device); + String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL); + int apiLevel; + try { + apiLevel = Integer.parseInt(apiLevelString); + } catch (NumberFormatException e) { + apiLevel = MIN_API_LEVEL; + } + if (apiLevel < MIN_API_LEVEL) { + MessageDialog.openError(shell, "GL Trace", + String.format("OpenGL Tracing is only supported on devices at API Level %1$d." + + "The selected device '%2$s' provides API level %3$s.", + MIN_API_LEVEL, traceOptions.device, apiLevelString)); + return; + } + + try { + setupForwarding(device, LOCAL_FORWARDED_PORT); + } catch (Exception e) { + MessageDialog.openError(shell, "Setup GL Trace", + "Error while setting up port forwarding: " + e.getMessage()); + return; + } + + try { + if (!SYSTEM_APP.equals(traceOptions.appToTrace)) { + startActivity(device, traceOptions.appToTrace, traceOptions.activityToTrace, + traceOptions.isActivityNameFullyQualified); + } + } catch (Exception e) { + MessageDialog.openError(shell, "Setup GL Trace", + "Error while launching application: " + e.getMessage()); + return; + } + + // if everything went well, the app should now be waiting for the gl debugger + // to connect + startTracing(shell, traceOptions, LOCAL_FORWARDED_PORT); + + // once tracing is complete, remove port forwarding + disablePortForwarding(device, LOCAL_FORWARDED_PORT); + + // and finally open the editor to view the file + openInEditor(shell, traceOptions.traceDestination); + } + + public static void openInEditor(Shell shell, String traceFilePath) { + final IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(traceFilePath)); + if (!fileStore.fetchInfo().exists()) { + return; + } + + final IWorkbench workbench = PlatformUI.getWorkbench(); + IWorkbenchWindow window = workbench.getActiveWorkbenchWindow(); + if (window == null) { + return; + } + + IWorkbenchPage page = window.getActivePage(); + if (page == null) { + return; + } + + try { + workbench.showPerspective("com.android.ide.eclipse.gltrace.perspective", window); + } catch (WorkbenchException e) { + } + + // if there is a editor already open, then refresh its model + GLFunctionTraceViewer viewer = getOpenTraceViewer(page, traceFilePath); + if (viewer != null) { + viewer.setInput(shell, traceFilePath); + } + + // open the editor (if not open), or bring it to foreground if it is already open + try { + IDE.openEditorOnFileStore(page, fileStore); + } catch (PartInitException e) { + GlTracePlugin.getDefault().logMessage( + "Unexpected error while opening gltrace file in editor: " + e); + return; + } + } + + /** + * Returns the editor part that has the provided file path open. + * @param page page containing editors + * @param traceFilePath file that should be open in an editor + * @return if given trace file is already open, then a reference to that editor part, + * null otherwise + */ + private static GLFunctionTraceViewer getOpenTraceViewer(IWorkbenchPage page, + String traceFilePath) { + IEditorReference[] editorRefs = page.getEditorReferences(); + for (IEditorReference ref : editorRefs) { + String id = ref.getId(); + if (!GLFunctionTraceViewer.ID.equals(id)) { + continue; + } + + IEditorInput input = null; + try { + input = ref.getEditorInput(); + } catch (PartInitException e) { + continue; + } + + if (!(input instanceof IURIEditorInput)) { + continue; + } + + if (traceFilePath.equals(((IURIEditorInput) input).getURI().getPath())) { + return (GLFunctionTraceViewer) ref.getEditor(true); + } + } + + return null; + } + + @SuppressWarnings("resource") // Closeables.closeQuietly + public static void startTracing(Shell shell, TraceOptions traceOptions, int port) { + Socket socket = new Socket(); + DataInputStream traceDataStream = null; + DataOutputStream traceCommandsStream = null; + try { + socket.connect(new java.net.InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$ + socket.setTcpNoDelay(true); + traceDataStream = new DataInputStream(socket.getInputStream()); + traceCommandsStream = new DataOutputStream(socket.getOutputStream()); + } catch (IOException e) { + MessageDialog.openError(shell, + "OpenGL Trace", + "Unable to connect to remote GL Trace Server: " + e.getMessage()); + return; + } + + // create channel to send trace commands to device + TraceCommandWriter traceCommandWriter = new TraceCommandWriter(traceCommandsStream); + try { + traceCommandWriter.setTraceOptions(traceOptions.collectFbOnEglSwap, + traceOptions.collectFbOnGlDraw, + traceOptions.collectTextureData); + } catch (IOException e) { + MessageDialog.openError(shell, + "OpenGL Trace", + "Unexpected error while setting trace options: " + e.getMessage()); + closeSocket(socket); + return; + } + + FileOutputStream fos = null; + try { + fos = new FileOutputStream(traceOptions.traceDestination, false); + } catch (FileNotFoundException e) { + // input path is valid, so this cannot occur + } + + // create trace writer that writes to a trace file + TraceFileWriter traceFileWriter = new TraceFileWriter(fos, traceDataStream); + traceFileWriter.start(); + + GLTraceCollectorDialog dlg = new GLTraceCollectorDialog(shell, + traceFileWriter, + traceCommandWriter, + traceOptions); + dlg.open(); + + traceFileWriter.stopTracing(); + traceCommandWriter.close(); + closeSocket(socket); + } + + private static void closeSocket(Socket socket) { + try { + socket.close(); + } catch (IOException e) { + // ignore error while closing socket + } + } + + private void startActivity(IDevice device, String appPackage, String activity, + boolean isActivityNameFullyQualified) + throws TimeoutException, AdbCommandRejectedException, + ShellCommandUnresponsiveException, IOException, InterruptedException { + killApp(device, appPackage); // kill app if it is already running + waitUntilAppKilled(device, appPackage, KILL_TIMEOUT); + + StringBuilder activityPath = new StringBuilder(appPackage); + if (!activity.isEmpty()) { + activityPath.append('/'); + if (!isActivityNameFullyQualified) { + activityPath.append('.'); + } + activityPath.append(activity); + } + String startAppCmd = String.format( + "am start --opengl-trace %s -a android.intent.action.MAIN -c android.intent.category.LAUNCHER", //$NON-NLS-1$ + activityPath.toString()); + + Semaphore launchCompletionSempahore = new Semaphore(0); + StartActivityOutputReceiver receiver = new StartActivityOutputReceiver( + launchCompletionSempahore); + device.executeShellCommand(startAppCmd, receiver); + + // wait until shell finishes launch command + launchCompletionSempahore.acquire(); + + // throw exception if there was an error during launch + String output = receiver.getOutput(); + if (output.contains("Error")) { //$NON-NLS-1$ + throw new RuntimeException(output); + } + + // wait until the app itself has been launched + waitUntilAppLaunched(device, appPackage, LAUNCH_TIMEOUT); + } + + private void killApp(IDevice device, String appName) { + Client client = device.getClient(appName); + if (client != null) { + client.kill(); + } + } + + private void waitUntilAppLaunched(final IDevice device, final String appName, int timeout) { + Callable<Boolean> c = new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + Client client; + do { + client = device.getClient(appName); + } while (client == null); + + return Boolean.TRUE; + } + }; + try { + new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true); + } catch (Exception e) { + throw new RuntimeException("Timed out waiting for application to launch."); + } + + // once the app has launched, wait an additional couple of seconds + // for it to start up + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + // ignore + } + } + + private void waitUntilAppKilled(final IDevice device, final String appName, int timeout) { + Callable<Boolean> c = new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + Client client; + while ((client = device.getClient(appName)) != null) { + client.kill(); + } + return Boolean.TRUE; + } + }; + try { + new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true); + } catch (Exception e) { + throw new RuntimeException("Timed out waiting for running application to die."); + } + } + + public static void setupForwarding(IDevice device, int i) + throws TimeoutException, AdbCommandRejectedException, IOException { + device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT); + } + + public static void disablePortForwarding(IDevice device, int port) { + try { + device.removeForward(port, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT); + } catch (Exception e) { + // ignore exceptions; + } + } + + private IDevice getDevice(String deviceName) { + IDevice[] devices = AndroidDebugBridge.getBridge().getDevices(); + + for (IDevice device : devices) { + if (device.getName().equals(deviceName)) { + return device; + } + } + + return null; + } + + private static class StartActivityOutputReceiver implements IShellOutputReceiver { + private Semaphore mSemaphore; + private StringBuffer sb = new StringBuffer(300); + + public StartActivityOutputReceiver(Semaphore s) { + mSemaphore = s; + } + + @Override + public void addOutput(byte[] data, int offset, int length) { + String d = new String(data, offset, length); + sb.append(d); + } + + @Override + public void flush() { + mSemaphore.release(); + } + + @Override + public boolean isCancelled() { + return false; + } + + public String getOutput() { + return sb.toString(); + } + } +} |