blob: e9cad0acfcc6c01489499c772868ce1caa8d6aa5 [file] [log] [blame]
Misha Wagner69236432019-01-07 14:13:24 +00001/*
2 * Copyright (C) 2018 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
17package com.android.internal.os;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNotNull;
21import static org.junit.Assert.assertTrue;
22
Misha Wagner4b32c9f2019-01-25 15:30:14 +000023import android.os.Process;
Misha Wagner69236432019-01-07 14:13:24 +000024import android.os.SystemClock;
Tadashi G. Takaoka7a4047f2019-02-02 09:55:59 +090025
26import androidx.test.ext.junit.runners.AndroidJUnit4;
27import androidx.test.filters.LargeTest;
Misha Wagner69236432019-01-07 14:13:24 +000028
29import com.android.internal.os.KernelCpuThreadReader.ProcessCpuUsage;
30import com.android.internal.os.KernelCpuThreadReader.ThreadCpuUsage;
31
32import org.junit.Test;
33import org.junit.runner.RunWith;
34
35import java.time.Duration;
36import java.util.Arrays;
37import java.util.List;
Misha Wagner3989eb02019-03-19 11:05:37 +000038import java.util.Optional;
Misha Wagner69236432019-01-07 14:13:24 +000039import java.util.OptionalDouble;
40import java.util.concurrent.CountDownLatch;
41import java.util.function.Consumer;
42import java.util.stream.Collectors;
43
44/**
45 * End to end test for {@link KernelCpuThreadReader} that checks the accuracy of the reported times
46 * by spawning threads that do a predictable amount of work
47 */
48@RunWith(AndroidJUnit4.class)
49@LargeTest
50public class KernelCpuThreadReaderEndToEndTest {
51
52 private static final int TIMED_NUM_SAMPLES = 5;
53 private static final int TIMED_START_MILLIS = 500;
54 private static final int TIMED_END_MILLIS = 2000;
55 private static final int TIMED_INCREMENT_MILLIS = 500;
56 private static final int TIMED_COMPARISON_DELTA_MILLIS = 200;
57
58 private static final int ITERATIVE_NUM_SAMPLES = 100;
59 private static final long ITERATIVE_LOW_ITERATIONS = (long) 1e8;
60 private static final long ITERATIVE_HIGH_ITERATIONS = (long) 2e8;
61 private static final double ITERATIONS_COMPARISONS_DELTA = 0.25;
62
63 /**
64 * Test that when we busy-wait for the thread-local time to reach N seconds, the time reported
65 * is also N seconds. Takes ~10s.
66 */
67 @Test
68 public void testTimedWork() throws InterruptedException {
69 for (int millis = TIMED_START_MILLIS;
70 millis <= TIMED_END_MILLIS;
71 millis += TIMED_INCREMENT_MILLIS) {
72 final Duration targetDuration = Duration.ofMillis(millis);
73 final Runnable work = timedWork(targetDuration);
74 Duration resultDuration = getAverageWorkTime(
75 work, String.format("timed%dms", millis), TIMED_NUM_SAMPLES);
76 assertEquals(
77 "Time worked according to currentThreadTimeMillis doesn't match "
78 + "KernelCpuThreadReader",
79 targetDuration.toMillis(), resultDuration.toMillis(),
80 TIMED_COMPARISON_DELTA_MILLIS);
81 }
82 }
83
84 /**
85 * Test that when we scale up the amount of work by N, the time reported also scales by N. Takes
86 * ~15s.
87 */
88 @Test
89 public void testIterativeWork() throws InterruptedException {
90 final Runnable lowAmountWork = iterativeWork(ITERATIVE_LOW_ITERATIONS);
91 final Runnable highAmountWork = iterativeWork(ITERATIVE_HIGH_ITERATIONS);
92 final Duration lowResultDuration =
93 getAverageWorkTime(lowAmountWork, "iterlow", ITERATIVE_NUM_SAMPLES);
94 final Duration highResultDuration =
95 getAverageWorkTime(highAmountWork, "iterhigh", ITERATIVE_NUM_SAMPLES);
96 assertEquals(
97 "Work scale and CPU time scale do not match",
98 ((double) ITERATIVE_HIGH_ITERATIONS) / ((double) ITERATIVE_LOW_ITERATIONS),
99 ((double) highResultDuration.toMillis()) / ((double) lowResultDuration.toMillis()),
100 ITERATIONS_COMPARISONS_DELTA);
101 }
102
103 /**
104 * Run some work {@code numSamples} times, and take the average CPU duration used for that work
105 * according to {@link KernelCpuThreadReader}
106 */
107 private Duration getAverageWorkTime(
108 Runnable work, String tag, int numSamples) throws InterruptedException {
109 // Count down every time a thread finishes work, so that we can wait for work to complete
110 final CountDownLatch workFinishedLatch = new CountDownLatch(numSamples);
111 // Count down once when threads can terminate (after we get them from
112 // `KernelCpuThreadReader`)
113 final CountDownLatch threadFinishedLatch = new CountDownLatch(1);
114
115 // Start `NUM_SAMPLE` threads to do the work
116 for (int i = 0; i < numSamples; i++) {
117 final String threadName = String.format("%s%d", tag, i);
118 // Check the thread name, as we rely on it later to identify threads
119 assertTrue("Max name length for linux threads is 15", threadName.length() <= 15);
120 doWork(work, threadName, workFinishedLatch, threadFinishedLatch);
121 }
122
123 // Wait for threads to finish
124 workFinishedLatch.await();
125
126 // Get thread data from KernelCpuThreadReader
Misha Wagner4b32c9f2019-01-25 15:30:14 +0000127 final KernelCpuThreadReader kernelCpuThreadReader =
Misha Wagner648d2032019-02-11 10:43:27 +0000128 KernelCpuThreadReader.create(8, uid -> uid == Process.myUid(), 0);
Misha Wagner69236432019-01-07 14:13:24 +0000129 assertNotNull(kernelCpuThreadReader);
Misha Wagner3989eb02019-03-19 11:05:37 +0000130 kernelCpuThreadReader.setUidPredicate(uid -> uid == Process.myUid());
131 final Optional<ProcessCpuUsage> currentProcessCpuUsage =
132 kernelCpuThreadReader.getProcessCpuUsage().stream()
133 .filter(p -> p.processId == Process.myPid())
134 .findFirst();
135 assertTrue(currentProcessCpuUsage.isPresent());
Misha Wagner69236432019-01-07 14:13:24 +0000136
137 // Threads can terminate, as we've finished crawling them from /proc
138 threadFinishedLatch.countDown();
139
140 // Check that we've got times for every thread we spawned
Misha Wagner3989eb02019-03-19 11:05:37 +0000141 final List<ThreadCpuUsage> threadCpuUsages =
142 currentProcessCpuUsage.get().threadCpuUsages.stream()
143 .filter((thread) -> thread.threadName.startsWith(tag))
144 .collect(Collectors.toList());
Misha Wagner69236432019-01-07 14:13:24 +0000145 assertEquals(
146 "Incorrect number of threads returned by KernelCpuThreadReader",
147 numSamples, threadCpuUsages.size());
148
149 // Calculate the average time spent working
150 final OptionalDouble averageWorkTimeMillis = threadCpuUsages.stream()
151 .mapToDouble((t) -> Arrays.stream(t.usageTimesMillis).sum())
152 .average();
153 assertTrue(averageWorkTimeMillis.isPresent());
154 return Duration.ofMillis((long) averageWorkTimeMillis.getAsDouble());
155 }
156
157 /**
158 * Work that lasts {@code duration} according to {@link SystemClock#currentThreadTimeMillis()}
159 */
160 private Runnable timedWork(Duration duration) {
161 return () -> {
162 // Busy loop until `duration` has elapsed for the thread timer
163 final long startTimeMillis = SystemClock.currentThreadTimeMillis();
164 final long durationMillis = duration.toMillis();
165 while (true) {
166 final long elapsedMillis = SystemClock.currentThreadTimeMillis() - startTimeMillis;
167 if (elapsedMillis >= durationMillis) {
168 break;
169 }
170 }
171 };
172 }
173
174 /**
175 * Work that iterates {@code iterations} times
176 */
177 private Runnable iterativeWork(long iterations) {
178 Consumer<Long> empty = (i) -> {
179 };
180 return () -> {
181 long count = 0;
182 for (long i = 0; i < iterations; i++) {
183 // Alternate branching to reduce effect of branch prediction
184 if (i % 2 == 0) {
185 count++;
186 }
187 }
188 // Call empty function with value to avoid loop getting optimized away
189 empty.accept(count);
190 };
191 }
192
193 /**
194 * Perform some work in another thread
195 *
196 * @param work the work to perform
197 * @param threadName the name of the spawned thread
198 * @param workFinishedLatch latch to register that the work has been completed
199 * @param threadFinishedLatch latch to pause termination of the thread until the latch is
200 * decremented
201 */
202 private void doWork(
203 Runnable work,
204 String threadName,
205 CountDownLatch workFinishedLatch,
206 CountDownLatch threadFinishedLatch) {
207 Runnable workWrapped = () -> {
208 // Do the work
209 work.run();
210 // Notify that the work is finished
211 workFinishedLatch.countDown();
212 // Wait until `threadFinishLatch` has been released in order to keep the thread alive so
213 // we can see it in `proc` filesystem
214 try {
215 threadFinishedLatch.await();
216 } catch (InterruptedException e) {
217 e.printStackTrace();
218 }
219 };
220 new Thread(workWrapped, threadName).start();
221 }
222}