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