Home | History | Annotate | Download | only in gltrace
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.ide.eclipse.gltrace;
     18 
     19 import com.android.ddmlib.AdbCommandRejectedException;
     20 import com.android.ddmlib.AndroidDebugBridge;
     21 import com.android.ddmlib.Client;
     22 import com.android.ddmlib.IDevice;
     23 import com.android.ddmlib.IDevice.DeviceUnixSocketNamespace;
     24 import com.android.ddmlib.IShellOutputReceiver;
     25 import com.android.ddmlib.ShellCommandUnresponsiveException;
     26 import com.android.ddmlib.TimeoutException;
     27 import com.android.ide.eclipse.gltrace.editors.GLFunctionTraceViewer;
     28 import com.google.common.io.Closeables;
     29 import com.google.common.util.concurrent.SimpleTimeLimiter;
     30 
     31 import org.eclipse.core.filesystem.EFS;
     32 import org.eclipse.core.filesystem.IFileStore;
     33 import org.eclipse.core.runtime.Path;
     34 import org.eclipse.jface.action.IAction;
     35 import org.eclipse.jface.dialogs.MessageDialog;
     36 import org.eclipse.jface.viewers.ISelection;
     37 import org.eclipse.jface.window.Window;
     38 import org.eclipse.swt.widgets.Display;
     39 import org.eclipse.swt.widgets.Shell;
     40 import org.eclipse.ui.IEditorInput;
     41 import org.eclipse.ui.IEditorReference;
     42 import org.eclipse.ui.IURIEditorInput;
     43 import org.eclipse.ui.IWorkbench;
     44 import org.eclipse.ui.IWorkbenchPage;
     45 import org.eclipse.ui.IWorkbenchWindow;
     46 import org.eclipse.ui.IWorkbenchWindowActionDelegate;
     47 import org.eclipse.ui.PartInitException;
     48 import org.eclipse.ui.PlatformUI;
     49 import org.eclipse.ui.WorkbenchException;
     50 import org.eclipse.ui.ide.IDE;
     51 
     52 import java.io.DataInputStream;
     53 import java.io.DataOutputStream;
     54 import java.io.FileNotFoundException;
     55 import java.io.FileOutputStream;
     56 import java.io.IOException;
     57 import java.net.Socket;
     58 import java.util.concurrent.Callable;
     59 import java.util.concurrent.Semaphore;
     60 import java.util.concurrent.TimeUnit;
     61 
     62 public class CollectTraceAction implements IWorkbenchWindowActionDelegate {
     63     /** Abstract Unix Domain Socket Name used by the gltrace device code. */
     64     private static final String GLTRACE_UDS = "gltrace";        //$NON-NLS-1$
     65 
     66     /** Local port that is forwarded to the device's {@link #GLTRACE_UDS} socket. */
     67     private static final int LOCAL_FORWARDED_PORT = 6039;
     68 
     69     /** Activity name to use for a system activity that has already been launched. */
     70     private static final String SYSTEM_APP = "system";          //$NON-NLS-1$
     71 
     72     /** Time to wait for the application to launch (seconds) */
     73     private static final int LAUNCH_TIMEOUT = 15;
     74 
     75     /** Time to wait for the application to die (seconds) */
     76     private static final int KILL_TIMEOUT = 5;
     77 
     78     private static final int MIN_API_LEVEL = 16;
     79 
     80     @Override
     81     public void run(IAction action) {
     82         connectToDevice();
     83     }
     84 
     85     @Override
     86     public void selectionChanged(IAction action, ISelection selection) {
     87     }
     88 
     89     @Override
     90     public void dispose() {
     91     }
     92 
     93     @Override
     94     public void init(IWorkbenchWindow window) {
     95     }
     96 
     97     private void connectToDevice() {
     98         Shell shell = Display.getDefault().getActiveShell();
     99         GLTraceOptionsDialog dlg = new GLTraceOptionsDialog(shell);
    100         if (dlg.open() != Window.OK) {
    101             return;
    102         }
    103 
    104         TraceOptions traceOptions = dlg.getTraceOptions();
    105 
    106         IDevice device = getDevice(traceOptions.device);
    107         String apiLevelString = device.getProperty(IDevice.PROP_BUILD_API_LEVEL);
    108         int apiLevel;
    109         try {
    110             apiLevel = Integer.parseInt(apiLevelString);
    111         } catch (NumberFormatException e) {
    112             apiLevel = MIN_API_LEVEL;
    113         }
    114         if (apiLevel < MIN_API_LEVEL) {
    115             MessageDialog.openError(shell, "GL Trace",
    116                     String.format("OpenGL Tracing is only supported on devices at API Level %1$d."
    117                             + "The selected device '%2$s' provides API level %3$s.",
    118                                     MIN_API_LEVEL, traceOptions.device, apiLevelString));
    119             return;
    120         }
    121 
    122         try {
    123             setupForwarding(device, LOCAL_FORWARDED_PORT);
    124         } catch (Exception e) {
    125             MessageDialog.openError(shell, "Setup GL Trace",
    126                     "Error while setting up port forwarding: " + e.getMessage());
    127             return;
    128         }
    129 
    130         try {
    131             if (!SYSTEM_APP.equals(traceOptions.appToTrace)) {
    132                 startActivity(device, traceOptions.appToTrace, traceOptions.activityToTrace,
    133                         traceOptions.isActivityNameFullyQualified);
    134             }
    135         } catch (Exception e) {
    136             MessageDialog.openError(shell, "Setup GL Trace",
    137                     "Error while launching application: " + e.getMessage());
    138             return;
    139         }
    140 
    141         // if everything went well, the app should now be waiting for the gl debugger
    142         // to connect
    143         startTracing(shell, traceOptions, LOCAL_FORWARDED_PORT);
    144 
    145         // once tracing is complete, remove port forwarding
    146         disablePortForwarding(device, LOCAL_FORWARDED_PORT);
    147 
    148         // and finally open the editor to view the file
    149         openInEditor(shell, traceOptions.traceDestination);
    150     }
    151 
    152     public static void openInEditor(Shell shell, String traceFilePath) {
    153         final IFileStore fileStore = EFS.getLocalFileSystem().getStore(new Path(traceFilePath));
    154         if (!fileStore.fetchInfo().exists()) {
    155             return;
    156         }
    157 
    158         final IWorkbench workbench = PlatformUI.getWorkbench();
    159         IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
    160         if (window == null) {
    161             return;
    162         }
    163 
    164         IWorkbenchPage page = window.getActivePage();
    165         if (page == null) {
    166             return;
    167         }
    168 
    169         try {
    170             workbench.showPerspective("com.android.ide.eclipse.gltrace.perspective", window);
    171         } catch (WorkbenchException e) {
    172         }
    173 
    174         // if there is a editor already open, then refresh its model
    175         GLFunctionTraceViewer viewer = getOpenTraceViewer(page, traceFilePath);
    176         if (viewer != null) {
    177             viewer.setInput(shell, traceFilePath);
    178         }
    179 
    180         // open the editor (if not open), or bring it to foreground if it is already open
    181         try {
    182             IDE.openEditorOnFileStore(page, fileStore);
    183         } catch (PartInitException e) {
    184             GlTracePlugin.getDefault().logMessage(
    185                     "Unexpected error while opening gltrace file in editor: " + e);
    186             return;
    187         }
    188     }
    189 
    190     /**
    191      * Returns the editor part that has the provided file path open.
    192      * @param page page containing editors
    193      * @param traceFilePath file that should be open in an editor
    194      * @return if given trace file is already open, then a reference to that editor part,
    195      *         null otherwise
    196      */
    197     private static GLFunctionTraceViewer getOpenTraceViewer(IWorkbenchPage page,
    198             String traceFilePath) {
    199         IEditorReference[] editorRefs = page.getEditorReferences();
    200         for (IEditorReference ref : editorRefs) {
    201             String id = ref.getId();
    202             if (!GLFunctionTraceViewer.ID.equals(id)) {
    203                 continue;
    204             }
    205 
    206             IEditorInput input = null;
    207             try {
    208                 input = ref.getEditorInput();
    209             } catch (PartInitException e) {
    210                 continue;
    211             }
    212 
    213             if (!(input instanceof IURIEditorInput)) {
    214                 continue;
    215             }
    216 
    217             if (traceFilePath.equals(((IURIEditorInput) input).getURI().getPath())) {
    218                 return (GLFunctionTraceViewer) ref.getEditor(true);
    219             }
    220         }
    221 
    222         return null;
    223     }
    224 
    225     @SuppressWarnings("resource") // Closeables.closeQuietly
    226     public static void startTracing(Shell shell, TraceOptions traceOptions, int port) {
    227         Socket socket = new Socket();
    228         DataInputStream traceDataStream = null;
    229         DataOutputStream traceCommandsStream = null;
    230         try {
    231             socket.connect(new java.net.InetSocketAddress("127.0.0.1", port)); //$NON-NLS-1$
    232             socket.setTcpNoDelay(true);
    233             traceDataStream = new DataInputStream(socket.getInputStream());
    234             traceCommandsStream = new DataOutputStream(socket.getOutputStream());
    235         } catch (IOException e) {
    236             MessageDialog.openError(shell,
    237                     "OpenGL Trace",
    238                     "Unable to connect to remote GL Trace Server: " + e.getMessage());
    239             return;
    240         }
    241 
    242         // create channel to send trace commands to device
    243         TraceCommandWriter traceCommandWriter = new TraceCommandWriter(traceCommandsStream);
    244         try {
    245             traceCommandWriter.setTraceOptions(traceOptions.collectFbOnEglSwap,
    246                     traceOptions.collectFbOnGlDraw,
    247                     traceOptions.collectTextureData);
    248         } catch (IOException e) {
    249             MessageDialog.openError(shell,
    250                     "OpenGL Trace",
    251                     "Unexpected error while setting trace options: " + e.getMessage());
    252             closeSocket(socket);
    253             return;
    254         }
    255 
    256         FileOutputStream fos = null;
    257         try {
    258             fos = new FileOutputStream(traceOptions.traceDestination, false);
    259         } catch (FileNotFoundException e) {
    260             // input path is valid, so this cannot occur
    261         }
    262 
    263         // create trace writer that writes to a trace file
    264         TraceFileWriter traceFileWriter = new TraceFileWriter(fos, traceDataStream);
    265         traceFileWriter.start();
    266 
    267         GLTraceCollectorDialog dlg = new GLTraceCollectorDialog(shell,
    268                 traceFileWriter,
    269                 traceCommandWriter,
    270                 traceOptions);
    271         dlg.open();
    272 
    273         traceFileWriter.stopTracing();
    274         traceCommandWriter.close();
    275         closeSocket(socket);
    276     }
    277 
    278     private static void closeSocket(Socket socket) {
    279         try {
    280             socket.close();
    281         } catch (IOException e) {
    282             // ignore error while closing socket
    283         }
    284     }
    285 
    286     private void startActivity(IDevice device, String appPackage, String activity,
    287             boolean isActivityNameFullyQualified)
    288             throws TimeoutException, AdbCommandRejectedException,
    289             ShellCommandUnresponsiveException, IOException, InterruptedException {
    290         killApp(device, appPackage); // kill app if it is already running
    291         waitUntilAppKilled(device, appPackage, KILL_TIMEOUT);
    292 
    293         StringBuilder activityPath = new StringBuilder(appPackage);
    294         if (!activity.isEmpty()) {
    295             activityPath.append('/');
    296             if (!isActivityNameFullyQualified) {
    297                 activityPath.append('.');
    298             }
    299             activityPath.append(activity);
    300         }
    301         String startAppCmd = String.format(
    302                 "am start --opengl-trace %s -a android.intent.action.MAIN -c android.intent.category.LAUNCHER", //$NON-NLS-1$
    303                 activityPath.toString());
    304 
    305         Semaphore launchCompletionSempahore = new Semaphore(0);
    306         StartActivityOutputReceiver receiver = new StartActivityOutputReceiver(
    307                 launchCompletionSempahore);
    308         device.executeShellCommand(startAppCmd, receiver);
    309 
    310         // wait until shell finishes launch command
    311         launchCompletionSempahore.acquire();
    312 
    313         // throw exception if there was an error during launch
    314         String output = receiver.getOutput();
    315         if (output.contains("Error")) {             //$NON-NLS-1$
    316             throw new RuntimeException(output);
    317         }
    318 
    319         // wait until the app itself has been launched
    320         waitUntilAppLaunched(device, appPackage, LAUNCH_TIMEOUT);
    321     }
    322 
    323     private void killApp(IDevice device, String appName) {
    324         Client client = device.getClient(appName);
    325         if (client != null) {
    326             client.kill();
    327         }
    328     }
    329 
    330     private void waitUntilAppLaunched(final IDevice device, final String appName, int timeout) {
    331         Callable<Boolean> c = new Callable<Boolean>() {
    332             @Override
    333             public Boolean call() throws Exception {
    334                 Client client;
    335                 do {
    336                     client = device.getClient(appName);
    337                 } while (client == null);
    338 
    339                 return Boolean.TRUE;
    340             }
    341         };
    342         try {
    343             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
    344         } catch (Exception e) {
    345             throw new RuntimeException("Timed out waiting for application to launch.");
    346         }
    347 
    348         // once the app has launched, wait an additional couple of seconds
    349         // for it to start up
    350         try {
    351             Thread.sleep(2000);
    352         } catch (InterruptedException e) {
    353             // ignore
    354         }
    355     }
    356 
    357     private void waitUntilAppKilled(final IDevice device, final String appName, int timeout) {
    358         Callable<Boolean> c = new Callable<Boolean>() {
    359             @Override
    360             public Boolean call() throws Exception {
    361                 Client client;
    362                 while ((client = device.getClient(appName)) != null) {
    363                     client.kill();
    364                 }
    365                 return Boolean.TRUE;
    366             }
    367         };
    368         try {
    369             new SimpleTimeLimiter().callWithTimeout(c, timeout, TimeUnit.SECONDS, true);
    370         } catch (Exception e) {
    371             throw new RuntimeException("Timed out waiting for running application to die.");
    372         }
    373     }
    374 
    375     public static void setupForwarding(IDevice device, int i)
    376             throws TimeoutException, AdbCommandRejectedException, IOException {
    377         device.createForward(i, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
    378     }
    379 
    380     public static void disablePortForwarding(IDevice device, int port) {
    381         try {
    382             device.removeForward(port, GLTRACE_UDS, DeviceUnixSocketNamespace.ABSTRACT);
    383         } catch (Exception e) {
    384             // ignore exceptions;
    385         }
    386     }
    387 
    388     private IDevice getDevice(String deviceName) {
    389         IDevice[] devices = AndroidDebugBridge.getBridge().getDevices();
    390 
    391         for (IDevice device : devices) {
    392             if (device.getName().equals(deviceName)) {
    393                 return device;
    394             }
    395         }
    396 
    397         return null;
    398     }
    399 
    400     private static class StartActivityOutputReceiver implements IShellOutputReceiver {
    401         private Semaphore mSemaphore;
    402         private StringBuffer sb = new StringBuffer(300);
    403 
    404         public StartActivityOutputReceiver(Semaphore s) {
    405             mSemaphore = s;
    406         }
    407 
    408         @Override
    409         public void addOutput(byte[] data, int offset, int length) {
    410             String d = new String(data, offset, length);
    411             sb.append(d);
    412         }
    413 
    414         @Override
    415         public void flush() {
    416             mSemaphore.release();
    417         }
    418 
    419         @Override
    420         public boolean isCancelled() {
    421             return false;
    422         }
    423 
    424         public String getOutput() {
    425             return sb.toString();
    426         }
    427     }
    428 }
    429