/* * Copyright (C) 2014 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.memory; import com.android.ddmlib.AndroidDebugBridge; import com.android.ddmlib.Client; import com.android.ddmlib.ClientData; import com.google.common.collect.Lists; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; public class MemorySampler implements Runnable, AndroidDebugBridge.IClientChangeListener, ClientData.IHprofDumpHandler { /** * Sample type when the device cannot be seen. */ public static final int TYPE_UNREACHABLE = 0; /** * Sample created from a valid HPIF response. */ public static final int TYPE_DATA = 1; /** * The device is reachable but no HPIF response was received in time. */ public static final int TYPE_TIMEOUT = 2; /** * A sample that marks the beginning of an HPROF request. */ public static final int TYPE_HPROF_REQUEST = 3; /** * A sample flagging that an HPROF dump has been received. */ public static final int TYPE_HPROF_RESULT = 4; private static final Logger LOG = Logger.getInstance(MemorySampler.class); private static int ourLastHprofRequestId = 0; @NotNull private final List<MemorySamplerListener> myListeners = Lists.newLinkedList(); @NotNull private final TimelineData myData; @NotNull private final Semaphore myDataSemaphore; private final int mySampleFrequencyMs; /** * The future representing the task being executed, which will return null upon successful completion. * If null, no current task is being executed. */ @Nullable private volatile Future<?> myExecutingTask; @Nullable private volatile Client myClient; private volatile boolean myRunning; private int myPendingHprofId; MemorySampler(@NotNull TimelineData data, int sampleFrequencyMs) { mySampleFrequencyMs = sampleFrequencyMs; myData = data; myDataSemaphore = new Semaphore(0, true); myPendingHprofId = 0; myData.freeze(); } private static int getNextHprofId() { return ++ourLastHprofRequestId; } @SuppressWarnings("ConstantConditions") private void sample(int type, int id) { float freeMb = 0.0f; float allocMb = 0.0f; if (myClient != null) { ClientData.HeapInfo m = myClient.getClientData().getVmHeapInfo(1); if (m != null) { allocMb = m.bytesAllocated / (1024.f * 1024.f); freeMb = m.sizeInBytes / (1024.f * 1024.f) - allocMb; } } else { type = TYPE_UNREACHABLE; } // We cannot use the timeStamp in HeapInfo because it's based on the current time of the attached device. myData.add(System.currentTimeMillis(), type, id, allocMb, freeMb); } @Override public void clientChanged(@NotNull Client client, int changeMask) { if (myClient != null && myClient == client) { if ((changeMask & Client.CHANGE_HEAP_DATA) != 0) { myDataSemaphore.release(); } } } @SuppressWarnings("ConstantConditions") public void start() { if (myExecutingTask == null && myClient != null) { myData.clear(); AndroidDebugBridge.addClientChangeListener(this); myRunning = true; myExecutingTask = ApplicationManager.getApplication().executeOnPooledThread(this); myClient.setHeapInfoUpdateEnabled(true); for (MemorySamplerListener listener : myListeners) { listener.onStart(); } } } @SuppressWarnings("ConstantConditions") public void stop() { if (myExecutingTask != null) { myRunning = false; myDataSemaphore.release(); try { // Wait for the task to finish. myExecutingTask.get(); } catch (InterruptedException e) { // Ignore } catch (ExecutionException e) { // Rethrow the original cause of the exception on this thread. throw new RuntimeException(e.getCause()); } myData.freeze(); AndroidDebugBridge.removeClientChangeListener(this); if (myClient != null) { myClient.setHeapInfoUpdateEnabled(false); } myExecutingTask = null; for (MemorySamplerListener listener : myListeners) { listener.onStop(); } } } public boolean isRunning() { return myExecutingTask != null && myRunning; } @Override public void run() { boolean pending = false; long wait = mySampleFrequencyMs; while (myRunning) { try { long now = System.currentTimeMillis(); if (myDataSemaphore.tryAcquire(wait, TimeUnit.MILLISECONDS)) { pending = false; sample(TYPE_DATA, 0); } else { if (pending) { sample(TYPE_TIMEOUT, 0); } Client client = myClient; if (client != null) { client.updateHeapInfo(); } pending = true; } wait -= (System.currentTimeMillis() - now); if (wait <= 0) { wait = mySampleFrequencyMs; } } catch (InterruptedException e) { myRunning = false; } } } @Override public void onSuccess(String remoteFilePath, Client client) { LOG.warn("Unexpected HPROF dump in remote file path."); } @Override public void onSuccess(final byte[] data, final Client client) { ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (myPendingHprofId == 0) { // We are not waiting for any dumps. We ignore it. return; } sample(TYPE_HPROF_RESULT, myPendingHprofId); myPendingHprofId = 0; for (MemorySamplerListener listener : myListeners) { listener.onHprofCompleted(data, client); } } }); } @Override public void onEndFailure(Client client, String message) { LOG.error("Error getting the HPROF dump."); } public boolean canRequestHeapDump() { return myPendingHprofId == 0; } @SuppressWarnings("ConstantConditions") public void requestHeapDump() { if (myClient != null) { ClientData.setHprofDumpHandler(this); myClient.dumpHprof(); myPendingHprofId = getNextHprofId(); sample(TYPE_HPROF_REQUEST, myPendingHprofId); } } public void setClient(@Nullable Client client) { if (client != myClient) { stop(); myClient = client; start(); } } public void addListener(MemorySamplerListener listener) { myListeners.add(listener); } @SuppressWarnings("ConstantConditions") public void requestGc() { if (myClient != null) { myClient.executeGarbageCollector(); } } public interface MemorySamplerListener { void onStart(); void onStop(); void onHprofCompleted(@NotNull byte[] data, @NotNull Client client); } }