Loading core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java 0 → 100644 +214 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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.internal.os; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.os.SystemClock; import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; import com.android.internal.os.KernelCpuThreadReader.ProcessCpuUsage; import com.android.internal.os.KernelCpuThreadReader.ThreadCpuUsage; import org.junit.Test; import org.junit.runner.RunWith; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.OptionalDouble; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.stream.Collectors; /** * End to end test for {@link KernelCpuThreadReader} that checks the accuracy of the reported times * by spawning threads that do a predictable amount of work */ @RunWith(AndroidJUnit4.class) @LargeTest public class KernelCpuThreadReaderEndToEndTest { private static final int TIMED_NUM_SAMPLES = 5; private static final int TIMED_START_MILLIS = 500; private static final int TIMED_END_MILLIS = 2000; private static final int TIMED_INCREMENT_MILLIS = 500; private static final int TIMED_COMPARISON_DELTA_MILLIS = 200; private static final int ITERATIVE_NUM_SAMPLES = 100; private static final long ITERATIVE_LOW_ITERATIONS = (long) 1e8; private static final long ITERATIVE_HIGH_ITERATIONS = (long) 2e8; private static final double ITERATIONS_COMPARISONS_DELTA = 0.25; /** * Test that when we busy-wait for the thread-local time to reach N seconds, the time reported * is also N seconds. Takes ~10s. */ @Test public void testTimedWork() throws InterruptedException { for (int millis = TIMED_START_MILLIS; millis <= TIMED_END_MILLIS; millis += TIMED_INCREMENT_MILLIS) { final Duration targetDuration = Duration.ofMillis(millis); final Runnable work = timedWork(targetDuration); Duration resultDuration = getAverageWorkTime( work, String.format("timed%dms", millis), TIMED_NUM_SAMPLES); assertEquals( "Time worked according to currentThreadTimeMillis doesn't match " + "KernelCpuThreadReader", targetDuration.toMillis(), resultDuration.toMillis(), TIMED_COMPARISON_DELTA_MILLIS); } } /** * Test that when we scale up the amount of work by N, the time reported also scales by N. Takes * ~15s. */ @Test public void testIterativeWork() throws InterruptedException { final Runnable lowAmountWork = iterativeWork(ITERATIVE_LOW_ITERATIONS); final Runnable highAmountWork = iterativeWork(ITERATIVE_HIGH_ITERATIONS); final Duration lowResultDuration = getAverageWorkTime(lowAmountWork, "iterlow", ITERATIVE_NUM_SAMPLES); final Duration highResultDuration = getAverageWorkTime(highAmountWork, "iterhigh", ITERATIVE_NUM_SAMPLES); assertEquals( "Work scale and CPU time scale do not match", ((double) ITERATIVE_HIGH_ITERATIONS) / ((double) ITERATIVE_LOW_ITERATIONS), ((double) highResultDuration.toMillis()) / ((double) lowResultDuration.toMillis()), ITERATIONS_COMPARISONS_DELTA); } /** * Run some work {@code numSamples} times, and take the average CPU duration used for that work * according to {@link KernelCpuThreadReader} */ private Duration getAverageWorkTime( Runnable work, String tag, int numSamples) throws InterruptedException { // Count down every time a thread finishes work, so that we can wait for work to complete final CountDownLatch workFinishedLatch = new CountDownLatch(numSamples); // Count down once when threads can terminate (after we get them from // `KernelCpuThreadReader`) final CountDownLatch threadFinishedLatch = new CountDownLatch(1); // Start `NUM_SAMPLE` threads to do the work for (int i = 0; i < numSamples; i++) { final String threadName = String.format("%s%d", tag, i); // Check the thread name, as we rely on it later to identify threads assertTrue("Max name length for linux threads is 15", threadName.length() <= 15); doWork(work, threadName, workFinishedLatch, threadFinishedLatch); } // Wait for threads to finish workFinishedLatch.await(); // Get thread data from KernelCpuThreadReader final KernelCpuThreadReader kernelCpuThreadReader = KernelCpuThreadReader.create(); assertNotNull(kernelCpuThreadReader); final ProcessCpuUsage currentProcessCpuUsage = kernelCpuThreadReader.getCurrentProcessCpuUsage(); // Threads can terminate, as we've finished crawling them from /proc threadFinishedLatch.countDown(); // Check that we've got times for every thread we spawned final List<ThreadCpuUsage> threadCpuUsages = currentProcessCpuUsage.threadCpuUsages .stream() .filter((thread) -> thread.threadName.startsWith(tag)) .collect(Collectors.toList()); assertEquals( "Incorrect number of threads returned by KernelCpuThreadReader", numSamples, threadCpuUsages.size()); // Calculate the average time spent working final OptionalDouble averageWorkTimeMillis = threadCpuUsages.stream() .mapToDouble((t) -> Arrays.stream(t.usageTimesMillis).sum()) .average(); assertTrue(averageWorkTimeMillis.isPresent()); return Duration.ofMillis((long) averageWorkTimeMillis.getAsDouble()); } /** * Work that lasts {@code duration} according to {@link SystemClock#currentThreadTimeMillis()} */ private Runnable timedWork(Duration duration) { return () -> { // Busy loop until `duration` has elapsed for the thread timer final long startTimeMillis = SystemClock.currentThreadTimeMillis(); final long durationMillis = duration.toMillis(); while (true) { final long elapsedMillis = SystemClock.currentThreadTimeMillis() - startTimeMillis; if (elapsedMillis >= durationMillis) { break; } } }; } /** * Work that iterates {@code iterations} times */ private Runnable iterativeWork(long iterations) { Consumer<Long> empty = (i) -> { }; return () -> { long count = 0; for (long i = 0; i < iterations; i++) { // Alternate branching to reduce effect of branch prediction if (i % 2 == 0) { count++; } } // Call empty function with value to avoid loop getting optimized away empty.accept(count); }; } /** * Perform some work in another thread * * @param work the work to perform * @param threadName the name of the spawned thread * @param workFinishedLatch latch to register that the work has been completed * @param threadFinishedLatch latch to pause termination of the thread until the latch is * decremented */ private void doWork( Runnable work, String threadName, CountDownLatch workFinishedLatch, CountDownLatch threadFinishedLatch) { Runnable workWrapped = () -> { // Do the work work.run(); // Notify that the work is finished workFinishedLatch.countDown(); // Wait until `threadFinishLatch` has been released in order to keep the thread alive so // we can see it in `proc` filesystem try { threadFinishedLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }; new Thread(workWrapped, threadName).start(); } } Loading
core/tests/coretests/src/com/android/internal/os/KernelCpuThreadReaderEndToEndTest.java 0 → 100644 +214 −0 Original line number Diff line number Diff line /* * Copyright (C) 2018 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.internal.os; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import android.os.SystemClock; import android.support.test.filters.LargeTest; import android.support.test.runner.AndroidJUnit4; import com.android.internal.os.KernelCpuThreadReader.ProcessCpuUsage; import com.android.internal.os.KernelCpuThreadReader.ThreadCpuUsage; import org.junit.Test; import org.junit.runner.RunWith; import java.time.Duration; import java.util.Arrays; import java.util.List; import java.util.OptionalDouble; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; import java.util.stream.Collectors; /** * End to end test for {@link KernelCpuThreadReader} that checks the accuracy of the reported times * by spawning threads that do a predictable amount of work */ @RunWith(AndroidJUnit4.class) @LargeTest public class KernelCpuThreadReaderEndToEndTest { private static final int TIMED_NUM_SAMPLES = 5; private static final int TIMED_START_MILLIS = 500; private static final int TIMED_END_MILLIS = 2000; private static final int TIMED_INCREMENT_MILLIS = 500; private static final int TIMED_COMPARISON_DELTA_MILLIS = 200; private static final int ITERATIVE_NUM_SAMPLES = 100; private static final long ITERATIVE_LOW_ITERATIONS = (long) 1e8; private static final long ITERATIVE_HIGH_ITERATIONS = (long) 2e8; private static final double ITERATIONS_COMPARISONS_DELTA = 0.25; /** * Test that when we busy-wait for the thread-local time to reach N seconds, the time reported * is also N seconds. Takes ~10s. */ @Test public void testTimedWork() throws InterruptedException { for (int millis = TIMED_START_MILLIS; millis <= TIMED_END_MILLIS; millis += TIMED_INCREMENT_MILLIS) { final Duration targetDuration = Duration.ofMillis(millis); final Runnable work = timedWork(targetDuration); Duration resultDuration = getAverageWorkTime( work, String.format("timed%dms", millis), TIMED_NUM_SAMPLES); assertEquals( "Time worked according to currentThreadTimeMillis doesn't match " + "KernelCpuThreadReader", targetDuration.toMillis(), resultDuration.toMillis(), TIMED_COMPARISON_DELTA_MILLIS); } } /** * Test that when we scale up the amount of work by N, the time reported also scales by N. Takes * ~15s. */ @Test public void testIterativeWork() throws InterruptedException { final Runnable lowAmountWork = iterativeWork(ITERATIVE_LOW_ITERATIONS); final Runnable highAmountWork = iterativeWork(ITERATIVE_HIGH_ITERATIONS); final Duration lowResultDuration = getAverageWorkTime(lowAmountWork, "iterlow", ITERATIVE_NUM_SAMPLES); final Duration highResultDuration = getAverageWorkTime(highAmountWork, "iterhigh", ITERATIVE_NUM_SAMPLES); assertEquals( "Work scale and CPU time scale do not match", ((double) ITERATIVE_HIGH_ITERATIONS) / ((double) ITERATIVE_LOW_ITERATIONS), ((double) highResultDuration.toMillis()) / ((double) lowResultDuration.toMillis()), ITERATIONS_COMPARISONS_DELTA); } /** * Run some work {@code numSamples} times, and take the average CPU duration used for that work * according to {@link KernelCpuThreadReader} */ private Duration getAverageWorkTime( Runnable work, String tag, int numSamples) throws InterruptedException { // Count down every time a thread finishes work, so that we can wait for work to complete final CountDownLatch workFinishedLatch = new CountDownLatch(numSamples); // Count down once when threads can terminate (after we get them from // `KernelCpuThreadReader`) final CountDownLatch threadFinishedLatch = new CountDownLatch(1); // Start `NUM_SAMPLE` threads to do the work for (int i = 0; i < numSamples; i++) { final String threadName = String.format("%s%d", tag, i); // Check the thread name, as we rely on it later to identify threads assertTrue("Max name length for linux threads is 15", threadName.length() <= 15); doWork(work, threadName, workFinishedLatch, threadFinishedLatch); } // Wait for threads to finish workFinishedLatch.await(); // Get thread data from KernelCpuThreadReader final KernelCpuThreadReader kernelCpuThreadReader = KernelCpuThreadReader.create(); assertNotNull(kernelCpuThreadReader); final ProcessCpuUsage currentProcessCpuUsage = kernelCpuThreadReader.getCurrentProcessCpuUsage(); // Threads can terminate, as we've finished crawling them from /proc threadFinishedLatch.countDown(); // Check that we've got times for every thread we spawned final List<ThreadCpuUsage> threadCpuUsages = currentProcessCpuUsage.threadCpuUsages .stream() .filter((thread) -> thread.threadName.startsWith(tag)) .collect(Collectors.toList()); assertEquals( "Incorrect number of threads returned by KernelCpuThreadReader", numSamples, threadCpuUsages.size()); // Calculate the average time spent working final OptionalDouble averageWorkTimeMillis = threadCpuUsages.stream() .mapToDouble((t) -> Arrays.stream(t.usageTimesMillis).sum()) .average(); assertTrue(averageWorkTimeMillis.isPresent()); return Duration.ofMillis((long) averageWorkTimeMillis.getAsDouble()); } /** * Work that lasts {@code duration} according to {@link SystemClock#currentThreadTimeMillis()} */ private Runnable timedWork(Duration duration) { return () -> { // Busy loop until `duration` has elapsed for the thread timer final long startTimeMillis = SystemClock.currentThreadTimeMillis(); final long durationMillis = duration.toMillis(); while (true) { final long elapsedMillis = SystemClock.currentThreadTimeMillis() - startTimeMillis; if (elapsedMillis >= durationMillis) { break; } } }; } /** * Work that iterates {@code iterations} times */ private Runnable iterativeWork(long iterations) { Consumer<Long> empty = (i) -> { }; return () -> { long count = 0; for (long i = 0; i < iterations; i++) { // Alternate branching to reduce effect of branch prediction if (i % 2 == 0) { count++; } } // Call empty function with value to avoid loop getting optimized away empty.accept(count); }; } /** * Perform some work in another thread * * @param work the work to perform * @param threadName the name of the spawned thread * @param workFinishedLatch latch to register that the work has been completed * @param threadFinishedLatch latch to pause termination of the thread until the latch is * decremented */ private void doWork( Runnable work, String threadName, CountDownLatch workFinishedLatch, CountDownLatch threadFinishedLatch) { Runnable workWrapped = () -> { // Do the work work.run(); // Notify that the work is finished workFinishedLatch.countDown(); // Wait until `threadFinishLatch` has been released in order to keep the thread alive so // we can see it in `proc` filesystem try { threadFinishedLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }; new Thread(workWrapped, threadName).start(); } }