• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright (C) 2020 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 #include "perfetto/perfetto_consumer.h"
16 
17 #include "common/trace.h"
18 
19 #include <android-base/logging.h>
20 #include <android-base/properties.h>
21 #include <utils/Looper.h>
22 #include <utils/Printer.h>
23 
24 #include <limits>
25 #include <map>
26 #include <memory>
27 #include <mutex>
28 #include <sstream>
29 #include <vector>
30 
31 #include <inttypes.h>
32 #include <time.h>
33 
34 namespace iorap::perfetto {
35 
36 using State = PerfettoConsumer::State;
37 using Handle = PerfettoConsumer::Handle;
38 static constexpr Handle kInvalidHandle = PerfettoConsumer::kInvalidHandle;
39 using OnStateChangedCb = PerfettoConsumer::OnStateChangedCb;
40 using TraceBuffer = PerfettoConsumer::TraceBuffer;
41 
42 enum class StateKind {
43   kUncreated,
44   kCreated,
45   kStartedTracing,
46   kReadTracing,
47   kTimedOutDestroyed,  // same as kDestroyed but timed out.
48   kDestroyed,          // calling kDestroyed before timing out.
49 };
50 
operator <<(std::ostream & os,StateKind kind)51 std::ostream& operator<<(std::ostream& os, StateKind kind) {
52   switch (kind) {
53     case StateKind::kUncreated:
54       os << "kUncreated";
55       break;
56     case StateKind::kCreated:
57       os << "kCreated";
58       break;
59     case StateKind::kStartedTracing:
60       os << "kStartedTracing";
61       break;
62     case StateKind::kReadTracing:
63       os << "kReadTracing";
64       break;
65     case StateKind::kTimedOutDestroyed:
66       os << "kTimedOutDestroyed";
67       break;
68     case StateKind::kDestroyed:
69       os << "kDestroyed";
70       break;
71     default:
72       os << "(invalid)";
73       break;
74   }
75   return os;
76 }
77 
ToString(StateKind kind)78 std::string ToString(StateKind kind) {
79   std::stringstream ss;
80   ss << kind;
81   return ss.str();
82 }
83 
84 static constexpr uint64_t kSecToNano = 1000000000LL;
85 
GetTimeNanoseconds()86 static uint64_t GetTimeNanoseconds() {
87   struct timespec now;
88   clock_gettime(CLOCK_REALTIME, &now);
89 
90   uint64_t now_ns = (now.tv_sec * kSecToNano + now.tv_nsec);
91   return now_ns;
92 }
93 
94 // Describe the state of our handle in detail for debugging/logging.
95 struct HandleDescription {
96   Handle handle_;
97   StateKind kind_{StateKind::kUncreated};  // Our state. required for correctness.
98   OnStateChangedCb callback_{nullptr};  // Required for Destroy callbacks.
99   void* callback_arg_{nullptr};
100 
101   // For dumping to logs:
102   State state_{State::kSessionNotFound};  // perfetto state
103   std::optional<uint64_t> started_tracing_ns_; // when StartedTracing last called.
104   std::optional<uint64_t> read_trace_ns_;  // When ReadTrace last called.
105   std::uint64_t last_transition_ns_{0};
106   std::optional<uint64_t> trace_cookie_;  // atrace beginning at StartTracing.
107   bool trace_ended_{false};               // atrace ending at ReadTrace or Destroy.
108 
HandleDescriptioniorap::perfetto::HandleDescription109   HandleDescription(Handle handle): handle_(handle) {}
110 };
111 
112 // pimpl idiom to hide the implementation details from header
113 //
114 // Track and verify that our perfetto usage is sane.
115 struct PerfettoConsumerImpl::Impl {
Impliorap::perfetto::PerfettoConsumerImpl::Impl116   Impl() : raw_{new PerfettoConsumerRawImpl{}},
117       message_handler_{new TraceMessageHandler{this}} {
__anonec49963b0102() 118     std::thread watchdog_thread{ [this]() {
119       ::android::sp<::android::Looper> looper;
120       {
121         std::lock_guard<std::mutex> guard{looper_mutex_};
122         looper = ::android::Looper::prepare(/*opts*/0);
123         looper_ = looper;
124       }
125 
126       static constexpr int kTimeoutMillis = std::numeric_limits<int>::max();
127 
128       while (true) {
129         // Execute any pending callbacks, otherwise just block forever.
130         int result = looper->pollAll(kTimeoutMillis);
131 
132         if (result == ::android::Looper::POLL_ERROR) {
133           LOG(ERROR) << "PerfettoConsumerImpl::Looper got a POLL_ERROR";
134         } else {
135           LOG(DEBUG) << "PerfettoConsumerImpl::Looper result was " << result;
136         }
137       }
138     }};
139 
140     // Let thread run freely on its own.
141     watchdog_thread.detach();
142 
143     // Block until looper_ is prepared.
144     while (true) {
145       std::lock_guard<std::mutex> guard{looper_mutex_};
146       if (looper_ != nullptr) {
147         break;
148       }
149     }
150   }
151 
152  private:
153   std::unique_ptr<PerfettoConsumerRawImpl> raw_;
154   std::map<Handle, HandleDescription> states_;
155 
156   // We need this to be a counter to avoid memory leaks.
157   Handle last_created_{0};
158   Handle last_destroyed_{0};
159   uint64_t trace_cookie_{0};
160 
161   std::mutex mutex_;  // Guard above values.
162 
163   ::android::sp<::android::Looper> looper_;
164   std::mutex looper_mutex_;  // Guard looper_.
165 
166   struct TraceMessageHandler : public ::android::MessageHandler {
TraceMessageHandleriorap::perfetto::PerfettoConsumerImpl::Impl::TraceMessageHandler167     TraceMessageHandler(Impl* impl) : impl_{impl} {
168       CHECK(impl != nullptr);
169     }
170 
171     Impl* impl_;
172 
handleMessageiorap::perfetto::PerfettoConsumerImpl::Impl::TraceMessageHandler173     virtual void handleMessage(const ::android::Message& message) override {
174       impl_->OnTraceMessage(static_cast<Handle>(message.what));
175     }
176   };
177 
178   ::android::sp<TraceMessageHandler> message_handler_;
179 
180  public:
Createiorap::perfetto::PerfettoConsumerImpl::Impl181   Handle Create(const void* config_proto,
182                 size_t config_len,
183                 OnStateChangedCb callback,
184                 void* callback_arg) {
185     LOG(VERBOSE) << "PerfettoConsumer::Create("
186                  << "config_len=" << config_len << ")";
187     Handle handle = raw_->Create(config_proto, config_len, callback, callback_arg);
188 
189     std::lock_guard<std::mutex> guard{mutex_};
190 
191     // Assume every Handle starts at 0 and then increments by 1 every Create.
192     ++last_created_;
193     CHECK_EQ(last_created_, handle) << "perfetto handle had unexpected behavior.";
194     // Without this^ increment-by-1 behavior our detection of untracked state values is broken.
195     // If we have to, we can go with Untracked=Uncreated|Destroyed but it's better to distinguish
196     // the two if possible.
197 
198     HandleDescription handle_desc{handle};
199     handle_desc.handle_ = handle;
200     handle_desc.callback_ = callback;
201     handle_desc.callback_arg_ = callback_arg;
202     UpdateHandleDescription(/*inout*/&handle_desc, StateKind::kCreated);
203 
204     // assume we never wrap around due to using int64
205     bool inserted = states_.insert({handle, handle_desc}).second;
206     CHECK(inserted) << "perfetto handle was re-used: " << handle;
207 
208     return handle;
209   }
210 
StartTracingiorap::perfetto::PerfettoConsumerImpl::Impl211   void StartTracing(Handle handle) {
212     LOG(DEBUG) << "PerfettoConsumer::StartTracing(handle=" << handle << ")";
213 
214     uint64_t trace_cookie;
215     {
216       std::lock_guard<std::mutex> guard{mutex_};
217 
218       auto it = states_.find(handle);
219       if (it == states_.end()) {
220         LOG(ERROR) << "Cannot StartTracing(" << handle << "), untracked handle";
221         return;
222       }
223       HandleDescription& handle_desc = it->second;
224 
225       raw_->StartTracing(handle);
226       UpdateHandleDescription(/*inout*/&handle_desc, StateKind::kStartedTracing);
227     }
228 
229     // Use a looper here to add a timeout and immediately destroy the trace buffer.
230     CHECK_LE(static_cast<int64_t>(handle), static_cast<int64_t>(std::numeric_limits<int>::max()));
231     int message_code = static_cast<int>(handle);
232     ::android::Message message{message_code};
233 
234     std::lock_guard<std::mutex> looper_guard{looper_mutex_};
235     looper_->sendMessageDelayed(static_cast<nsecs_t>(GetPropertyTraceTimeoutNs()),
236                                 message_handler_,
237                                 message);
238   }
239 
ReadTraceiorap::perfetto::PerfettoConsumerImpl::Impl240   TraceBuffer ReadTrace(Handle handle) {
241     LOG(DEBUG) << "PerfettoConsumer::ReadTrace(handle=" << handle << ")";
242 
243     std::lock_guard<std::mutex> guard{mutex_};
244 
245     auto it = states_.find(handle);
246     if (it == states_.end()) {
247       LOG(ERROR) << "Cannot ReadTrace(" << handle << "), untracked handle";
248       return TraceBuffer{};
249     }
250 
251     HandleDescription& handle_desc = it->second;
252 
253     TraceBuffer trace_buffer = raw_->ReadTrace(handle);
254     UpdateHandleDescription(/*inout*/&handle_desc, StateKind::kReadTracing);
255 
256     return trace_buffer;
257   }
258 
Destroyiorap::perfetto::PerfettoConsumerImpl::Impl259   void Destroy(Handle handle) {
260     HandleDescription handle_desc{handle};
261     TryDestroy(handle, /*do_destroy*/true, /*out*/&handle_desc);;
262   }
263 
TryDestroyiorap::perfetto::PerfettoConsumerImpl::Impl264   bool TryDestroy(Handle handle, bool do_destroy, /*out*/HandleDescription* handle_desc_out) {
265     CHECK(handle_desc_out != nullptr);
266 
267     LOG(VERBOSE) << "PerfettoConsumer::Destroy(handle=" << handle << ")";
268 
269     std::lock_guard<std::mutex> guard{mutex_};
270 
271     auto it = states_.find(handle);
272     if (it == states_.end()) {
273       // Leniency for calling Destroy multiple times. It's not a mistake.
274       LOG(ERROR) << "Cannot Destroy(" << handle << "), untracked handle";
275       return false;
276     }
277 
278     HandleDescription& handle_desc = it->second;
279 
280     if (do_destroy) {
281       raw_->Destroy(handle);
282     }
283     UpdateHandleDescription(/*inout*/&handle_desc, StateKind::kDestroyed);
284 
285     *handle_desc_out = handle_desc;
286 
287     // No longer track this handle to avoid memory leaks.
288     last_destroyed_ = handle;
289     states_.erase(it);
290 
291     return true;
292   }
293 
PollStateiorap::perfetto::PerfettoConsumerImpl::Impl294   State PollState(Handle handle) {
295     // Just pass-through the call, we never use it directly anyway.
296     return raw_->PollState(handle);
297   }
298 
299   // Either fetch or infer the current handle state from a handle.
300   // Meant for debugging/logging only.
GetOrInferHandleDescriptioniorap::perfetto::PerfettoConsumerImpl::Impl301   HandleDescription GetOrInferHandleDescription(Handle handle) {
302     std::lock_guard<std::mutex> guard{mutex_};
303 
304     auto it = states_.find(handle);
305     if (it == states_.end()) {
306       HandleDescription state{handle};
307       // If it's untracked it hasn't been created yet, or it was already destroyed.
308       if (IsDestroyed(handle)) {
309         UpdateHandleDescription(/*inout*/&state, StateKind::kDestroyed);
310       } else {
311         if (!IsUncreated(handle)) {
312           LOG(WARNING) << "bad state detection";
313         }
314         UpdateHandleDescription(/*inout*/&state, StateKind::kUncreated);
315       }
316       return state;
317     }
318     return it->second;
319   }
320 
OnTraceMessageiorap::perfetto::PerfettoConsumerImpl::Impl321   void OnTraceMessage(Handle handle) {
322     LOG(VERBOSE) << "OnTraceMessage(" << static_cast<int64_t>(handle) << ")";
323     HandleDescription handle_desc{handle};
324     {
325       std::lock_guard<std::mutex> guard{mutex_};
326 
327       auto it = states_.find(handle);
328       if (it == states_.end()) {
329         // Handle values are never re-used, so we can simply ignore the message here
330         // instead of having to remove it from the message queue.
331         LOG(VERBOSE) << "OnTraceMessage(" << static_cast<int64_t>(handle)
332                      << ") no longer tracked handle";
333         return;
334       }
335       handle_desc = it->second;
336     }
337 
338     // First check. Has this trace been active for too long?
339     uint64_t now_ns = GetTimeNanoseconds();
340     if (handle_desc.kind_ == StateKind::kStartedTracing) {
341       // Ignore other kinds of traces because they don't exhaust perfetto resources.
342       CHECK(handle_desc.started_tracing_ns_.has_value()) << static_cast<int64_t>(handle);
343 
344       uint64_t started_tracing_ns = *handle_desc.started_tracing_ns_;
345 
346       if ((now_ns - started_tracing_ns) > GetPropertyTraceTimeoutNs()) {
347         LOG(WARNING) << "Perfetto Handle timed out after " << (now_ns - started_tracing_ns) << "ns"
348                      << ", forcibly destroying";
349 
350         // Let the callback handler call Destroy.
351         handle_desc.callback_(handle, State::kTraceFailed, handle_desc.callback_arg_);
352       }
353     }
354 
355     // Second check. Are there too many traces now? Cull the old traces.
356     std::vector<HandleDescription> handle_list;
357     do {
358       std::lock_guard<std::mutex> guard{mutex_};
359 
360       size_t max_trace_count = GetPropertyMaxTraceCount();
361       if (states_.size() > max_trace_count) {
362         size_t overflow_count = states_.size() - max_trace_count;
363         LOG(WARNING) << "Too many perfetto handles, overflowed by " << overflow_count
364                      << ", pruning down to " << max_trace_count;
365       } else {
366         break;
367       }
368 
369       size_t prune_count = states_.size() - max_trace_count;
370       auto it = states_.begin();
371       for (size_t i = 0; i < prune_count; ++i) {
372         // Simply prune by handle 1,2,3,4...
373         // We could do better with a timestamp if we wanted to.
374         ++it;
375         handle_list.push_back(it->second);
376       }
377     } while (false);
378 
379     for (HandleDescription& handle_desc : handle_list) {
380       LOG(DEBUG) << "Perfetto handle pruned: " << static_cast<int64_t>(handle);
381 
382       // Let the callback handler call Destroy.
383       handle_desc.callback_(handle, State::kTraceFailed, handle_desc.callback_arg_);
384     }
385   }
386 
387  private:
GetPropertyTraceTimeoutNsiorap::perfetto::PerfettoConsumerImpl::Impl388   static uint64_t GetPropertyTraceTimeoutNs() {
389     static uint64_t value =                       // property is timeout in seconds
390         ::android::base::GetUintProperty<uint64_t>("iorapd.perfetto.timeout", /*default*/10);
391     return value * kSecToNano;
392   }
393 
GetPropertyMaxTraceCountiorap::perfetto::PerfettoConsumerImpl::Impl394   static size_t GetPropertyMaxTraceCount() {
395     static size_t value =
396         ::android::base::GetUintProperty<size_t>("iorapd.perfetto.max_traces", /*default*/5);
397     return value;
398   }
399 
UpdateHandleDescriptioniorap::perfetto::PerfettoConsumerImpl::Impl400   void UpdateHandleDescription(/*inout*/HandleDescription* handle_desc, StateKind kind) {
401     CHECK(handle_desc != nullptr);
402     handle_desc->kind_ = kind;
403     handle_desc->state_ = raw_->PollState(handle_desc->handle_);
404 
405     handle_desc->last_transition_ns_ = GetTimeNanoseconds();
406     if (kind == StateKind::kStartedTracing) {
407       if (!handle_desc->started_tracing_ns_) {
408         handle_desc->started_tracing_ns_ = handle_desc->last_transition_ns_;
409 
410         handle_desc->trace_cookie_ = ++trace_cookie_;
411 
412         atrace_async_begin(ATRACE_TAG_ACTIVITY_MANAGER,
413                            "Perfetto Scoped Trace",
414                            *handle_desc->trace_cookie_);
415         atrace_int(ATRACE_TAG_ACTIVITY_MANAGER,
416                    "Perfetto::Trace Handle",
417                    static_cast<int32_t>(handle_desc->handle_));
418       }
419     }
420 
421     if (kind == StateKind::kReadTracing) {
422       if (!handle_desc->read_trace_ns_) {
423         handle_desc->read_trace_ns_ = handle_desc->last_transition_ns_;
424 
425         if (handle_desc->trace_cookie_.has_value() && !handle_desc->trace_ended_) {
426           atrace_async_end(ATRACE_TAG_ACTIVITY_MANAGER,
427                            "Perfetto Scoped Trace",
428                            handle_desc->trace_cookie_.value());
429 
430           handle_desc->trace_ended_ = true;
431         }
432       }
433     }
434 
435     // If Destroy is called prior to ReadTrace, mark the atrace as finished.
436     if (kind == StateKind::kDestroyed && handle_desc->trace_cookie_ && !handle_desc->trace_ended_) {
437       atrace_async_end(ATRACE_TAG_ACTIVITY_MANAGER,
438                        "Perfetto Scoped Trace",
439                        *handle_desc->trace_cookie_);
440       handle_desc->trace_ended_ = true;
441     }
442   }
443 
444   // The following state detection is for debugging only.
445   // We figure out if something is destroyed, uncreated, or live.
446 
447   // Does not distinguish between kTimedOutDestroyed and kDestroyed.
IsDestroyediorap::perfetto::PerfettoConsumerImpl::Impl448   bool IsDestroyed(Handle handle) const {
449     auto it = states_.find(handle);
450     if (it != states_.end()) {
451       // Tracked values are not destroyed yet.
452       return false;
453     }
454 
455     if (handle == kInvalidHandle) {
456       return false;
457     }
458 
459     // The following assumes handles are incrementally generated:
460     if (it == states_.end()) {
461       // value is in range of [0, last_destroyed]  => destroyed.
462       return handle <= last_destroyed_;
463     }
464 
465     auto min_it = states_.begin();
466     if (handle < min_it->first) {
467       // value smaller than anything tracked: it was destroyed and we stopped tracking it.
468       return true;
469     }
470 
471     auto max_it = states_.rbegin();
472     if (handle > max_it->first) {
473       // value too big: it's uncreated;
474       return false;
475     }
476 
477     // else it was a value that was previously tracked within [min,max] but no longer
478     return true;
479   }
480 
IsUncreatediorap::perfetto::PerfettoConsumerImpl::Impl481   bool IsUncreated(Handle handle) const {
482     auto it = states_.find(handle);
483     if (it != states_.end()) {
484       // Tracked values are not uncreated.
485       return false;
486     }
487 
488     if (handle == kInvalidHandle) {
489       // Strangely enough, an invalid handle can never be created.
490       return true;
491     }
492 
493     // The following assumes handles are incrementally generated:
494     if (it == states_.end()) {
495       // value is in range of (last_destroyed, inf)  => uncreated.
496       return handle > last_destroyed_;
497     }
498 
499     auto min_it = states_.begin();
500     if (handle < min_it->first) {
501       // value smaller than anything tracked: it was destroyed and we stopped tracking it.
502       return false;
503     }
504 
505     auto max_it = states_.rbegin();
506     if (handle > max_it->first) {
507       // value too big: it's uncreated;
508       return true;
509     }
510 
511     // else it was a value that was previously tracked within [min,max] but no longer
512     return false;
513   }
514 
515  public:
Dumpiorap::perfetto::PerfettoConsumerImpl::Impl516   void Dump(::android::Printer& printer) {
517     // Locking can fail if we dump during a deadlock, so just do a best-effort lock here.
518     bool is_it_locked = mutex_.try_lock();
519 
520     printer.printFormatLine("Perfetto consumer state:");
521     if (!is_it_locked) {
522       printer.printLine("""""  (possible deadlock)");
523     }
524     printer.printFormatLine("  Last destroyed handle: %" PRId64, last_destroyed_);
525     printer.printFormatLine("  Last created handle: %" PRId64, last_created_);
526     printer.printFormatLine("");
527     printer.printFormatLine("  In-flight handles:");
528 
529     for (auto it = states_.begin(); it != states_.end(); ++it) {
530       HandleDescription& handle_desc = it->second;
531       uint64_t started_tracing =
532           handle_desc.started_tracing_ns_ ? *handle_desc.started_tracing_ns_ : 0;
533       printer.printFormatLine("    Handle %" PRId64, handle_desc.handle_);
534       printer.printFormatLine("      Kind: %s", ToString(handle_desc.kind_).c_str());
535       printer.printFormatLine("      Perfetto State: %d", static_cast<int>(handle_desc.state_));
536       printer.printFormatLine("      Started tracing at: %" PRIu64, started_tracing);
537       printer.printFormatLine("      Last transition at: %" PRIu64,
538                               handle_desc.last_transition_ns_);
539     }
540     if (states_.empty()) {
541       printer.printFormatLine("    (None)");
542     }
543 
544     printer.printFormatLine("");
545 
546     if (is_it_locked) {  // u.b. if calling unlock on an unlocked mutex.
547       mutex_.unlock();
548     }
549   }
550 
GetImplSingletoniorap::perfetto::PerfettoConsumerImpl::Impl551   static PerfettoConsumerImpl::Impl* GetImplSingleton() {
552     static PerfettoConsumerImpl::Impl impl;
553     return &impl;
554   }
555 };
556 
557 // Use a singleton because fruit instantiates a new PerfettoConsumer object for every
558 // new rx chain in RxProducerFactory. However, we want to track all perfetto transitions globally
559 // through 1 impl object.
560 //
561 // TODO: Avoiding a singleton would mean a more significant refactoring to remove the fruit/perfetto
562 // usage.
563 
564 
565 //
566 // Forward all calls to PerfettoConsumerImpl::Impl
567 //
568 
~PerfettoConsumerImpl()569 PerfettoConsumerImpl::~PerfettoConsumerImpl() {
570   // delete impl_;  // TODO: no singleton
571 }
572 
Initialize()573 void PerfettoConsumerImpl::Initialize() {
574   // impl_ = new PerfettoConsumerImpl::Impl();  // TODO: no singleton
575   impl_ = PerfettoConsumerImpl::Impl::GetImplSingleton();
576 }
577 
Dump(::android::Printer & printer)578 void PerfettoConsumerImpl::Dump(::android::Printer& printer) {
579   PerfettoConsumerImpl::Impl::GetImplSingleton()->Dump(/*borrow*/printer);
580 }
581 
Create(const void * config_proto,size_t config_len,PerfettoConsumer::OnStateChangedCb callback,void * callback_arg)582 PerfettoConsumer::Handle PerfettoConsumerImpl::Create(const void* config_proto,
583                                     size_t config_len,
584                                     PerfettoConsumer::OnStateChangedCb callback,
585                                     void* callback_arg) {
586   return impl_->Create(config_proto,
587                        config_len,
588                        callback,
589                        callback_arg);
590 }
591 
StartTracing(PerfettoConsumer::Handle handle)592 void PerfettoConsumerImpl::StartTracing(PerfettoConsumer::Handle handle) {
593   impl_->StartTracing(handle);
594 }
595 
ReadTrace(PerfettoConsumer::Handle handle)596 PerfettoConsumer::TraceBuffer PerfettoConsumerImpl::ReadTrace(PerfettoConsumer::Handle handle) {
597   return impl_->ReadTrace(handle);
598 }
599 
Destroy(PerfettoConsumer::Handle handle)600 void PerfettoConsumerImpl::Destroy(PerfettoConsumer::Handle handle) {
601   impl_->Destroy(handle);
602 }
603 
PollState(PerfettoConsumer::Handle handle)604 PerfettoConsumer::State PerfettoConsumerImpl::PollState(PerfettoConsumer::Handle handle) {
605   return impl_->PollState(handle);
606 }
607 
608 }  // namespace iorap::perfetto
609