Java tutorial
/* * Copyright (C) 2016 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.tools.idea.monitor.tool; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.ddmlib.*; import com.android.tools.datastore.DataStoreService; import com.android.tools.datastore.LegacyAllocationConverter.CallStack; import com.android.tools.profilers.ProfilerClient; import com.android.tools.datastore.LegacyAllocationConverter; import com.android.tools.datastore.LegacyAllocationTracker; import com.android.tools.idea.ddms.EdtExecutor; import com.android.tools.idea.ddms.adb.AdbService; import com.android.tools.profiler.proto.Profiler; import com.google.common.base.Charsets; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.util.Consumer; import com.intellij.util.net.NetUtils; import org.jetbrains.android.sdk.AndroidSdkUtils; import org.jetbrains.annotations.NotNull; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.*; import java.util.concurrent.ExecutorService; import static com.android.ddmlib.Client.CHANGE_NAME; import static com.android.ddmlib.IDevice.CHANGE_STATE; /** * Manages the interactions between DDMLIB provided devices, and what is needed to spawn ProfilerClient's. * On device connection it will spawn the performance daemon on device, and will notify the profiler system that * a new device has been connected. *ALL* interaction with IDevice is encapsulated in this class. */ class StudioProfilerDeviceManager implements AndroidDebugBridge.IClientChangeListener, AndroidDebugBridge.IDeviceChangeListener, AndroidDebugBridge.IDebugBridgeChangeListener { private static final int DEVICE_PORT = 12389; private static final String DATASTORE_NAME = "DataStoreService"; private final ProfilerClient myClient; private final DataStoreService myDataStoreService; private final StudioLegacyAllocationTracker myLegacyAllocationTracker; private AndroidDebugBridge myBridge; public StudioProfilerDeviceManager(@NotNull Project project) throws IOException { final File adb = AndroidSdkUtils.getAdb(project); if (adb == null) { throw new IllegalStateException("No adb found"); } //TODO: Spawn the datastore in the right place (service)? myDataStoreService = new DataStoreService(DATASTORE_NAME); // The client is referenced in the update devices callback. As such the client needs to be set before we register // ourself as a listener for this callback. Otherwise we may get the callback before we are fully constructed myClient = new ProfilerClient(DATASTORE_NAME); ListenableFuture<AndroidDebugBridge> future = AdbService.getInstance().getDebugBridge(adb); Futures.addCallback(future, new FutureCallback<AndroidDebugBridge>() { @Override public void onSuccess(@Nullable AndroidDebugBridge bridge) { myBridge = bridge; } @Override public void onFailure(@NotNull Throwable t) { } }, EdtExecutor.INSTANCE); AndroidDebugBridge.addClientChangeListener(this); AndroidDebugBridge.addDeviceChangeListener(this); AndroidDebugBridge.addDebugBridgeChangeListener(this); myLegacyAllocationTracker = new StudioLegacyAllocationTracker(); myDataStoreService.setLegacyAllocationTracker(myLegacyAllocationTracker); } private static Logger getLogger() { return Logger.getInstance(StudioProfilerDeviceManager.class); } public void updateDevices() { if (myBridge != null) { Profiler.SetProcessesRequest.Builder builder = Profiler.SetProcessesRequest.newBuilder(); for (IDevice device : myBridge.getDevices()) { if (device.isOnline()) { Profiler.Device profilerDevice = Profiler.Device.newBuilder() .setSerial(device.getSerialNumber()).setModel(device.getName()).build(); Profiler.DeviceProcesses.Builder deviceProcesses = Profiler.DeviceProcesses.newBuilder(); deviceProcesses.setDevice(profilerDevice); for (Client client : device.getClients()) { String description = client.getClientData().getClientDescription(); deviceProcesses.addProcess(Profiler.Process.newBuilder() .setName(description == null ? "[UNKNOWN]" : description) .setPid(client.getClientData().getPid()).build()); } builder.addDeviceProcesses(deviceProcesses.build()); myLegacyAllocationTracker.setDevice(device); } } myClient.getProfilerClient().setProcesses(builder.build()); } } @Override public void clientChanged(@NonNull Client client, int changeMask) { if ((changeMask & CHANGE_NAME) != 0) { updateDevices(); } } @Override public void deviceConnected(@NonNull IDevice device) { updateDevices(); if (device.isOnline()) { spawnPerfd(device); } } @Override public void deviceDisconnected(@NonNull IDevice device) { updateDevices(); } @Override public void deviceChanged(@NonNull IDevice device, int changeMask) { updateDevices(); if ((changeMask & CHANGE_STATE) != 0 && device.isOnline()) { spawnPerfd(device); } } @Override public void bridgeChanged(@Nullable AndroidDebugBridge bridge) { myBridge = bridge; } public ProfilerClient getClient() { return myClient; } private void spawnPerfd(@NonNull IDevice device) { PerfdThread thread = new PerfdThread(device, myClient); thread.start(); } private static class NullReceiver implements IShellOutputReceiver { @Override public void addOutput(byte[] data, int offset, int length) { } @Override public void flush() { } @Override public boolean isCancelled() { return false; } } private static class PerfdThread extends Thread { private final IDevice myDevice; private final ProfilerClient myClient; private int myLocalPort; public PerfdThread(IDevice device, ProfilerClient client) { super("Perfd Thread: " + device.getSerialNumber()); myDevice = device; myClient = client; myLocalPort = 0; } @Override public void run() { try { // TODO: Add support for non-development perfd locations. String dir = "../../out/studio/native/out/release"; File perfd = null; for (String abi : myDevice.getAbis()) { File candidate = new File(PathManager.getHomePath(), dir + "/" + abi + "/perfd"); if (candidate.exists()) { perfd = candidate; } } // TODO: Handle the case where we don't have perfd for this platform. assert perfd != null; // TODO: Add debug support for development String devicePath = "/data/local/tmp/perfd/"; myDevice.executeShellCommand("mkdir -p " + devicePath, new NullReceiver()); myDevice.pushFile(perfd.getAbsolutePath(), devicePath + "/perfd"); myDevice.executeShellCommand("chmod +x " + devicePath + "perfd", new NullReceiver()); myDevice.executeShellCommand(devicePath + "perfd", new IShellOutputReceiver() { @Override public void addOutput(byte[] data, int offset, int length) { String s = new String(data, Charsets.UTF_8); getLogger().info("[perfd]: " + s); if (s.contains("Server listening")) { try { myLocalPort = NetUtils.findAvailableSocketPort(); myDevice.createForward(myLocalPort, DEVICE_PORT); myClient.getProfilerClient() .connect(Profiler.ConnectRequest.newBuilder().setPort(myLocalPort).build()); } catch (TimeoutException | AdbCommandRejectedException | IOException e) { throw new RuntimeException(e); } } } @Override public void flush() { } @Override public boolean isCancelled() { return false; } }, 0, null); if (myLocalPort > 0) { myClient.getProfilerClient() .disconnect(Profiler.DisconnectRequest.newBuilder().setPort(myLocalPort).build()); getLogger().info("Terminating perfd thread"); } } catch (TimeoutException | AdbCommandRejectedException | SyncException | ShellCommandUnresponsiveException | IOException e) { throw new RuntimeException(e); } } } private class StudioLegacyAllocationTracker implements LegacyAllocationTracker { private IDevice myDevice; private final LegacyAllocationConverter myConverter = new LegacyAllocationConverter(); public void setDevice(IDevice device) { myDevice = device; } @Override public boolean setAllocationTrackingEnabled(int processId, boolean enabled) { Client client = getClient(myDevice, processId); if (client == null) { return false; } client.enableAllocationTracker(enabled); return true; } @Override public void getAllocationTrackingDump(int processId, @NotNull ExecutorService executorService, @NotNull Consumer<byte[]> consumer) { Client targetClient = getClient(myDevice, processId); if (targetClient == null) { return; } AndroidDebugBridge.addClientChangeListener(new AndroidDebugBridge.IClientChangeListener() { @Override public void clientChanged(@NonNull Client client, int changeMask) { if (targetClient == client && (changeMask & Client.CHANGE_HEAP_ALLOCATIONS) != 0) { final byte[] data = client.getClientData().getAllocationsData(); executorService.submit(() -> consumer.consume(data)); AndroidDebugBridge.removeClientChangeListener(this); } } }); targetClient.requestAllocationDetails(); } @NotNull @Override public LegacyAllocationConverter parseDump(@NotNull byte[] dumpData) { myConverter.prepare(); // TODO fix allocation file overflow bug AllocationInfo[] rawInfos = AllocationsParser.parse(ByteBuffer.wrap(dumpData)); for (AllocationInfo info : rawInfos) { List<StackTraceElement> stackTraceElements = Arrays.asList(info.getStackTrace()); CallStack callStack = myConverter.addCallStack(stackTraceElements); int classId = myConverter.addClassName(info.getAllocatedClass()); myConverter.addAllocation(new LegacyAllocationConverter.Allocation(classId, info.getSize(), info.getThreadId(), callStack.getId())); } return myConverter; } @Nullable private Client getClient(@NotNull IDevice device, int processId) { if (myBridge == null) { return null; } return device.getClient(device.getClientName(processId)); } } }