1 /*
2 * Copyright 2022 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 #undef LOG_TAG
18 #define LOG_TAG "GpuWork"
19 #define ATRACE_TAG ATRACE_TAG_GRAPHICS
20
21 #include "gpuwork/GpuWork.h"
22
23 #include <android-base/stringprintf.h>
24 #include <binder/PermissionCache.h>
25 #include <bpf/WaitForProgsLoaded.h>
26 #include <libbpf.h>
27 #include <log/log.h>
28 #include <random>
29 #include <stats_event.h>
30 #include <statslog.h>
31 #include <unistd.h>
32 #include <utils/Timers.h>
33 #include <utils/Trace.h>
34
35 #include <bit>
36 #include <chrono>
37 #include <cstdint>
38 #include <limits>
39 #include <map>
40 #include <mutex>
41 #include <unordered_map>
42 #include <unordered_set>
43 #include <vector>
44
45 #include "gpuwork/gpuWork.h"
46
47 #define ONE_MS_IN_NS (10000000)
48
49 namespace android {
50 namespace gpuwork {
51
52 namespace {
53
lessThanGpuIdUid(const android::gpuwork::GpuIdUid & l,const android::gpuwork::GpuIdUid & r)54 bool lessThanGpuIdUid(const android::gpuwork::GpuIdUid& l, const android::gpuwork::GpuIdUid& r) {
55 return std::tie(l.gpu_id, l.uid) < std::tie(r.gpu_id, r.uid);
56 }
57
hashGpuIdUid(const android::gpuwork::GpuIdUid & gpuIdUid)58 size_t hashGpuIdUid(const android::gpuwork::GpuIdUid& gpuIdUid) {
59 return static_cast<size_t>((gpuIdUid.gpu_id << 5U) + gpuIdUid.uid);
60 }
61
equalGpuIdUid(const android::gpuwork::GpuIdUid & l,const android::gpuwork::GpuIdUid & r)62 bool equalGpuIdUid(const android::gpuwork::GpuIdUid& l, const android::gpuwork::GpuIdUid& r) {
63 return std::tie(l.gpu_id, l.uid) == std::tie(r.gpu_id, r.uid);
64 }
65
66 // Gets a BPF map from |mapPath|.
67 template <class Key, class Value>
getBpfMap(const char * mapPath,bpf::BpfMap<Key,Value> * out)68 bool getBpfMap(const char* mapPath, bpf::BpfMap<Key, Value>* out) {
69 errno = 0;
70 auto map = bpf::BpfMap<Key, Value>(mapPath);
71 if (!map.isValid()) {
72 ALOGW("Failed to create bpf map from %s [%d(%s)]", mapPath, errno, strerror(errno));
73 return false;
74 }
75 *out = std::move(map);
76 return true;
77 }
78
79 template <typename SourceType>
80 inline int32_t cast_int32(SourceType) = delete;
81
82 template <typename SourceType>
83 inline int32_t bitcast_int32(SourceType) = delete;
84
85 template <>
bitcast_int32(uint32_t source)86 inline int32_t bitcast_int32<uint32_t>(uint32_t source) {
87 int32_t result;
88 memcpy(&result, &source, sizeof(result));
89 return result;
90 }
91
92 } // namespace
93
94 using base::StringAppendF;
95
~GpuWork()96 GpuWork::~GpuWork() {
97 // If we created our clearer thread, then we must stop it and join it.
98 if (mMapClearerThread.joinable()) {
99 // Tell the thread to terminate.
100 {
101 std::scoped_lock<std::mutex> lock(mMutex);
102 mIsTerminating = true;
103 mIsTerminatingConditionVariable.notify_all();
104 }
105
106 // Now, we can join it.
107 mMapClearerThread.join();
108 }
109
110 {
111 std::scoped_lock<std::mutex> lock(mMutex);
112 if (mStatsdRegistered) {
113 AStatsManager_clearPullAtomCallback(android::util::GPU_WORK_PER_UID);
114 }
115 }
116
117 bpf_detach_tracepoint("power", "gpu_work_period");
118 }
119
initialize()120 void GpuWork::initialize() {
121 // Make sure BPF programs are loaded.
122 bpf::waitForProgsLoaded();
123
124 waitForPermissions();
125
126 // Get the BPF maps before trying to attach the BPF program; if we can't get
127 // the maps then there is no point in attaching the BPF program.
128 {
129 std::lock_guard<std::mutex> lock(mMutex);
130
131 if (!getBpfMap("/sys/fs/bpf/map_gpuWork_gpu_work_map", &mGpuWorkMap)) {
132 return;
133 }
134
135 if (!getBpfMap("/sys/fs/bpf/map_gpuWork_gpu_work_global_data", &mGpuWorkGlobalDataMap)) {
136 return;
137 }
138
139 mPreviousMapClearTimePoint = std::chrono::steady_clock::now();
140 }
141
142 // Attach the tracepoint.
143 if (!attachTracepoint("/sys/fs/bpf/prog_gpuWork_tracepoint_power_gpu_work_period", "power",
144 "gpu_work_period")) {
145 return;
146 }
147
148 // Create the map clearer thread, and store it to |mMapClearerThread|.
149 std::thread thread([this]() { periodicallyClearMap(); });
150
151 mMapClearerThread.swap(thread);
152
153 {
154 std::lock_guard<std::mutex> lock(mMutex);
155 AStatsManager_setPullAtomCallback(int32_t{android::util::GPU_WORK_PER_UID}, nullptr,
156 GpuWork::pullAtomCallback, this);
157 mStatsdRegistered = true;
158 }
159
160 ALOGI("Initialized!");
161
162 mInitialized.store(true);
163 }
164
dump(const Vector<String16> &,std::string * result)165 void GpuWork::dump(const Vector<String16>& /* args */, std::string* result) {
166 if (!mInitialized.load()) {
167 result->append("GPU work information is not available.\n");
168 return;
169 }
170
171 // Ordered map ensures output data is sorted.
172 std::map<GpuIdUid, UidTrackingInfo, decltype(lessThanGpuIdUid)*> dumpMap(&lessThanGpuIdUid);
173
174 {
175 std::lock_guard<std::mutex> lock(mMutex);
176
177 if (!mGpuWorkMap.isValid()) {
178 result->append("GPU work map is not available.\n");
179 return;
180 }
181
182 // Iteration of BPF hash maps can be unreliable (no data races, but elements
183 // may be repeated), as the map is typically being modified by other
184 // threads. The buckets are all preallocated. Our eBPF program only updates
185 // entries (in-place) or adds entries. |GpuWork| only iterates or clears the
186 // map while holding |mMutex|. Given this, we should be able to iterate over
187 // all elements reliably. Nevertheless, we copy into a map to avoid
188 // duplicates.
189
190 // Note that userspace reads of BPF maps make a copy of the value, and
191 // thus the returned value is not being concurrently accessed by the BPF
192 // program (no atomic reads needed below).
193
194 mGpuWorkMap.iterateWithValue(
195 [&dumpMap](const GpuIdUid& key, const UidTrackingInfo& value,
196 const android::bpf::BpfMap<GpuIdUid, UidTrackingInfo>&)
197 -> base::Result<void> {
198 dumpMap[key] = value;
199 return {};
200 });
201 }
202
203 // Dump work information.
204 // E.g.
205 // GPU work information.
206 // gpu_id uid total_active_duration_ns total_inactive_duration_ns
207 // 0 1000 0 0
208 // 0 1003 1234 123
209 // [errors:3]0 1006 4567 456
210
211 // Header.
212 result->append("GPU work information.\ngpu_id uid total_active_duration_ns "
213 "total_inactive_duration_ns\n");
214
215 for (const auto& idToUidInfo : dumpMap) {
216 if (idToUidInfo.second.error_count) {
217 StringAppendF(result, "[errors:%" PRIu32 "]", idToUidInfo.second.error_count);
218 }
219 StringAppendF(result, "%" PRIu32 " %" PRIu32 " %" PRIu64 " %" PRIu64 "\n",
220 idToUidInfo.first.gpu_id, idToUidInfo.first.uid,
221 idToUidInfo.second.total_active_duration_ns,
222 idToUidInfo.second.total_inactive_duration_ns);
223 }
224 }
225
attachTracepoint(const char * programPath,const char * tracepointGroup,const char * tracepointName)226 bool GpuWork::attachTracepoint(const char* programPath, const char* tracepointGroup,
227 const char* tracepointName) {
228 errno = 0;
229 base::unique_fd fd(bpf::retrieveProgram(programPath));
230 if (fd < 0) {
231 ALOGW("Failed to retrieve pinned program from %s [%d(%s)]", programPath, errno,
232 strerror(errno));
233 return false;
234 }
235
236 // Attach the program to the tracepoint. The tracepoint is automatically enabled.
237 errno = 0;
238 int count = 0;
239 while (bpf_attach_tracepoint(fd.get(), tracepointGroup, tracepointName) < 0) {
240 if (++count > kGpuWaitTimeoutSeconds) {
241 ALOGW("Failed to attach bpf program to %s/%s tracepoint [%d(%s)]", tracepointGroup,
242 tracepointName, errno, strerror(errno));
243 return false;
244 }
245 // Retry until GPU driver loaded or timeout.
246 if (mStop.load()) return false;
247 sleep(1);
248 errno = 0;
249 }
250
251 return true;
252 }
253
pullAtomCallback(int32_t atomTag,AStatsEventList * data,void * cookie)254 AStatsManager_PullAtomCallbackReturn GpuWork::pullAtomCallback(int32_t atomTag,
255 AStatsEventList* data,
256 void* cookie) {
257 ATRACE_CALL();
258
259 GpuWork* gpuWork = reinterpret_cast<GpuWork*>(cookie);
260 if (atomTag == android::util::GPU_WORK_PER_UID) {
261 return gpuWork->pullWorkAtoms(data);
262 }
263
264 return AStatsManager_PULL_SKIP;
265 }
266
pullWorkAtoms(AStatsEventList * data)267 AStatsManager_PullAtomCallbackReturn GpuWork::pullWorkAtoms(AStatsEventList* data) {
268 ATRACE_CALL();
269
270 if (!data || !mInitialized.load()) {
271 return AStatsManager_PULL_SKIP;
272 }
273
274 std::lock_guard<std::mutex> lock(mMutex);
275
276 if (!mGpuWorkMap.isValid()) {
277 return AStatsManager_PULL_SKIP;
278 }
279
280 std::unordered_map<GpuIdUid, UidTrackingInfo, decltype(hashGpuIdUid)*, decltype(equalGpuIdUid)*>
281 workMap(32, &hashGpuIdUid, &equalGpuIdUid);
282
283 // Iteration of BPF hash maps can be unreliable (no data races, but elements
284 // may be repeated), as the map is typically being modified by other
285 // threads. The buckets are all preallocated. Our eBPF program only updates
286 // entries (in-place) or adds entries. |GpuWork| only iterates or clears the
287 // map while holding |mMutex|. Given this, we should be able to iterate over
288 // all elements reliably. Nevertheless, we copy into a map to avoid
289 // duplicates.
290
291 // Note that userspace reads of BPF maps make a copy of the value, and thus
292 // the returned value is not being concurrently accessed by the BPF program
293 // (no atomic reads needed below).
294
295 mGpuWorkMap.iterateWithValue([&workMap](const GpuIdUid& key, const UidTrackingInfo& value,
296 const android::bpf::BpfMap<GpuIdUid, UidTrackingInfo>&)
297 -> base::Result<void> {
298 workMap[key] = value;
299 return {};
300 });
301
302 // Get a list of just the UIDs; the order does not matter.
303 std::vector<Uid> uids;
304 // Get a list of the GPU IDs, in order.
305 std::set<uint32_t> gpuIds;
306 {
307 // To avoid adding duplicate UIDs.
308 std::unordered_set<Uid> addedUids;
309
310 for (const auto& workInfo : workMap) {
311 if (addedUids.insert(workInfo.first.uid).second) {
312 // Insertion was successful.
313 uids.push_back(workInfo.first.uid);
314 }
315 gpuIds.insert(workInfo.first.gpu_id);
316 }
317 }
318
319 ALOGI("pullWorkAtoms: uids.size() == %zu", uids.size());
320 ALOGI("pullWorkAtoms: gpuIds.size() == %zu", gpuIds.size());
321
322 if (gpuIds.size() > kNumGpusHardLimit) {
323 // If we observe a very high number of GPUs then something has probably
324 // gone wrong, so don't log any atoms.
325 return AStatsManager_PULL_SKIP;
326 }
327
328 size_t numSampledUids = kNumSampledUids;
329
330 if (gpuIds.size() > kNumGpusSoftLimit) {
331 // If we observe a high number of GPUs then we just sample 1 UID.
332 numSampledUids = 1;
333 }
334
335 // Remove all UIDs that do not have at least |kMinGpuTimeNanoseconds| on at
336 // least one GPU.
337 {
338 auto uidIt = uids.begin();
339 while (uidIt != uids.end()) {
340 bool hasEnoughGpuTime = false;
341 for (uint32_t gpuId : gpuIds) {
342 auto infoIt = workMap.find(GpuIdUid{gpuId, *uidIt});
343 if (infoIt == workMap.end()) {
344 continue;
345 }
346 if (infoIt->second.total_active_duration_ns +
347 infoIt->second.total_inactive_duration_ns >=
348 kMinGpuTimeNanoseconds) {
349 hasEnoughGpuTime = true;
350 break;
351 }
352 }
353 if (hasEnoughGpuTime) {
354 ++uidIt;
355 } else {
356 uidIt = uids.erase(uidIt);
357 }
358 }
359 }
360
361 ALOGI("pullWorkAtoms: after removing uids with very low GPU time: uids.size() == %zu",
362 uids.size());
363
364 std::random_device device;
365 std::default_random_engine random_engine(device());
366
367 // If we have more than |numSampledUids| UIDs, choose |numSampledUids|
368 // random UIDs. We swap them to the front of the list. Given the list
369 // indices 0..i..n-1, we have the following inclusive-inclusive ranges:
370 // - [0, i-1] == the randomly chosen elements.
371 // - [i, n-1] == the remaining unchosen elements.
372 if (uids.size() > numSampledUids) {
373 for (size_t i = 0; i < numSampledUids; ++i) {
374 std::uniform_int_distribution<size_t> uniform_dist(i, uids.size() - 1);
375 size_t random_index = uniform_dist(random_engine);
376 std::swap(uids[i], uids[random_index]);
377 }
378 // Only keep the front |numSampledUids| elements.
379 uids.resize(numSampledUids);
380 }
381
382 ALOGI("pullWorkAtoms: after random selection: uids.size() == %zu", uids.size());
383
384 auto now = std::chrono::steady_clock::now();
385 long long duration =
386 std::chrono::duration_cast<std::chrono::seconds>(now - mPreviousMapClearTimePoint)
387 .count();
388 if (duration > std::numeric_limits<int32_t>::max() || duration < 0) {
389 // This is essentially impossible. If it does somehow happen, give up,
390 // but still clear the map.
391 clearMap();
392 return AStatsManager_PULL_SKIP;
393 }
394
395 // Log an atom for each (gpu id, uid) pair for which we have data.
396 for (uint32_t gpuId : gpuIds) {
397 for (Uid uid : uids) {
398 auto it = workMap.find(GpuIdUid{gpuId, uid});
399 if (it == workMap.end()) {
400 continue;
401 }
402 const UidTrackingInfo& info = it->second;
403
404 uint64_t total_active_duration_ms = info.total_active_duration_ns / ONE_MS_IN_NS;
405 uint64_t total_inactive_duration_ms = info.total_inactive_duration_ns / ONE_MS_IN_NS;
406
407 // Skip this atom if any numbers are out of range. |duration| is
408 // already checked above.
409 if (total_active_duration_ms > std::numeric_limits<int32_t>::max() ||
410 total_inactive_duration_ms > std::numeric_limits<int32_t>::max()) {
411 continue;
412 }
413
414 ALOGI("pullWorkAtoms: adding stats for GPU ID %" PRIu32 "; UID %" PRIu32, gpuId, uid);
415 android::util::addAStatsEvent(data, int32_t{android::util::GPU_WORK_PER_UID},
416 // uid
417 bitcast_int32(uid),
418 // gpu_id
419 bitcast_int32(gpuId),
420 // time_duration_seconds
421 static_cast<int32_t>(duration),
422 // total_active_duration_millis
423 static_cast<int32_t>(total_active_duration_ms),
424 // total_inactive_duration_millis
425 static_cast<int32_t>(total_inactive_duration_ms));
426 }
427 }
428 clearMap();
429 return AStatsManager_PULL_SUCCESS;
430 }
431
periodicallyClearMap()432 void GpuWork::periodicallyClearMap() {
433 std::unique_lock<std::mutex> lock(mMutex);
434
435 auto previousTime = std::chrono::steady_clock::now();
436
437 while (true) {
438 if (mIsTerminating) {
439 break;
440 }
441 auto nextTime = std::chrono::steady_clock::now();
442 auto differenceSeconds =
443 std::chrono::duration_cast<std::chrono::seconds>(nextTime - previousTime);
444 if (differenceSeconds.count() > kMapClearerWaitDurationSeconds) {
445 // It has been >1 hour, so clear the map, if needed.
446 clearMapIfNeeded();
447 // We only update |previousTime| if we actually checked the map.
448 previousTime = nextTime;
449 }
450 // Sleep for ~1 hour. It does not matter if we don't check the map for 2
451 // hours.
452 mIsTerminatingConditionVariable.wait_for(lock,
453 std::chrono::seconds{
454 kMapClearerWaitDurationSeconds});
455 }
456 }
457
clearMapIfNeeded()458 void GpuWork::clearMapIfNeeded() {
459 if (!mInitialized.load() || !mGpuWorkMap.isValid() || !mGpuWorkGlobalDataMap.isValid()) {
460 ALOGW("Map clearing could not occur because we are not initialized properly");
461 return;
462 }
463
464 base::Result<GlobalData> globalData = mGpuWorkGlobalDataMap.readValue(0);
465 if (!globalData.ok()) {
466 ALOGW("Could not read BPF global data map entry");
467 return;
468 }
469
470 // Note that userspace reads of BPF maps make a copy of the value, and thus
471 // the return value is not being concurrently accessed by the BPF program
472 // (no atomic reads needed below).
473
474 uint64_t numEntries = globalData.value().num_map_entries;
475
476 // If the map is <=75% full, we do nothing.
477 if (numEntries <= (kMaxTrackedGpuIdUids / 4) * 3) {
478 return;
479 }
480
481 clearMap();
482 }
483
clearMap()484 void GpuWork::clearMap() {
485 if (!mInitialized.load() || !mGpuWorkMap.isValid() || !mGpuWorkGlobalDataMap.isValid()) {
486 ALOGW("Map clearing could not occur because we are not initialized properly");
487 return;
488 }
489
490 base::Result<GlobalData> globalData = mGpuWorkGlobalDataMap.readValue(0);
491 if (!globalData.ok()) {
492 ALOGW("Could not read BPF global data map entry");
493 return;
494 }
495
496 // Iterating BPF maps to delete keys is tricky. If we just repeatedly call
497 // |getFirstKey()| and delete that, we may loop forever (or for a long time)
498 // because our BPF program might be repeatedly re-adding keys. Also, even if
499 // we limit the number of elements we try to delete, we might only delete
500 // new entries, leaving old entries in the map. If we delete a key A and
501 // then call |getNextKey(A)|, the first key in the map is returned, so we
502 // have the same issue.
503 //
504 // Thus, we instead get the next key and then delete the previous key. We
505 // also limit the number of deletions we try, just in case.
506
507 base::Result<GpuIdUid> key = mGpuWorkMap.getFirstKey();
508
509 for (size_t i = 0; i < kMaxTrackedGpuIdUids; ++i) {
510 if (!key.ok()) {
511 break;
512 }
513 base::Result<GpuIdUid> previousKey = key;
514 key = mGpuWorkMap.getNextKey(previousKey.value());
515 mGpuWorkMap.deleteValue(previousKey.value());
516 }
517
518 // Reset our counter; |globalData| is a copy of the data, so we have to use
519 // |writeValue|.
520 globalData.value().num_map_entries = 0;
521 mGpuWorkGlobalDataMap.writeValue(0, globalData.value(), BPF_ANY);
522
523 // Update |mPreviousMapClearTimePoint| so we know when we started collecting
524 // the stats.
525 mPreviousMapClearTimePoint = std::chrono::steady_clock::now();
526 }
527
waitForPermissions()528 void GpuWork::waitForPermissions() {
529 const String16 permissionRegisterStatsPullAtom(kPermissionRegisterStatsPullAtom);
530 int count = 0;
531 while (!PermissionCache::checkPermission(permissionRegisterStatsPullAtom, getpid(), getuid())) {
532 if (++count > kPermissionsWaitTimeoutSeconds) {
533 ALOGW("Timed out waiting for android.permission.REGISTER_STATS_PULL_ATOM");
534 return;
535 }
536 // Retry.
537 sleep(1);
538 }
539 }
540
541 } // namespace gpuwork
542 } // namespace android
543