diff options
author | Suren Baghdasaryan | 2018-01-22 18:16:06 -0600 |
---|---|---|
committer | Suren Baghdasaryan | 2018-03-09 13:19:24 -0600 |
commit | e708b6fc687962941e89aaea69d81a7d1244e32c (patch) | |
tree | a604ca2659f29773d8443cf6e423846f6b40a3cb /lmkd | |
parent | a92de71a51f675b054e052b4b6a444db4a22d037 (diff) | |
download | platform-system-core-e708b6fc687962941e89aaea69d81a7d1244e32c.tar.gz platform-system-core-e708b6fc687962941e89aaea69d81a7d1244e32c.tar.xz platform-system-core-e708b6fc687962941e89aaea69d81a7d1244e32c.zip |
lmkd: Implement lmkd-test
(cherry pick from commit 5f5b30e58ba8964f78231ebf96e4ec80e7a31ecd)
lmkd-test executes alloc-stress native application and monitors kernel
logs for OOM kills.
Bug: 63631020
Change-Id: Ia9441cc5f8e283131f0bc839cb808a911bcdb168
Merged-In: Ia9441cc5f8e283131f0bc839cb808a911bcdb168
Signed-off-by: Suren Baghdasaryan <surenb@google.com>
Diffstat (limited to 'lmkd')
-rw-r--r-- | lmkd/tests/Android.bp | 40 | ||||
-rw-r--r-- | lmkd/tests/lmkd_test.cpp | 373 |
2 files changed, 413 insertions, 0 deletions
diff --git a/lmkd/tests/Android.bp b/lmkd/tests/Android.bp new file mode 100644 index 000000000..cbf44e9fc --- /dev/null +++ b/lmkd/tests/Android.bp | |||
@@ -0,0 +1,40 @@ | |||
1 | // Copyright (C) 2018 The Android Open Source Project | ||
2 | // | ||
3 | // Licensed under the Apache License, Version 2.0 (the "License"); | ||
4 | // you may not use this file except in compliance with the License. | ||
5 | // You may obtain a copy of the License at | ||
6 | // | ||
7 | // http://www.apache.org/licenses/LICENSE-2.0 | ||
8 | // | ||
9 | // Unless required by applicable law or agreed to in writing, software | ||
10 | // distributed under the License is distributed on an "AS IS" BASIS, | ||
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
12 | // See the License for the specific language governing permissions and | ||
13 | // limitations under the License. | ||
14 | |||
15 | cc_test { | ||
16 | name: "lmkd_unit_test", | ||
17 | |||
18 | shared_libs: [ | ||
19 | "libbase", | ||
20 | "liblog", | ||
21 | ], | ||
22 | |||
23 | static_libs: [ | ||
24 | "liblmkd_utils", | ||
25 | ], | ||
26 | |||
27 | target: { | ||
28 | android: { | ||
29 | srcs: ["lmkd_test.cpp"], | ||
30 | }, | ||
31 | }, | ||
32 | |||
33 | cflags: [ | ||
34 | "-Wall", | ||
35 | "-Wextra", | ||
36 | "-Werror", | ||
37 | ], | ||
38 | |||
39 | compile_multilib: "first", | ||
40 | } | ||
diff --git a/lmkd/tests/lmkd_test.cpp b/lmkd/tests/lmkd_test.cpp new file mode 100644 index 000000000..4afaeb81f --- /dev/null +++ b/lmkd/tests/lmkd_test.cpp | |||
@@ -0,0 +1,373 @@ | |||
1 | /* | ||
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 | |||
17 | #include <sstream> | ||
18 | #include <stdio.h> | ||
19 | #include <string.h> | ||
20 | #include <string> | ||
21 | #include <sys/mman.h> | ||
22 | #include <sys/types.h> | ||
23 | #include <unistd.h> | ||
24 | |||
25 | #include <android-base/file.h> | ||
26 | #include <android-base/logging.h> | ||
27 | #include <android-base/properties.h> | ||
28 | #include <android-base/stringprintf.h> | ||
29 | #include <android-base/strings.h> | ||
30 | #include <gtest/gtest.h> | ||
31 | #include <lmkd.h> | ||
32 | #include <liblmkd_utils.h> | ||
33 | #include <log/log_properties.h> | ||
34 | #include <private/android_filesystem_config.h> | ||
35 | |||
36 | using namespace android::base; | ||
37 | |||
38 | #define INKERNEL_MINFREE_PATH "/sys/module/lowmemorykiller/parameters/minfree" | ||
39 | #define LMKDTEST_RESPAWN_FLAG "LMKDTEST_RESPAWN" | ||
40 | |||
41 | #define LMKD_LOGCAT_MARKER "lowmemorykiller" | ||
42 | #define LMKD_KILL_MARKER_TEMPLATE LMKD_LOGCAT_MARKER ": Killing '%s'" | ||
43 | #define OOM_MARKER "Out of memory" | ||
44 | #define OOM_KILL_MARKER "Killed process" | ||
45 | #define MIN_LOG_SIZE 100 | ||
46 | |||
47 | #define ONE_MB (1 << 20) | ||
48 | |||
49 | /* Test constant parameters */ | ||
50 | #define OOM_ADJ_MAX 1000 | ||
51 | #define OOM_ADJ_MIN 0 | ||
52 | #define OOM_ADJ_STEP 100 | ||
53 | #define STEP_COUNT ((OOM_ADJ_MAX - OOM_ADJ_MIN) / OOM_ADJ_STEP + 1) | ||
54 | |||
55 | #define ALLOC_STEP (ONE_MB) | ||
56 | #define ALLOC_DELAY 1000 | ||
57 | |||
58 | /* Utility functions */ | ||
59 | std::string readCommand(const std::string& command) { | ||
60 | FILE* fp = popen(command.c_str(), "r"); | ||
61 | std::string content; | ||
62 | ReadFdToString(fileno(fp), &content); | ||
63 | pclose(fp); | ||
64 | return content; | ||
65 | } | ||
66 | |||
67 | std::string readLogcat(const std::string& marker) { | ||
68 | std::string content = readCommand("logcat -d -b all"); | ||
69 | size_t pos = content.find(marker); | ||
70 | if (pos == std::string::npos) return ""; | ||
71 | content.erase(0, pos); | ||
72 | return content; | ||
73 | } | ||
74 | |||
75 | bool writeFile(const std::string& file, const std::string& string) { | ||
76 | if (getuid() == static_cast<unsigned>(AID_ROOT)) { | ||
77 | return WriteStringToFile(string, file); | ||
78 | } | ||
79 | return string == readCommand( | ||
80 | "echo -n '" + string + "' | su root tee " + file + " 2>&1"); | ||
81 | } | ||
82 | |||
83 | bool writeKmsg(const std::string& marker) { | ||
84 | return writeFile("/dev/kmsg", marker); | ||
85 | } | ||
86 | |||
87 | std::string getTextAround(const std::string& text, size_t pos, | ||
88 | size_t lines_before, size_t lines_after) { | ||
89 | size_t start_pos = pos; | ||
90 | |||
91 | // find start position | ||
92 | // move up lines_before number of lines | ||
93 | while (lines_before > 0 && | ||
94 | (start_pos = text.rfind('\n', start_pos)) != std::string::npos) { | ||
95 | lines_before--; | ||
96 | } | ||
97 | // move to the beginning of the line | ||
98 | start_pos = text.rfind('\n', start_pos); | ||
99 | start_pos = (start_pos == std::string::npos) ? 0 : start_pos + 1; | ||
100 | |||
101 | // find end position | ||
102 | // move down lines_after number of lines | ||
103 | while (lines_after > 0 && | ||
104 | (pos = text.find('\n', pos)) != std::string::npos) { | ||
105 | pos++; | ||
106 | lines_after--; | ||
107 | } | ||
108 | return text.substr(start_pos, (pos == std::string::npos) ? | ||
109 | std::string::npos : pos - start_pos); | ||
110 | } | ||
111 | |||
112 | bool getExecPath(std::string &path) { | ||
113 | char buf[PATH_MAX + 1]; | ||
114 | int ret = readlink("/proc/self/exe", buf, sizeof(buf) - 1); | ||
115 | if (ret < 0) { | ||
116 | return false; | ||
117 | } | ||
118 | buf[ret] = '\0'; | ||
119 | path = buf; | ||
120 | return true; | ||
121 | } | ||
122 | |||
123 | /* Child synchronization primitives */ | ||
124 | #define STATE_INIT 0 | ||
125 | #define STATE_CHILD_READY 1 | ||
126 | #define STATE_PARENT_READY 2 | ||
127 | |||
128 | struct state_sync { | ||
129 | pthread_mutex_t mutex; | ||
130 | pthread_cond_t condition; | ||
131 | int state; | ||
132 | }; | ||
133 | |||
134 | struct state_sync * init_state_sync_obj() { | ||
135 | struct state_sync *ssync; | ||
136 | |||
137 | ssync = (struct state_sync*)mmap(NULL, sizeof(struct state_sync), | ||
138 | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); | ||
139 | if (ssync == MAP_FAILED) { | ||
140 | return NULL; | ||
141 | } | ||
142 | |||
143 | pthread_mutexattr_t mattr; | ||
144 | pthread_mutexattr_init(&mattr); | ||
145 | pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_SHARED); | ||
146 | pthread_mutex_init(&ssync->mutex, &mattr); | ||
147 | |||
148 | pthread_condattr_t cattr; | ||
149 | pthread_condattr_init(&cattr); | ||
150 | pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED); | ||
151 | pthread_cond_init(&ssync->condition, &cattr); | ||
152 | |||
153 | ssync->state = STATE_INIT; | ||
154 | return ssync; | ||
155 | } | ||
156 | |||
157 | void destroy_state_sync_obj(struct state_sync *ssync) { | ||
158 | pthread_cond_destroy(&ssync->condition); | ||
159 | pthread_mutex_destroy(&ssync->mutex); | ||
160 | munmap(ssync, sizeof(struct state_sync)); | ||
161 | } | ||
162 | |||
163 | void signal_state(struct state_sync *ssync, int state) { | ||
164 | pthread_mutex_lock(&ssync->mutex); | ||
165 | ssync->state = state; | ||
166 | pthread_cond_signal(&ssync->condition); | ||
167 | pthread_mutex_unlock(&ssync->mutex); | ||
168 | } | ||
169 | |||
170 | void wait_for_state(struct state_sync *ssync, int state) { | ||
171 | pthread_mutex_lock(&ssync->mutex); | ||
172 | while (ssync->state != state) { | ||
173 | pthread_cond_wait(&ssync->condition, &ssync->mutex); | ||
174 | } | ||
175 | pthread_mutex_unlock(&ssync->mutex); | ||
176 | } | ||
177 | |||
178 | /* Memory allocation and data sharing */ | ||
179 | struct shared_data { | ||
180 | size_t allocated; | ||
181 | bool finished; | ||
182 | size_t total_size; | ||
183 | size_t step_size; | ||
184 | size_t step_delay; | ||
185 | int oomadj; | ||
186 | }; | ||
187 | |||
188 | volatile void *gptr; | ||
189 | void add_pressure(struct shared_data *data) { | ||
190 | volatile void *ptr; | ||
191 | size_t allocated_size = 0; | ||
192 | |||
193 | data->finished = false; | ||
194 | while (allocated_size < data->total_size) { | ||
195 | ptr = mmap(NULL, data->step_size, PROT_READ | PROT_WRITE, | ||
196 | MAP_ANONYMOUS | MAP_PRIVATE, 0, 0); | ||
197 | if (ptr != MAP_FAILED) { | ||
198 | /* create ptr aliasing to prevent compiler optimizing the access */ | ||
199 | gptr = ptr; | ||
200 | /* make data non-zero */ | ||
201 | memset((void*)ptr, (int)(allocated_size + 1), data->step_size); | ||
202 | allocated_size += data->step_size; | ||
203 | data->allocated = allocated_size; | ||
204 | } | ||
205 | usleep(data->step_delay); | ||
206 | } | ||
207 | data->finished = (allocated_size >= data->total_size); | ||
208 | } | ||
209 | |||
210 | /* Memory stress test main body */ | ||
211 | void runMemStressTest() { | ||
212 | struct shared_data *data; | ||
213 | struct state_sync *ssync; | ||
214 | int sock; | ||
215 | pid_t pid; | ||
216 | uid_t uid = getuid(); | ||
217 | |||
218 | ASSERT_FALSE((sock = lmkd_connect()) < 0) | ||
219 | << "Failed to connect to lmkd process, err=" << strerror(errno); | ||
220 | |||
221 | /* allocate shared memory to communicate params with a child */ | ||
222 | data = (struct shared_data*)mmap(NULL, sizeof(struct shared_data), | ||
223 | PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); | ||
224 | ASSERT_FALSE(data == MAP_FAILED) << "Memory allocation failure"; | ||
225 | data->total_size = (size_t)-1; /* allocate until killed */ | ||
226 | data->step_size = ALLOC_STEP; | ||
227 | data->step_delay = ALLOC_DELAY; | ||
228 | |||
229 | /* allocate state sync object */ | ||
230 | ASSERT_FALSE((ssync = init_state_sync_obj()) == NULL) | ||
231 | << "Memory allocation failure"; | ||
232 | |||
233 | /* run the test gradually decreasing oomadj */ | ||
234 | data->oomadj = OOM_ADJ_MAX; | ||
235 | while (data->oomadj >= OOM_ADJ_MIN) { | ||
236 | ASSERT_FALSE((pid = fork()) < 0) | ||
237 | << "Failed to spawn a child process, err=" << strerror(errno); | ||
238 | if (pid != 0) { | ||
239 | /* Parent */ | ||
240 | struct lmk_procprio params; | ||
241 | /* wait for child to start and get ready */ | ||
242 | wait_for_state(ssync, STATE_CHILD_READY); | ||
243 | params.pid = pid; | ||
244 | params.uid = uid; | ||
245 | params.oomadj = data->oomadj; | ||
246 | ASSERT_FALSE(lmkd_register_proc(sock, ¶ms) < 0) | ||
247 | << "Failed to communicate with lmkd, err=" << strerror(errno); | ||
248 | // signal the child it can proceed | ||
249 | signal_state(ssync, STATE_PARENT_READY); | ||
250 | waitpid(pid, NULL, 0); | ||
251 | if (data->finished) { | ||
252 | GTEST_LOG_(INFO) << "Child [pid=" << pid << "] allocated " | ||
253 | << data->allocated / ONE_MB << "MB"; | ||
254 | } else { | ||
255 | GTEST_LOG_(INFO) << "Child [pid=" << pid << "] allocated " | ||
256 | << data->allocated / ONE_MB | ||
257 | << "MB before being killed"; | ||
258 | } | ||
259 | data->oomadj -= OOM_ADJ_STEP; | ||
260 | } else { | ||
261 | /* Child */ | ||
262 | pid = getpid(); | ||
263 | GTEST_LOG_(INFO) << "Child [pid=" << pid | ||
264 | << "] is running at oomadj=" | ||
265 | << data->oomadj; | ||
266 | data->allocated = 0; | ||
267 | data->finished = false; | ||
268 | ASSERT_FALSE(create_memcg(uid, pid) != 0) | ||
269 | << "Child [pid=" << pid << "] failed to create a cgroup"; | ||
270 | signal_state(ssync, STATE_CHILD_READY); | ||
271 | wait_for_state(ssync, STATE_PARENT_READY); | ||
272 | add_pressure(data); | ||
273 | /* should not reach here, child should be killed by OOM/LMK */ | ||
274 | FAIL() << "Child [pid=" << pid << "] was not killed"; | ||
275 | break; | ||
276 | } | ||
277 | } | ||
278 | destroy_state_sync_obj(ssync); | ||
279 | munmap(data, sizeof(struct shared_data)); | ||
280 | close(sock); | ||
281 | } | ||
282 | |||
283 | TEST(lmkd, check_for_oom) { | ||
284 | // test requirements | ||
285 | // userdebug build | ||
286 | if (!__android_log_is_debuggable()) { | ||
287 | GTEST_LOG_(INFO) << "Must be userdebug build, terminating test"; | ||
288 | return; | ||
289 | } | ||
290 | // check if in-kernel LMK driver is present | ||
291 | if (!access(INKERNEL_MINFREE_PATH, W_OK)) { | ||
292 | GTEST_LOG_(INFO) << "Must not have kernel lowmemorykiller driver," | ||
293 | << " terminating test"; | ||
294 | return; | ||
295 | } | ||
296 | |||
297 | // if respawned test process then run the test and exit (no analysis) | ||
298 | if (getenv(LMKDTEST_RESPAWN_FLAG) != NULL) { | ||
299 | runMemStressTest(); | ||
300 | return; | ||
301 | } | ||
302 | |||
303 | // Main test process | ||
304 | // mark the beginning of the test | ||
305 | std::string marker = StringPrintf( | ||
306 | "LMKD test start %lu\n", static_cast<unsigned long>(time(nullptr))); | ||
307 | ASSERT_TRUE(writeKmsg(marker)); | ||
308 | |||
309 | // get executable complete path | ||
310 | std::string test_path; | ||
311 | ASSERT_TRUE(getExecPath(test_path)); | ||
312 | |||
313 | std::string test_output; | ||
314 | if (getuid() != static_cast<unsigned>(AID_ROOT)) { | ||
315 | // if not root respawn itself as root and capture output | ||
316 | std::string command = StringPrintf( | ||
317 | "%s=true su root %s 2>&1", LMKDTEST_RESPAWN_FLAG, | ||
318 | test_path.c_str()); | ||
319 | std::string test_output = readCommand(command); | ||
320 | GTEST_LOG_(INFO) << test_output; | ||
321 | } else { | ||
322 | // main test process is root, run the test | ||
323 | runMemStressTest(); | ||
324 | } | ||
325 | |||
326 | // Analyze results | ||
327 | // capture logcat containind kernel logs | ||
328 | std::string logcat_out = readLogcat(marker); | ||
329 | |||
330 | // 1. extract LMKD kills from logcat output, count kills | ||
331 | std::stringstream kill_logs; | ||
332 | int hit_count = 0; | ||
333 | size_t pos = 0; | ||
334 | marker = StringPrintf(LMKD_KILL_MARKER_TEMPLATE, test_path.c_str()); | ||
335 | |||
336 | while (true) { | ||
337 | if ((pos = logcat_out.find(marker, pos)) != std::string::npos) { | ||
338 | kill_logs << getTextAround(logcat_out, pos, 0, 1); | ||
339 | pos += marker.length(); | ||
340 | hit_count++; | ||
341 | } else { | ||
342 | break; | ||
343 | } | ||
344 | } | ||
345 | GTEST_LOG_(INFO) << "====Logged kills====" << std::endl | ||
346 | << kill_logs.str(); | ||
347 | EXPECT_TRUE(hit_count == STEP_COUNT) << "Number of kills " << hit_count | ||
348 | << " is less than expected " | ||
349 | << STEP_COUNT; | ||
350 | |||
351 | // 2. check kernel logs for OOM kills | ||
352 | pos = logcat_out.find(OOM_MARKER); | ||
353 | bool oom_detected = (pos != std::string::npos); | ||
354 | bool oom_kill_detected = (oom_detected && | ||
355 | logcat_out.find(OOM_KILL_MARKER, pos) != std::string::npos); | ||
356 | |||
357 | EXPECT_FALSE(oom_kill_detected) << "OOM kill is detected!"; | ||
358 | if (oom_detected || oom_kill_detected) { | ||
359 | // capture logcat with logs around all OOMs | ||
360 | pos = 0; | ||
361 | while ((pos = logcat_out.find(OOM_MARKER, pos)) != std::string::npos) { | ||
362 | GTEST_LOG_(INFO) << "====Logs around OOM====" << std::endl | ||
363 | << getTextAround(logcat_out, pos, | ||
364 | MIN_LOG_SIZE / 2, MIN_LOG_SIZE / 2); | ||
365 | pos += strlen(OOM_MARKER); | ||
366 | } | ||
367 | } | ||
368 | |||
369 | // output complete logcat with kernel (might get truncated) | ||
370 | GTEST_LOG_(INFO) << "====Complete logcat output====" << std::endl | ||
371 | << logcat_out; | ||
372 | } | ||
373 | |||