Use sampling for deviceutilstatsmonitor

Current device utilization mechanism seems flawed, with machines often
going into state where they report questionable metrics due to 'state end
already reported for device' errors.

This commit changes the util calculation to a simpler moving average mechanism.

Bug: 16016815

Change-Id: Ic86972479b79c319c829b78852103b65c71a5d91
diff --git a/src/com/android/tradefed/command/CommandScheduler.java b/src/com/android/tradefed/command/CommandScheduler.java
index 73cdcdc..00ec78a 100644
--- a/src/com/android/tradefed/command/CommandScheduler.java
+++ b/src/com/android/tradefed/command/CommandScheduler.java
@@ -802,19 +802,21 @@
         List<DeviceDescriptor> deviceDescs = getDeviceManager().listAllDevices();
 
         for (DeviceDescriptor deviceDesc : deviceDescs) {
-            String device = deviceDesc.getSerial();
-            String[] argsWithDevice = Arrays.copyOf(args, args.length + 2);
-            argsWithDevice[argsWithDevice.length - 2] = "-s";
-            argsWithDevice[argsWithDevice.length - 1] = device;
-            CommandTracker cmdTracker = createCommandTracker(argsWithDevice, cmdFilePath);
-            cmdTracker.incrementExecTime(totalExecTime);
-            IConfiguration config = getConfigFactory().createConfigurationFromArgs(
-                    cmdTracker.getArgs());
-            CLog.logAndDisplay(LogLevel.INFO, "Scheduling '%s' on '%s'", cmdTracker.getArgs()[0],
-                    device);
-            config.getDeviceRequirements().setSerial(device);
-            ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
-            addExecCommandToQueue(execCmd, 0);
+            if (!deviceDesc.isStubDevice()) {
+                String device = deviceDesc.getSerial();
+                String[] argsWithDevice = Arrays.copyOf(args, args.length + 2);
+                argsWithDevice[argsWithDevice.length - 2] = "-s";
+                argsWithDevice[argsWithDevice.length - 1] = device;
+                CommandTracker cmdTracker = createCommandTracker(argsWithDevice, cmdFilePath);
+                cmdTracker.incrementExecTime(totalExecTime);
+                IConfiguration config = getConfigFactory().createConfigurationFromArgs(
+                        cmdTracker.getArgs());
+                CLog.logAndDisplay(LogLevel.INFO, "Scheduling '%s' on '%s'", cmdTracker.getArgs()[0],
+                        device);
+                config.getDeviceRequirements().setSerial(device);
+                ExecutableCommand execCmd = createExecutableCommand(cmdTracker, config, false);
+                addExecCommandToQueue(execCmd, 0);
+            }
         }
     }
 
diff --git a/src/com/android/tradefed/device/DeviceManager.java b/src/com/android/tradefed/device/DeviceManager.java
index 7899a6c..35c7e32 100644
--- a/src/com/android/tradefed/device/DeviceManager.java
+++ b/src/com/android/tradefed/device/DeviceManager.java
@@ -747,11 +747,6 @@
         IDeviceSelection selector = getDeviceSelectionOptions();
         for (IManagedTestDevice d : mManagedDeviceList) {
             IDevice idevice = d.getIDevice();
-            if (idevice instanceof StubDevice
-                    && d.getAllocationState() != DeviceAllocationState.Allocated) {
-                // don't add placeholder devices
-                continue;
-            }
             serialStates.add(new DeviceDescriptor(idevice.getSerialNumber(),
                     idevice instanceof StubDevice,
                     d.getAllocationState(),
diff --git a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java b/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
index c9f67e8..476f38b 100644
--- a/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
+++ b/src/com/android/tradefed/device/DeviceUtilStatsMonitor.java
@@ -16,26 +16,29 @@
 
 package com.android.tradefed.device;
 
+import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.config.GlobalConfiguration;
 import com.android.tradefed.config.Option;
 import com.android.tradefed.log.LogUtil.CLog;
+import com.android.tradefed.util.CircularByteArray;
 
 import java.util.HashMap;
 import java.util.Hashtable;
-import java.util.LinkedList;
-import java.util.ListIterator;
 import java.util.Map;
-import java.util.Map.Entry;
+import java.util.Timer;
+import java.util.TimerTask;
 
 /**
  * A {@link IDeviceMonitor} that calculates device utilization stats.
  * <p/>
- * Currently measures allocation time % out of allocation + avail time, over a 24 hour window.
- * <p/>
- * If used, {@link #getUtilizationStats()} must be called periodically to clear accumulated memory.
+ * Currently measures simple moving average of allocation time % over a 24 hour window.
  */
 public class DeviceUtilStatsMonitor implements IDeviceMonitor {
 
+    static final int DEFAULT_MAX_SAMPLES = 24 * 60;
+
+    private static final int mInitialDelayMs = 100;
+
     /**
      * Enum for configuring treatment of stub devices when calculating average host utilization
      */
@@ -61,31 +64,6 @@
 
     private boolean mNullDeviceAllocated = false;
     private boolean mEmulatorAllocated = false;
-    /**
-     * A record of the start and end time a device spent in one of the measured states (either
-     * available or allocated).
-     */
-    private class StateRecord {
-        long mStartTime = -1;
-        long mEndTime = -1;
-
-        StateRecord() {
-            mStartTime = mTimeProvider.getCurrentTimeMillis();
-        }
-
-        void setEnd() {
-            mEndTime = mTimeProvider.getCurrentTimeMillis();
-        }
-    }
-
-    /**
-     * Holds the total accounting of time spent in available and allocated state, over the
-     * {@link #WINDOW_MS}
-     */
-    private static class DeviceStateRecords {
-        LinkedList<StateRecord> mAvailableRecords = new LinkedList<>();
-        LinkedList<StateRecord> mAllocatedRecords = new LinkedList<>();
-    }
 
     /**
      * Container for utilization stats.
@@ -122,139 +100,128 @@
         }
     }
 
-    /**
-     * interface for retrieving current time. Used so unit test can mock
-     */
-    static interface ITimeProvider {
-        long getCurrentTimeMillis();
-    }
+    private class DeviceUtilRecord {
+        // store samples of device util, where 0 = avail, 1 = allocated
+        // TODO: save memory by using CircularBitArray
+        private CircularByteArray mData;
+        private int mConsecutiveMissedSamples = 0;
 
-    static class SystemTimeProvider implements ITimeProvider {
-        @Override
-        public long getCurrentTimeMillis() {
-            return System.currentTimeMillis();
+        DeviceUtilRecord() {
+            mData = new CircularByteArray(mMaxSamples);
+        }
+
+        public void addSample(DeviceAllocationState state) {
+            if (DeviceAllocationState.Allocated.equals(state)) {
+                mData.add((byte)1);
+            } else {
+                mData.add((byte)0);
+            }
+            mConsecutiveMissedSamples = 0;
+        }
+
+        public long getNumAllocations() {
+            return mData.getSum();
+        }
+
+        public long getTotalSamples() {
+            return mData.size();
+        }
+
+        /**
+         * Record sample for missing device.
+         *
+         * @param serial device serial number
+         * @return true if sample was added, false if device has been missing for more than max
+         * samples
+         */
+        public boolean addMissingSample(String serial) {
+            mConsecutiveMissedSamples++;
+            if (mConsecutiveMissedSamples > mMaxSamples) {
+                return false;
+            }
+            mData.add((byte)0);
+            return true;
         }
     }
 
-    // use 24 hour window
-    final static long WINDOW_MS = 24L * 60L * 60L * 1000L;
+    private class SamplingTask extends TimerTask {
+        @Override
+        public void run() {
+            CLog.d("Collecting utilization");
+            // track devices that we have records for, but are not reported by device lister
+            Map<String, DeviceUtilRecord> goneDevices = new HashMap<>(mDeviceUtilMap);
 
-    /** a map of device serial to state records */
-    private Map<String, DeviceStateRecords> mDeviceUtilMap = new Hashtable<>();
+            for (DeviceDescriptor deviceDesc : mDeviceLister.listDevices()) {
+                DeviceUtilRecord record = getDeviceRecord(deviceDesc.getSerial());
+                record.addSample(deviceDesc.getState());
+                goneDevices.remove(deviceDesc.getSerial());
+            }
 
-    private final ITimeProvider mTimeProvider;
-    /** stores the startup time of this stats object */
-    private final long mStartTime;
-
-    DeviceUtilStatsMonitor(ITimeProvider p) {
-        mTimeProvider = p;
-        mStartTime = p.getCurrentTimeMillis();
+            // now record samples for gone devices
+            for (Map.Entry<String, DeviceUtilRecord> goneSerialEntry : goneDevices.entrySet()) {
+                String serial = goneSerialEntry.getKey();
+                if (!goneSerialEntry.getValue().addMissingSample(serial)) {
+                    CLog.d("Forgetting device %s", serial);
+                    mDeviceUtilMap.remove(serial);
+                }
+            }
+        }
     }
 
-    public DeviceUtilStatsMonitor() {
-        this(new SystemTimeProvider());
-    }
+    private int mSamplePeriodMs = 60 * 1000;
+
+    // by default, use 24 hour window - calculated by number of measurement interval (1 min) in
+    // this window
+    private int mMaxSamples = DEFAULT_MAX_SAMPLES;
+
+    /** a map of device serial to device records */
+    private Map<String, DeviceUtilRecord> mDeviceUtilMap = new Hashtable<>();
+
+    private DeviceLister mDeviceLister;
+
+    private Timer mTimer;
+    SamplingTask mSamplingTask = new SamplingTask();
 
     /**
      * Get the device utilization up to the last 24 hours
      */
     public synchronized UtilizationDesc getUtilizationStats() {
         CLog.d("Calculating device util");
-        long currentTime = mTimeProvider.getCurrentTimeMillis();
-        cleanAllRecords(currentTime);
 
-        long totalAvailTime = 0;
-        long totalAllocTime = 0;
-        long windowStartTime = currentTime - WINDOW_MS;
-        if (windowStartTime < mStartTime) {
-            // this class has been running less than window time - use start time as start of
-            // window
-            windowStartTime = mStartTime;
-        }
-        Map<String, Integer> deviceUtilMap = new HashMap<>(mDeviceUtilMap.size());
-        Map<String, DeviceStateRecords> mapCopy = new HashMap<>(mDeviceUtilMap);
-        for (Map.Entry<String, DeviceStateRecords> deviceRecordEntry : mapCopy.entrySet()) {
-            if (shouldTrackDevice(deviceRecordEntry)) {
-                long availTime = countTime(windowStartTime, currentTime,
-                        deviceRecordEntry.getValue().mAvailableRecords);
-                long allocTime = countTime(windowStartTime, currentTime,
-                        deviceRecordEntry.getValue().mAllocatedRecords);
-                totalAvailTime += availTime;
-                totalAllocTime += allocTime;
-                deviceUtilMap.put(deviceRecordEntry.getKey(), getUtil(availTime, allocTime));
+        long totalAllocSamples = 0;
+        long totalSamples = 0;
+        Map<String, Integer> deviceUtilMap = new HashMap<>();
+        for (Map.Entry<String, DeviceUtilRecord> deviceRecordEntry : mDeviceUtilMap.entrySet()) {
+            if (shouldTrackDevice(deviceRecordEntry.getKey())) {
+                long allocSamples = deviceRecordEntry.getValue().getNumAllocations();
+                long numSamples = deviceRecordEntry.getValue().getTotalSamples();
+                totalAllocSamples += allocSamples;
+                totalSamples += numSamples;
+                deviceUtilMap.put(deviceRecordEntry.getKey(), getUtil(allocSamples, numSamples));
             }
         }
-        return new UtilizationDesc(getUtil(totalAvailTime, totalAllocTime), deviceUtilMap);
-    }
-
-    /**
-     * Return the total time in ms spent in state in window
-     */
-    private long countTime(long windowStartTime, long currentTime, LinkedList<StateRecord> records) {
-        long totalTime = 0;
-        for (StateRecord r : records) {
-            long startTime = r.mStartTime;
-            // started before window - truncate to window start time
-            if (startTime < windowStartTime) {
-                startTime = windowStartTime;
-            }
-            long endTime = r.mEndTime;
-            // hasn't ended yet - there should only be one of these. Truncate to current time
-            if (endTime < 0) {
-                endTime = currentTime;
-            }
-            if (endTime < startTime) {
-                CLog.w("endtime %d is less than start time %d", endTime, startTime);
-                continue;
-            }
-            totalTime += (endTime - startTime);
-        }
-        return totalTime;
+        return new UtilizationDesc(getUtil(totalAllocSamples, totalSamples), deviceUtilMap);
     }
 
     /**
      * Get device utilization as a percent
      */
-    private int getUtil(long availTime, long allocTime) {
-        long totalTime = availTime + allocTime;
-        if (totalTime <= 0) {
+    private static int getUtil(long allocSamples, long numSamples) {
+        if (numSamples <= 0) {
             return 0;
         }
-        return (int)((allocTime * 100) / totalTime);
-    }
-
-    /**
-     * Remove all old records outside the moving average window (currently 24 hours)
-     */
-    private void cleanAllRecords(long currentTime) {
-        long obsoleteTime = currentTime - WINDOW_MS;
-        for (DeviceStateRecords r : mDeviceUtilMap.values()) {
-            cleanRecordList(obsoleteTime, r.mAllocatedRecords);
-            cleanRecordList(obsoleteTime, r.mAvailableRecords);
-        }
-    }
-
-    private void cleanRecordList(long obsoleteTime, LinkedList<StateRecord> records) {
-        ListIterator<StateRecord> li = records.listIterator();
-        while (li.hasNext()) {
-            StateRecord r = li.next();
-            if (r.mEndTime > 0 && r.mEndTime < obsoleteTime) {
-                li.remove();
-            } else {
-                // since records are sorted, just end now
-                return;
-            }
-        }
+        return (int)((allocSamples * 100) / numSamples);
     }
 
     @Override
     public void run() {
-        // ignore
+        mTimer  = new Timer();
+        mTimer.scheduleAtFixedRate(mSamplingTask, mInitialDelayMs, mSamplePeriodMs);
     }
 
     @Override
     public void setDeviceLister(DeviceLister lister) {
-        // ignore
+        mDeviceLister = lister;
     }
 
     /**
@@ -264,62 +231,34 @@
     @Override
     public synchronized void notifyDeviceStateChange(String serial, DeviceAllocationState oldState,
             DeviceAllocationState newState) {
-        // record the 'state ended' time
-        DeviceStateRecords stateRecord = getDeviceRecords(serial);
-        if (DeviceAllocationState.Available.equals(oldState)) {
-            recordStateEnd(stateRecord.mAvailableRecords);
-        } else if (DeviceAllocationState.Allocated.equals(oldState)) {
-            recordStateEnd(stateRecord.mAllocatedRecords);
+        if (mNullDeviceAllocated && mEmulatorAllocated) {
+            // optimization, don't enter calculation below unless needed
+            return;
         }
-
-        // record the 'state started' time
-        if (DeviceAllocationState.Available.equals(newState)) {
-            recordStateStart(stateRecord.mAvailableRecords);
-        } else if (DeviceAllocationState.Allocated.equals(newState)) {
+        if (DeviceAllocationState.Allocated.equals(newState)) {
             IDeviceManager dvcMgr = getDeviceManager();
             if (dvcMgr.isNullDevice(serial)) {
                 mNullDeviceAllocated = true;
             } else if (dvcMgr.isEmulator(serial)) {
                 mEmulatorAllocated = true;
             }
-            recordStateStart(stateRecord.mAllocatedRecords);
         }
     }
 
-    private void recordStateEnd(LinkedList<StateRecord> records) {
-        if (records.isEmpty()) {
-            CLog.e("error, no records exist");
-            return;
-        }
-        StateRecord r = records.getLast();
-        if (r.mEndTime != -1) {
-            CLog.e("error, last state already marked as ended");
-            return;
-        }
-        r.setEnd();
-    }
-
-    private void recordStateStart(LinkedList<StateRecord> records) {
-        // TODO: do some correctness checks
-        StateRecord r = new StateRecord();
-        records.add(r);
-    }
-
     /**
-     * Get the device state records for given serial, creating if necessary.
+     * Get the device util records for given serial, creating if necessary.
      */
-    private DeviceStateRecords getDeviceRecords(String serial) {
-        DeviceStateRecords r = mDeviceUtilMap.get(serial);
+    private DeviceUtilRecord getDeviceRecord(String serial) {
+        DeviceUtilRecord r = mDeviceUtilMap.get(serial);
         if (r == null) {
-            r = new DeviceStateRecords();
+            r = new DeviceUtilRecord();
             mDeviceUtilMap.put(serial, r);
         }
         return r;
     }
 
-    private boolean shouldTrackDevice(Entry<String, DeviceStateRecords> deviceRecordEntry) {
+    private boolean shouldTrackDevice(String serial) {
         IDeviceManager dvcMgr = getDeviceManager();
-        String serial = deviceRecordEntry.getKey();
         if (dvcMgr.isNullDevice(serial)) {
             switch (mCollectNullDevice) {
                 case ALWAYS_INCLUDE:
@@ -345,4 +284,12 @@
     IDeviceManager getDeviceManager() {
         return GlobalConfiguration.getDeviceManagerInstance();
     }
+
+    TimerTask getSamplingTask() {
+        return mSamplingTask;
+    }
+
+    void setMaxSamples(int maxSamples) {
+        mMaxSamples = maxSamples;
+    }
 }
diff --git a/src/com/android/tradefed/util/CircularByteArray.java b/src/com/android/tradefed/util/CircularByteArray.java
new file mode 100644
index 0000000..599968c
--- /dev/null
+++ b/src/com/android/tradefed/util/CircularByteArray.java
@@ -0,0 +1,68 @@
+/*
+ * 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.tradefed.util;
+
+/**
+ * Data structure for holding a fixed size array that operates as a circular buffer,
+ * and tracks the total sum of all values in the array.
+ */
+public class CircularByteArray {
+
+    private byte[] mArray;
+    private int mCurPos = 0;
+    private boolean mIsWrapped = false;
+    private long mSum = 0;
+
+    public CircularByteArray(int size) {
+        mArray = new byte[size];
+    }
+
+    /**
+     * Adds a new value to array, replacing oldest value if necessary
+     * @param value
+     */
+    public void add(byte value) {
+        if (mIsWrapped) {
+            // pop value and adjust total
+            mSum -= mArray[mCurPos];
+        }
+        mArray[mCurPos] = value;
+        mCurPos++;
+        mSum += value;
+        if (mCurPos >= mArray.length) {
+            mIsWrapped = true;
+            mCurPos = 0;
+        }
+    }
+
+    /**
+     * Get the number of elements stored
+     */
+    public int size() {
+        if (mIsWrapped) {
+            return mArray.length;
+        } else {
+            return mCurPos;
+        }
+    }
+
+    /**
+     * Gets the total value of all elements currently stored in array
+     */
+    public long getSum() {
+        return mSum;
+    }
+}
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
index 275f8ba..0538c91 100644
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorLoadTest.java
@@ -16,12 +16,16 @@
 
 package com.android.tradefed.device;
 
-import com.android.tradefed.device.DeviceUtilStatsMonitor.ITimeProvider;
-import com.android.tradefed.device.DeviceUtilStatsMonitor.UtilizationDesc;
-import com.android.tradefed.util.TimeUtil;
+import com.android.tradefed.command.remote.DeviceDescriptor;
+import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
 
 import junit.framework.TestCase;
 
+import org.easymock.EasyMock;
+
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Load test for {@link DeviceUtilStatsMonitor} Used to ensure memory used by monitor under heavy
  * load is reasonable
@@ -29,52 +33,60 @@
 public class DeviceUtilStatsMonitorLoadTest extends TestCase {
 
     private static final int NUM_DEVICES = 100;
-    private static final long ALLOC_TIME_MS = 60 * 1000;
+
+    private IDeviceManager mMockDeviceManager;
+    private DeviceUtilStatsMonitor mDeviceUtilMonitor;
+
+    @Override
+    public void setUp() {
+        mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
+        mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
+            @Override
+            IDeviceManager getDeviceManager() {
+                return mMockDeviceManager;
+            }
+        };
+        mDeviceUtilMonitor.setDeviceLister(new DeviceLister() {
+            @Override
+            public List<DeviceDescriptor> listDevices() {
+                return mMockDeviceManager.listAllDevices();
+            }
+        });
+    }
 
     /**
-     * Simulate a heavy load by generating constant allocation events of ALLOC_TIME_MS length for
+     * Simulate a heavy load by generating constant allocation events length for
      * all NUM_DEVICES devices.
      * <p/>
      * Intended to be run under a profiler.
+     * @throws InterruptedException
      */
-    public void testManyRecords() {
-        MockTimeProvider timeProvider = new MockTimeProvider();
-        DeviceUtilStatsMonitor monitor = new DeviceUtilStatsMonitor(timeProvider);
-        for (int i = 0; i < NUM_DEVICES; i++) {
-            monitor.notifyDeviceStateChange(Integer.toString(i), DeviceAllocationState.Unknown,
-                    DeviceAllocationState.Available);
+    public void testManyRecords() throws InterruptedException {
+        List<DeviceDescriptor> deviceList = new ArrayList<>(NUM_DEVICES);
+        for (int i =0; i <NUM_DEVICES; i++) {
+            DeviceDescriptor device = createDeviceDesc("serial" + i, DeviceAllocationState.Allocated);
+            deviceList.add(device);
         }
-        while (timeProvider.mCurrentTime < DeviceUtilStatsMonitor.WINDOW_MS) {
-            for (int i = 0; i < NUM_DEVICES; i++) {
-                monitor.notifyDeviceStateChange(Integer.toString(i),
-                        DeviceAllocationState.Available, DeviceAllocationState.Allocated);
-            }
-            timeProvider.incrementTime();
-            for (int i = 0; i < NUM_DEVICES; i++) {
-                monitor.notifyDeviceStateChange(Integer.toString(i),
-                        DeviceAllocationState.Allocated, DeviceAllocationState.Available);
-            }
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andStubReturn(deviceList);
+        EasyMock.replay(mMockDeviceManager);
+
+        for (int i = 0; i < DeviceUtilStatsMonitor.DEFAULT_MAX_SAMPLES; i++) {
+            mDeviceUtilMonitor.getSamplingTask().run();
         }
-        long startTime = System.currentTimeMillis();
-        UtilizationDesc d = monitor.getUtilizationStats();
-        System.out.println(TimeUtil.formatElapsedTime(System.currentTimeMillis() - startTime));
+        // This takes ~ 5.7 MB in heap if DeviceUtilStatsMonitor uses a LinkedList<Byte> to
+        // store samples
+        // takes ~ 270K if CircularByteArray is used
+        Thread.sleep(5 * 60 * 1000);
     }
 
-    private static class MockTimeProvider implements ITimeProvider {
-
-        long mCurrentTime = 0;
-
-        void incrementTime() {
-            mCurrentTime += ALLOC_TIME_MS;
-        }
-
-        @Override
-        public long getCurrentTimeMillis() {
-            return mCurrentTime;
-        }
+    /**
+     * Helper method to create a {@link DeviceDescriptor} using only serial and state.
+     */
+    private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
+        return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
     }
 
     public static void main(String[] args) {
-        new DeviceUtilStatsMonitorLoadTest().testManyRecords();
+        //new DeviceUtilStatsMonitorLoadTest().testManyRecords();
     }
 }
diff --git a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
index 857c2d2..bfd953c 100644
--- a/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
+++ b/tests/src/com/android/tradefed/device/DeviceUtilStatsMonitorTest.java
@@ -15,197 +15,145 @@
  */
 package com.android.tradefed.device;
 
-import com.android.tradefed.config.ArgsOptionParser;
-import com.android.tradefed.config.ConfigurationException;
-import com.android.tradefed.device.DeviceUtilStatsMonitor.ITimeProvider;
+import com.android.tradefed.command.remote.DeviceDescriptor;
 import com.android.tradefed.device.DeviceUtilStatsMonitor.UtilizationDesc;
+import com.android.tradefed.device.IDeviceMonitor.DeviceLister;
 
 import junit.framework.TestCase;
 
 import org.easymock.EasyMock;
 
+import java.util.ArrayList;
+import java.util.List;
+
 /**
  * Simple unit tests for {@link DeviceUtilStatsMonitor}
  */
 public class DeviceUtilStatsMonitorTest extends TestCase {
 
     private IDeviceManager mMockDeviceManager;
-    private ITimeProvider mMockTime;
+    private DeviceUtilStatsMonitor mDeviceUtilMonitor;
 
     @Override
     public void setUp() {
         mMockDeviceManager = EasyMock.createNiceMock(IDeviceManager.class);
-        mMockTime = EasyMock.createNiceMock(ITimeProvider.class);
+        mDeviceUtilMonitor = new DeviceUtilStatsMonitor() {
+            @Override
+            IDeviceManager getDeviceManager() {
+                return mMockDeviceManager;
+            }
+        };
+        mDeviceUtilMonitor.setDeviceLister(new DeviceLister() {
+            @Override
+            public List<DeviceDescriptor> listDevices() {
+                return mMockDeviceManager.listAllDevices();
+            }
+        });
     }
 
     public void testEmpty() {
-        EasyMock.replay(mMockTime, mMockDeviceManager);
-        assertEquals(0, createUtilMonitor().getUtilizationStats().mTotalUtil);
+        EasyMock.replay(mMockDeviceManager);
+        assertEquals(0, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
     }
 
     /**
      * Test case where device has been available but never allocated
      */
     public void testOnlyAvailable() {
-        // use a time of 0 for starttime, and available starttime
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // use time of 10 for current time when getUtil call happens
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(10L);
-        EasyMock.replay(mMockTime, mMockDeviceManager);
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
+                DeviceAllocationState.Available));
+        EasyMock.replay(mMockDeviceManager);
 
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Available);
-
-        UtilizationDesc desc = s.getUtilizationStats();
+        mDeviceUtilMonitor.getSamplingTask().run();
+        UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
         assertEquals(0, desc.mTotalUtil);
         assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(0L, (long)desc.mDeviceUtil.get(serial));
-    }
-
-    private DeviceUtilStatsMonitor createUtilMonitor() {
-        return new DeviceUtilStatsMonitor(mMockTime) {
-            @Override
-            IDeviceManager getDeviceManager() {
-                return mMockDeviceManager;
-            }
-        };
+        assertEquals(0L, (long)desc.mDeviceUtil.get("serial0"));
     }
 
     /**
      * Test case where device has been allocated but never available
      */
     public void testOnlyAllocated() {
-        // use a time of 0 for starttime, and available starttime
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // use time of 10 for current time when getUtil call happens
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(10L);
-        EasyMock.replay(mMockTime, mMockDeviceManager);
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
+                DeviceAllocationState.Allocated));
+        EasyMock.replay(mMockDeviceManager);
 
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Allocated);
-
-        UtilizationDesc desc = s.getUtilizationStats();
+        mDeviceUtilMonitor.getSamplingTask().run();
+        UtilizationDesc desc = mDeviceUtilMonitor.getUtilizationStats();
         assertEquals(100L, desc.mTotalUtil);
         assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(100L, (long)desc.mDeviceUtil.get(serial));
+        assertEquals(100L, (long)desc.mDeviceUtil.get("serial0"));
     }
 
     /**
-     * Test case where device has been allocated for half the time
+     * Test case where samples exceed max
      */
-    public void testHalfAllocated() {
-        // use a time of 0 for starttime, and available starttime
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // use time of 5 for current time when available end, and alloc start happens
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(5L).times(2);
-        // use time of 10 when getUtil time happens
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(10L);
-        EasyMock.replay(mMockTime, mMockDeviceManager);
+    public void testExceededSamples() {
+        mDeviceUtilMonitor.setMaxSamples(2);
+        // first return allocated, then return samples with device missing
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
+                DeviceAllocationState.Allocated));
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(DeviceAllocationState.Available));
+        EasyMock.replay(mMockDeviceManager);
 
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Available);
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Available,
-                DeviceAllocationState.Allocated);
-
-        UtilizationDesc desc = s.getUtilizationStats();
-        assertEquals(50L, desc.mTotalUtil);
-        assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(50L, (long)desc.mDeviceUtil.get(serial));
+        mDeviceUtilMonitor.getSamplingTask().run();
+        // only 1 sample - allocated
+        assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
+        mDeviceUtilMonitor.getSamplingTask().run();
+        // 1 out of 2
+        assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
+        mDeviceUtilMonitor.getSamplingTask().run();
+        // 0 out of 2
+        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
     }
 
     /**
-     * Ensure records from older than window are discarded.
-     * <p/>
-     * Simulate by recording available record entirely before window, and allocation start before
-     * window. therefore expect utilization 100%
+     * Test case where device disappears. Ensure util numbers are calculated until > max samples
+     * have been collected with it missing
      */
-    public void testCleanRecords() {
-        long fakeCurrentTime = DeviceUtilStatsMonitor.WINDOW_MS + 100;
-        // this will be available start and starttime- use time of 0
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // this is available end and alloc start
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(50L).times(2);
-        // for all other calls use current time which is > window
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andStubReturn(fakeCurrentTime);
+    public void testMissingDevice() {
+        mDeviceUtilMonitor.setMaxSamples(2);
+        // first return allocated, then return samples with device missing
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList(
+                DeviceAllocationState.Allocated));
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
+        EasyMock.expect(mMockDeviceManager.listAllDevices()).andReturn(buildDeviceList());
+        EasyMock.replay(mMockDeviceManager);
 
-        EasyMock.replay(mMockTime, mMockDeviceManager);
+        // only 1 sample - allocated
+        mDeviceUtilMonitor.getSamplingTask().run();
+        assertEquals(100L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
 
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Available);
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Available,
-                DeviceAllocationState.Allocated);
+        // 1 out of 2
+        mDeviceUtilMonitor.getSamplingTask().run();
+        assertEquals(50L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
 
-        UtilizationDesc desc = s.getUtilizationStats();
-        assertEquals(100L, desc.mTotalUtil);
-        assertEquals(1, desc.mDeviceUtil.size());
-        assertEquals(100L, (long)desc.mDeviceUtil.get(serial));
+        // 0 out of 2
+        mDeviceUtilMonitor.getSamplingTask().run();
+        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mTotalUtil);
+
+        // now removed
+        mDeviceUtilMonitor.getSamplingTask().run();
+        assertEquals(0L, mDeviceUtilMonitor.getUtilizationStats().mDeviceUtil.size());
+
+    }
+
+    private List<DeviceDescriptor> buildDeviceList(DeviceAllocationState... states) {
+        List<DeviceDescriptor> deviceList = new ArrayList<>(states.length);
+        for (int i =0; i < states.length; i++) {
+            DeviceDescriptor device = createDeviceDesc("serial" + i, states[i]);
+            deviceList.add(device);
+        }
+        return deviceList;
     }
 
     /**
-     * Ensures null device data is dropped when --collect-null-device==IGNORE
-     * @throws ConfigurationException
+     * Helper method to create a {@link DeviceDescriptor} using only serial and state.
      */
-    public void testNullDevice_ignored() throws ConfigurationException {
-        // this will be available start and starttime- use time of 0
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // this is available end and alloc start
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(50L).times(2);
-        // for all other calls use 100
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andStubReturn(100L);
-
-        EasyMock.expect(mMockDeviceManager.isNullDevice(EasyMock.<String>anyObject()))
-                .andStubReturn(Boolean.TRUE);
-        EasyMock.replay(mMockTime, mMockDeviceManager);
-
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        new ArgsOptionParser(s).parse("--collect-null-device", "IGNORE");
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Available);
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Available,
-                DeviceAllocationState.Allocated);
-        UtilizationDesc desc = s.getUtilizationStats();
-        assertEquals(0, desc.mTotalUtil);
-        assertEquals(0, desc.mDeviceUtil.size());
-    }
-
-    /**
-     * Ensures null device data treatment when --collect-null-device==INCLUDE_IF_USED
-     * @throws ConfigurationException
-     */
-    public void testNullDevice_whenUsed() throws ConfigurationException {
-        // this will be available start and starttime- use time of 0
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(0L).times(2);
-        // this is available end and alloc start
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andReturn(50L).times(3);
-        // for all other calls use 100
-        EasyMock.expect(mMockTime.getCurrentTimeMillis()).andStubReturn(100L);
-
-        EasyMock.expect(mMockDeviceManager.isNullDevice(EasyMock.<String>anyObject()))
-                .andStubReturn(Boolean.TRUE);
-        EasyMock.replay(mMockTime, mMockDeviceManager);
-
-        DeviceUtilStatsMonitor s = createUtilMonitor();
-        new ArgsOptionParser(s).parse("--collect-null-device", "INCLUDE_IF_USED");
-        final String serial = "serial";
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Unknown,
-                DeviceAllocationState.Available);
-        UtilizationDesc desc = s.getUtilizationStats();
-        assertEquals(0, desc.mTotalUtil);
-        assertEquals(0, desc.mDeviceUtil.size());
-
-        s.notifyDeviceStateChange(serial, DeviceAllocationState.Available,
-                DeviceAllocationState.Allocated);
-        desc = s.getUtilizationStats();
-        assertEquals(50, desc.mTotalUtil);
-        assertEquals(1, desc.mDeviceUtil.size());
+    private DeviceDescriptor createDeviceDesc(String serial, DeviceAllocationState state) {
+        return new DeviceDescriptor(serial, false, state, null, null, null, null, null);
     }
 }