/*
 * Copyright (C) 2019 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/traced/probes/ftrace/compact_sched.h"

#include <stdint.h>

#include "perfetto/ext/base/optional.h"
#include "protos/perfetto/config/ftrace/ftrace_config.gen.h"
#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
#include "src/traced/probes/ftrace/event_info_constants.h"
#include "src/traced/probes/ftrace/ftrace_config_utils.h"

namespace perfetto {

namespace {

// Pre-parse the format of sched_switch, checking if our simplifying
// assumptions about possible widths/signedness hold, and record the subset
// of the format that will be used during parsing.
base::Optional<CompactSchedSwitchFormat> ValidateSchedSwitchFormat(
    const Event& event) {
  using protos::pbzero::SchedSwitchFtraceEvent;

  CompactSchedSwitchFormat switch_format;
  switch_format.event_id = event.ftrace_event_id;
  switch_format.size = event.size;

  bool prev_state_valid = false;
  bool next_pid_valid = false;
  bool next_prio_valid = false;
  bool next_comm_valid = false;
  for (const auto& field : event.fields) {
    switch (field.proto_field_id) {
      case SchedSwitchFtraceEvent::kPrevStateFieldNumber:
        switch_format.prev_state_offset = field.ftrace_offset;
        switch_format.prev_state_type = field.ftrace_type;

        // kernel type: long
        prev_state_valid = (field.ftrace_type == kFtraceInt32 ||
                            field.ftrace_type == kFtraceInt64);
        break;

      case SchedSwitchFtraceEvent::kNextPidFieldNumber:
        switch_format.next_pid_offset = field.ftrace_offset;
        switch_format.next_pid_type = field.ftrace_type;

        // kernel type: pid_t
        next_pid_valid = (field.ftrace_type == kFtracePid32);
        break;

      case SchedSwitchFtraceEvent::kNextPrioFieldNumber:
        switch_format.next_prio_offset = field.ftrace_offset;
        switch_format.next_prio_type = field.ftrace_type;

        // kernel type: int
        next_prio_valid = (field.ftrace_type == kFtraceInt32);
        break;

      case SchedSwitchFtraceEvent::kNextCommFieldNumber:
        switch_format.next_comm_offset = field.ftrace_offset;

        next_comm_valid =
            (field.ftrace_type == kFtraceFixedCString &&
             field.ftrace_size == CommInterner::kExpectedCommLength);
        break;
      default:
        break;
    }
  }

  if (!prev_state_valid || !next_pid_valid || !next_prio_valid ||
      !next_comm_valid) {
    return base::nullopt;
  }
  return base::make_optional(switch_format);
}

// Pre-parse the format of sched_waking, checking if our simplifying
// assumptions about possible widths/signedness hold, and record the subset
// of the format that will be used during parsing.
base::Optional<CompactSchedWakingFormat> ValidateSchedWakingFormat(
    const Event& event) {
  using protos::pbzero::SchedWakingFtraceEvent;

  CompactSchedWakingFormat waking_format;
  waking_format.event_id = event.ftrace_event_id;
  waking_format.size = event.size;

  bool pid_valid = false;
  bool target_cpu_valid = false;
  bool prio_valid = false;
  bool comm_valid = false;
  for (const auto& field : event.fields) {
    switch (field.proto_field_id) {
      case SchedWakingFtraceEvent::kPidFieldNumber:
        waking_format.pid_offset = field.ftrace_offset;
        waking_format.pid_type = field.ftrace_type;

        // kernel type: pid_t
        pid_valid = (field.ftrace_type == kFtracePid32);
        break;

      case SchedWakingFtraceEvent::kTargetCpuFieldNumber:
        waking_format.target_cpu_offset = field.ftrace_offset;
        waking_format.target_cpu_type = field.ftrace_type;

        // kernel type: int
        target_cpu_valid = (field.ftrace_type == kFtraceInt32);
        break;

      case SchedWakingFtraceEvent::kPrioFieldNumber:
        waking_format.prio_offset = field.ftrace_offset;
        waking_format.prio_type = field.ftrace_type;

        // kernel type: int
        prio_valid = (field.ftrace_type == kFtraceInt32);
        break;

      case SchedWakingFtraceEvent::kCommFieldNumber:
        waking_format.comm_offset = field.ftrace_offset;

        comm_valid = (field.ftrace_type == kFtraceFixedCString &&
                      field.ftrace_size == CommInterner::kExpectedCommLength);
        break;
      default:
        break;
    }
  }

  if (!pid_valid || !target_cpu_valid || !prio_valid || !comm_valid) {
    return base::nullopt;
  }
  return base::make_optional(waking_format);
}

}  // namespace

// TODO(rsavitski): could avoid looping over all events if the caller did the
// work to remember the relevant events (translation table construction already
// loops over them).
CompactSchedEventFormat ValidateFormatForCompactSched(
    const std::vector<Event>& events) {
  using protos::pbzero::FtraceEvent;

  base::Optional<CompactSchedSwitchFormat> switch_format;
  base::Optional<CompactSchedWakingFormat> waking_format;
  for (const Event& event : events) {
    if (event.proto_field_id == FtraceEvent::kSchedSwitchFieldNumber) {
      switch_format = ValidateSchedSwitchFormat(event);
    }
    if (event.proto_field_id == FtraceEvent::kSchedWakingFieldNumber) {
      waking_format = ValidateSchedWakingFormat(event);
    }
  }

  if (switch_format.has_value() && waking_format.has_value()) {
    return CompactSchedEventFormat{/*format_valid=*/true, switch_format.value(),
                                   waking_format.value()};
  } else {
    PERFETTO_ELOG("Unexpected sched_switch or sched_waking format.");
    return CompactSchedEventFormat{/*format_valid=*/false,
                                   CompactSchedSwitchFormat{},
                                   CompactSchedWakingFormat{}};
  }
}

CompactSchedEventFormat InvalidCompactSchedEventFormatForTesting() {
  return CompactSchedEventFormat{/*format_valid=*/false,
                                 CompactSchedSwitchFormat{},
                                 CompactSchedWakingFormat{}};
}

// TODO(rsavitski): find the correct place in the trace for, and method of,
// reporting rejection of compact_sched due to compile-time assumptions not
// holding at runtime.
CompactSchedConfig CreateCompactSchedConfig(
    const FtraceConfig& request,
    const CompactSchedEventFormat& compact_format) {
  if (!request.compact_sched().enabled())
    return CompactSchedConfig{/*enabled=*/false};

  if (!compact_format.format_valid)
    return CompactSchedConfig{/*enabled=*/false};

  return CompactSchedConfig{/*enabled=*/true};
}

CompactSchedConfig EnabledCompactSchedConfigForTesting() {
  return CompactSchedConfig{/*enabled=*/true};
}

CompactSchedConfig DisabledCompactSchedConfigForTesting() {
  return CompactSchedConfig{/*enabled=*/false};
}

// Check size of stack-allocated bundle state.
static_assert(sizeof(CompactSchedBuffer) <= 1 << 18,
              "CompactSchedBuffer's on-stack size excessively large.");

void CompactSchedSwitchBuffer::Write(
    protos::pbzero::FtraceEventBundle::CompactSched* compact_out) const {
  compact_out->set_switch_timestamp(timestamp_);
  compact_out->set_switch_next_pid(next_pid_);
  compact_out->set_switch_prev_state(prev_state_);
  compact_out->set_switch_next_prio(next_prio_);
  compact_out->set_switch_next_comm_index(next_comm_index_);
}

void CompactSchedSwitchBuffer::Reset() {
  last_timestamp_ = 0;
  timestamp_.Reset();
  next_pid_.Reset();
  prev_state_.Reset();
  next_prio_.Reset();
  next_comm_index_.Reset();
}

void CompactSchedWakingBuffer::Write(
    protos::pbzero::FtraceEventBundle::CompactSched* compact_out) const {
  compact_out->set_waking_timestamp(timestamp_);
  compact_out->set_waking_pid(pid_);
  compact_out->set_waking_target_cpu(target_cpu_);
  compact_out->set_waking_prio(prio_);
  compact_out->set_waking_comm_index(comm_index_);
}

void CompactSchedWakingBuffer::Reset() {
  last_timestamp_ = 0;
  timestamp_.Reset();
  pid_.Reset();
  target_cpu_.Reset();
  prio_.Reset();
  comm_index_.Reset();
}

void CommInterner::Write(
    protos::pbzero::FtraceEventBundle::CompactSched* compact_out) const {
  for (size_t i = 0; i < interned_comms_size_; i++) {
    compact_out->add_intern_table(interned_comms_[i].data(),
                                  interned_comms_[i].size());
  }
}

void CommInterner::Reset() {
  intern_buf_write_pos_ = 0;
  interned_comms_size_ = 0;
}

void CompactSchedBuffer::WriteAndReset(
    protos::pbzero::FtraceEventBundle* bundle) {
  if (switch_.size() > 0 || waking_.size() > 0) {
    auto* compact_out = bundle->set_compact_sched();

    PERFETTO_DCHECK(interner_.interned_comms_size() > 0);
    interner_.Write(compact_out);

    if (switch_.size() > 0)
      switch_.Write(compact_out);

    if (waking_.size() > 0)
      waking_.Write(compact_out);
  }

  interner_.Reset();
  switch_.Reset();
  waking_.Reset();
}

}  // namespace perfetto
