/* * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "src/trace_redaction/redact_sched_events.h" #include "perfetto/protozero/scattered_heap_buffer.h" #include "src/trace_processor/util/status_macros.h" #include "src/trace_redaction/proto_util.h" #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h" #include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h" #include "protos/perfetto/trace/ftrace/sched.pbzero.h" namespace perfetto::trace_redaction { namespace { bool IsTrue(bool value) { return value; } // Copy a field from 'decoder' to 'message' if the field can be found. Returns // false if the field cannot be found. bool Passthrough(protozero::ProtoDecoder& decoder, uint32_t field_id, protozero::Message* message) { auto field = decoder.FindField(field_id); if (field.valid()) { proto_util::AppendField(field, message); return true; } return false; } } // namespace int64_t InternTable::Push(const char* data, size_t size) { std::string_view outer(data, size); for (size_t i = 0; i < interned_comms_.size(); ++i) { auto view = interned_comms_[i]; if (view == outer) { return static_cast(i); } } // No room for the new string, reject the request. if (comms_length_ + size > comms_.size()) { return -1; } auto* head = comms_.data() + comms_length_; // Important note, the null byte is not copied. memcpy(head, data, size); comms_length_ += size; size_t id = interned_comms_.size(); interned_comms_.emplace_back(head, size); return static_cast(id); } std::string_view InternTable::Find(size_t index) const { if (index < interned_comms_.size()) { return interned_comms_[index]; } return {}; } // Redact sched switch trace events in an ftrace event bundle: // // event { // timestamp: 6702093744772646 // pid: 0 // sched_switch { // prev_comm: "swapper/0" // prev_pid: 0 // prev_prio: 120 // prev_state: 0 // next_comm: "writer" // next_pid: 23020 // next_prio: 96 // } // } // // In the above message, it should be noted that "event.pid" will always be // equal to "event.sched_switch.prev_pid". // // "ftrace_event_bundle_message" is the ftrace event bundle (contains a // collection of ftrace event messages) because data in a sched_switch message // is needed in order to know if the event should be added to the bundle. base::Status RedactSchedEvents::Transform(const Context& context, std::string* packet) const { PERFETTO_DCHECK(modifier_); PERFETTO_DCHECK(waking_filter_); if (!context.timeline) { return base::ErrStatus("RedactSchedEvents: missing timeline."); } if (!context.package_uid.has_value()) { return base::ErrStatus("RedactSchedEvents: missing package uid."); } if (!packet || packet->empty()) { return base::ErrStatus("RedactSchedEvents: null or empty packet."); } protozero::HeapBuffered message; protozero::ProtoDecoder decoder(*packet); for (auto field = decoder.ReadField(); field.valid(); field = decoder.ReadField()) { if (field.id() == protos::pbzero::TracePacket::kFtraceEventsFieldNumber) { RETURN_IF_ERROR( OnFtraceEvents(context, field, message->set_ftrace_events())); } else { proto_util::AppendField(field, message.get()); } } packet->assign(message.SerializeAsString()); return base::OkStatus(); } base::Status RedactSchedEvents::OnFtraceEvents( const Context& context, protozero::Field ftrace_events, protos::pbzero::FtraceEventBundle* message) const { PERFETTO_DCHECK(ftrace_events.id() == protos::pbzero::TracePacket::kFtraceEventsFieldNumber); protozero::ProtoDecoder decoder(ftrace_events.as_bytes()); auto cpu = decoder.FindField(protos::pbzero::FtraceEventBundle::kCpuFieldNumber); if (!cpu.valid()) { return base::ErrStatus( "RedactSchedEvents: missing cpu in ftrace event bundle."); } for (auto field = decoder.ReadField(); field.valid(); field = decoder.ReadField()) { if (field.id() == protos::pbzero::FtraceEventBundle::kEventFieldNumber) { RETURN_IF_ERROR( OnFtraceEvent(context, cpu.as_int32(), field, message->add_event())); continue; } if (field.id() == protos::pbzero::FtraceEventBundle::kCompactSchedFieldNumber) { protos::pbzero::FtraceEventBundle::CompactSched::Decoder comp_sched( field.as_bytes()); RETURN_IF_ERROR(OnCompSched(context, cpu.as_int32(), comp_sched, message->set_compact_sched())); continue; } proto_util::AppendField(field, message); } return base::OkStatus(); } base::Status RedactSchedEvents::OnFtraceEvent( const Context& context, int32_t cpu, protozero::Field ftrace_event, protos::pbzero::FtraceEvent* message) const { PERFETTO_DCHECK(ftrace_event.id() == protos::pbzero::FtraceEventBundle::kEventFieldNumber); protozero::ProtoDecoder decoder(ftrace_event.as_bytes()); auto ts = decoder.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber); if (!ts.valid()) { return base::ErrStatus( "RedactSchedEvents: missing timestamp in ftrace event."); } std::string scratch_str; for (auto field = decoder.ReadField(); field.valid(); field = decoder.ReadField()) { switch (field.id()) { case protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber: { protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch( field.as_bytes()); RETURN_IF_ERROR(OnFtraceEventSwitch(context, ts.as_uint64(), cpu, sched_switch, &scratch_str, message->set_sched_switch())); break; } case protos::pbzero::FtraceEvent::kSchedWakingFieldNumber: { protos::pbzero::SchedWakingFtraceEvent::Decoder sched_waking( field.as_bytes()); RETURN_IF_ERROR(OnFtraceEventWaking( context, ts.as_uint64(), cpu, sched_waking, &scratch_str, message)); break; } default: { proto_util::AppendField(field, message); break; } } } return base::OkStatus(); } base::Status RedactSchedEvents::OnFtraceEventSwitch( const Context& context, uint64_t ts, int32_t cpu, protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch, std::string* scratch_str, protos::pbzero::SchedSwitchFtraceEvent* message) const { PERFETTO_DCHECK(modifier_); PERFETTO_DCHECK(scratch_str); PERFETTO_DCHECK(message); std::array has_fields = { sched_switch.has_prev_comm(), sched_switch.has_prev_pid(), sched_switch.has_prev_prio(), sched_switch.has_prev_state(), sched_switch.has_next_comm(), sched_switch.has_next_pid(), sched_switch.has_next_prio()}; if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: missing required SchedSwitchFtraceEvent " "field."); } auto prev_pid = sched_switch.prev_pid(); auto prev_comm = sched_switch.prev_comm(); auto next_pid = sched_switch.next_pid(); auto next_comm = sched_switch.next_comm(); // There are 7 values in a sched switch message. Since 4 of the 7 can be // replaced, it is easier/cleaner to go value-by-value. Go in proto-defined // order. scratch_str->assign(prev_comm.data, prev_comm.size); modifier_->Modify(context, ts, cpu, &prev_pid, scratch_str); message->set_prev_comm(*scratch_str); // FieldNumber = 1 message->set_prev_pid(prev_pid); // FieldNumber = 2 message->set_prev_prio(sched_switch.prev_prio()); // FieldNumber = 3 message->set_prev_state(sched_switch.prev_state()); // FieldNumber = 4 scratch_str->assign(next_comm.data, next_comm.size); modifier_->Modify(context, ts, cpu, &next_pid, scratch_str); message->set_next_comm(*scratch_str); // FieldNumber = 5 message->set_next_pid(next_pid); // FieldNumber = 6 message->set_next_prio(sched_switch.next_prio()); // FieldNumber = 7 return base::OkStatus(); } // Redact sched waking trace events in a ftrace event bundle: // // event { // timestamp: 6702093787823849 // pid: 814 <-- waker // sched_waking { // comm: "surfaceflinger" // pid: 756 <-- target // prio: 97 // success: 1 // target_cpu: 2 // } // } base::Status RedactSchedEvents::OnFtraceEventWaking( const Context& context, uint64_t ts, int32_t cpu, protos::pbzero::SchedWakingFtraceEvent::Decoder& sched_waking, std::string* scratch_str, protos::pbzero::FtraceEvent* parent_message) const { PERFETTO_DCHECK(modifier_); PERFETTO_DCHECK(scratch_str); PERFETTO_DCHECK(parent_message); std::array has_fields = { sched_waking.has_comm(), sched_waking.has_pid(), sched_waking.has_prio(), sched_waking.has_success(), sched_waking.has_target_cpu()}; if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: missing required SchedWakingFtraceEvent " "field."); } auto pid = sched_waking.pid(); if (!waking_filter_->Includes(context, ts, pid)) { return base::OkStatus(); } auto comm = sched_waking.comm(); // There are 5 values in a sched switch message. Since 2 of the 5 can be // replaced, it is easier/cleaner to go value-by-value. Go in proto-defined // order. scratch_str->assign(comm.data, comm.size); modifier_->Modify(context, ts, cpu, &pid, scratch_str); auto message = parent_message->set_sched_waking(); message->set_comm(*scratch_str); // FieldNumber = 1 message->set_pid(pid); // FieldNumber = 2 message->set_prio(sched_waking.prio()); // FieldNumber = 3 message->set_success(sched_waking.success()); // FieldNumber = 4 message->set_target_cpu(sched_waking.target_cpu()); // FieldNumber = 5 return base::OkStatus(); } base::Status RedactSchedEvents::OnCompSched( const Context& context, int32_t cpu, protos::pbzero::FtraceEventBundle::CompactSched::Decoder& comp_sched, protos::pbzero::FtraceEventBundle::CompactSched* message) const { // Populate the intern table once; it will be used by both sched and waking. InternTable intern_table; for (auto it = comp_sched.intern_table(); it; ++it) { auto chars = it->as_string(); auto index = intern_table.Push(chars.data, chars.size); if (index < 0) { return base::ErrStatus( "RedactSchedEvents: failed to insert string into intern " "table."); } } std::array has_switch_fields = { comp_sched.has_switch_timestamp(), comp_sched.has_switch_prev_state(), comp_sched.has_switch_next_pid(), comp_sched.has_switch_next_prio(), comp_sched.has_switch_next_comm_index(), }; if (std::any_of(has_switch_fields.begin(), has_switch_fields.end(), IsTrue)) { RETURN_IF_ERROR( OnCompSchedSwitch(context, cpu, comp_sched, &intern_table, message)); } std::array has_waking_fields = { comp_sched.has_waking_timestamp(), comp_sched.has_waking_pid(), comp_sched.has_waking_target_cpu(), comp_sched.has_waking_prio(), comp_sched.has_waking_comm_index(), comp_sched.has_waking_common_flags(), }; if (std::any_of(has_waking_fields.begin(), has_waking_fields.end(), IsTrue)) { RETURN_IF_ERROR( OnCompactSchedWaking(context, comp_sched, &intern_table, message)); } // IMPORTANT: The intern table can only be added after switch and waking // because switch and/or waking can/will modify the intern table. for (auto view : intern_table.values()) { message->add_intern_table(view.data(), view.size()); } return base::OkStatus(); } base::Status RedactSchedEvents::OnCompSchedSwitch( const Context& context, int32_t cpu, protos::pbzero::FtraceEventBundle::CompactSched::Decoder& comp_sched, InternTable* intern_table, protos::pbzero::FtraceEventBundle::CompactSched* message) const { PERFETTO_DCHECK(modifier_); PERFETTO_DCHECK(message); std::array has_fields = { comp_sched.has_intern_table(), comp_sched.has_switch_timestamp(), comp_sched.has_switch_prev_state(), comp_sched.has_switch_next_pid(), comp_sched.has_switch_next_prio(), comp_sched.has_switch_next_comm_index(), }; if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: missing required FtraceEventBundle::CompactSched " "switch field."); } std::string scratch_str; protozero::PackedVarInt packed_comm; protozero::PackedVarInt packed_pid; // The first it_ts value is an absolute value, all other values are delta // values. uint64_t ts = 0; std::array parse_errors = {false, false, false}; auto it_ts = comp_sched.switch_timestamp(&parse_errors.at(0)); auto it_pid = comp_sched.switch_next_pid(&parse_errors.at(1)); auto it_comm = comp_sched.switch_next_comm_index(&parse_errors.at(2)); while (it_ts && it_pid && it_comm) { ts += *it_ts; auto pid = *it_pid; auto comm_index = *it_comm; auto comm = intern_table->Find(comm_index); scratch_str.assign(comm); modifier_->Modify(context, ts, cpu, &pid, &scratch_str); auto found = intern_table->Push(scratch_str.data(), scratch_str.size()); if (found < 0) { return base::ErrStatus( "RedactSchedEvents: failed to insert string into intern table."); } packed_comm.Append(found); packed_pid.Append(pid); ++it_ts; ++it_pid; ++it_comm; } if (std::any_of(parse_errors.begin(), parse_errors.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: error reading FtraceEventBundle::CompactSched."); } if (it_ts || it_pid || it_comm) { return base::ErrStatus( "RedactSchedEvents: uneven associative arrays in " "FtraceEventBundle::CompactSched (switch)."); } message->set_switch_next_pid(packed_pid); message->set_switch_next_comm_index(packed_comm); // There's a lot of data in a compact sched message. Most of it is packed data // and most of the data is not going to change. To avoid unpacking, doing // nothing, and then packing... cheat. Find the fields and pass them as opaque // blobs. // // kInternTableFieldNumber: The intern table will be modified by both // switch events and waking events. It will // be written elsewhere. // // kSwitchNextPidFieldNumber: The switch pid will change during thread // merging. // // kSwitchNextCommIndexFieldNumber: The switch comm value will change when // clearing thread names and replaced // during thread merging. auto passed_through = { Passthrough(comp_sched, protos::pbzero::FtraceEventBundle::CompactSched:: kSwitchTimestampFieldNumber, message), Passthrough(comp_sched, protos::pbzero::FtraceEventBundle::CompactSched:: kSwitchPrevStateFieldNumber, message), Passthrough(comp_sched, protos::pbzero::FtraceEventBundle::CompactSched:: kSwitchNextPrioFieldNumber, message)}; if (!std::all_of(passed_through.begin(), passed_through.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: missing required " "FtraceEventBundle::CompactSched switch field."); } return base::OkStatus(); } base::Status RedactSchedEvents::OnCompactSchedWaking( const Context& context, protos::pbzero::FtraceEventBundle::CompactSched::Decoder& compact_sched, InternTable* intern_table, protos::pbzero::FtraceEventBundle::CompactSched* compact_sched_message) const { protozero::PackedVarInt var_comm_index; protozero::PackedVarInt var_common_flags; protozero::PackedVarInt var_pid; protozero::PackedVarInt var_prio; protozero::PackedVarInt var_target_cpu; protozero::PackedVarInt var_timestamp; // Time is expressed as delta time, for example: // // Event: A B C D // Absolute Time: 20 30 35 41 // | | | | // Delta Time: 20 10 5 6 // // When an event is removed, for example, event B, delta times are off: // // Event: A * C D // Absolute Time: 20 30 35 41 // | | | | // Delta Time: 20 * 5 6 // | | | // Effective Abs. Time: 20 25 31 // Error: 0 10 10 // // To address this issue, delta times are added into a bucket. The bucket is // drained each time an event is retained. If an event is dropped, its time // is added to the bucket, but the bucket won't be drained until a retained // event drains it. uint64_t ts_bucket = 0; uint64_t ts_absolute = 0; std::string comm; std::array parse_errors = {!compact_sched.has_intern_table(), false, false, false, false, false, false}; // A note on readability, because the waking iterators are the primary focus, // they won't have a "waking" prefix. auto it_comm_index = compact_sched.waking_comm_index(&parse_errors.at(1)); auto it_common_flags = compact_sched.waking_common_flags(&parse_errors.at(2)); auto it_pid = compact_sched.waking_pid(&parse_errors.at(3)); auto it_prio = compact_sched.waking_prio(&parse_errors.at(4)); auto it_target_cpu = compact_sched.waking_target_cpu(&parse_errors.at(5)); auto it_timestamp = compact_sched.waking_timestamp(&parse_errors.at(6)); while (it_comm_index && it_common_flags && it_pid && it_prio && it_target_cpu && it_timestamp) { ts_bucket += *it_timestamp; // add time to the bucket ts_absolute += *it_timestamp; if (waking_filter_->Includes(context, ts_absolute, *it_pid)) { // Now that the waking event will be kept, it can be modified using the // same rules as switch events. auto pid = *it_pid; comm.assign(intern_table->Find(*it_comm_index)); modifier_->Modify(context, ts_absolute, *it_target_cpu, &pid, &comm); auto comm_it = intern_table->Push(comm.data(), comm.size()); var_comm_index.Append(comm_it); var_common_flags.Append(*it_common_flags); var_pid.Append(pid); var_prio.Append(*it_prio); var_target_cpu.Append(*it_target_cpu); var_timestamp.Append(ts_bucket); ts_bucket = 0; // drain the whole bucket. } ++it_comm_index; ++it_common_flags; ++it_pid; ++it_prio; ++it_target_cpu; ++it_timestamp; } if (std::any_of(parse_errors.begin(), parse_errors.end(), IsTrue)) { return base::ErrStatus( "RedactSchedEvents: failed to parse FtraceEventBundle::CompactSched."); } if (it_comm_index || it_common_flags || it_pid || it_prio || it_target_cpu || it_timestamp) { return base::ErrStatus( "RedactSchedEvents: uneven associative arrays in " "FtraceEventBundle::CompactSched (waking)."); } compact_sched_message->set_waking_comm_index(var_comm_index); compact_sched_message->set_waking_common_flags(var_common_flags); compact_sched_message->set_waking_pid(var_pid); compact_sched_message->set_waking_prio(var_prio); compact_sched_message->set_waking_target_cpu(var_target_cpu); compact_sched_message->set_waking_timestamp(var_timestamp); return base::OkStatus(); } } // namespace perfetto::trace_redaction