#!/usr/bin/env ruby

# This script parses the boot log file generated by ftrace(tracer: function).
# 'parse' mode outputs per-cpu function info(CPUID, normalized timestamp, function name).
# 'analysis' mode outputs per-cpu boot performance and overall performance.
# Default mode: 'analysis'
# Default unit: 'cycles'

LKP_SRC = ENV['LKP_SRC'] || File.dirname(File.dirname(File.realpath($PROGRAM_NAME)))

require "#{LKP_SRC}/lib/ftrace"
require "#{LKP_SRC}/lib/statistics"
require 'English'
require 'optparse'

class ApBootInfo
  attr_accessor :cpu_id, :cpu_up_time, :bringup_cpu_time, :sched_cpu_starting_time, \
                :cpuhp_online_idle_time, :global_param

  def initialize(id: -1, cu: 0, buc: 0, scs: 0, coi: 0, gp: nil)
    @cpu_id = id
    @cpu_up_time = cu
    @bringup_cpu_time = buc
    @sched_cpu_starting_time = scs
    @cpuhp_online_idle_time = coi
    @global_param = gp
  end

  def boot_duration
    @cpuhp_online_idle_time - @sched_cpu_starting_time
  end

  def early_boot_duration
    @sched_cpu_starting_time - @bringup_cpu_time
  end

  def bp_boot_duration
    @bringup_cpu_time - @cpu_up_time
  end

  def boot_start
    if !@global_param[:smp_init_time].zero?
      @cpu_up_time - @global_param[:smp_init_time]
    else
      printf("Please set smp_init_time for cpu %d first\n", @cpu_id)
    end
  end

  def bp_boot_start
    if !@global_param[:smp_init_time].zero?
      @cpuhp_online_idle_time - @global_param[:smp_init_time]
    else
      printf("Please set smp_init_time for cpu %d first\n", @cpu_id)
    end
  end
end

# Actually this is the parse function of BootTraceParser, to adjust the code style
# we wrap it into a class.
class CpuBootLogParser
  attr_accessor :normalized, :global_param, :apbootinfo_hash

  def initialize(normalized: 1)
    @cpu_up_arr = []
    @boot_info_arr = []
    @global_param = {}
    @apbootinfo_hash = {}
    @normalized = normalized
  end

  def parse(func, timestamp, cpu_id)
    case func
    when 'smp_init', 'sched_init_smp', 'run_init_process'
      process_global_params(timestamp, func)
    when 'cpu_up'
      process_cpu_up(timestamp)
    when 'bringup_cpu'
      process_bringup_cpu(timestamp)
    when 'sched_cpu_starting'
      process_sched_cpu_starting(timestamp, cpu_id)
    when 'cpuhp_online_idle'
      process_cpuhp_online_idle(timestamp, cpu_id)
    end
  end

  def process_global_params(timestamp, func)
    @global_param["#{func}_time".to_sym] = timestamp / @normalized
  end

  def process_cpu_up(timestamp)
    @cpu_up_arr.push(timestamp / @normalized)
  end

  def process_bringup_cpu(timestamp)
    cu = @cpu_up_arr.shift
    buc = timestamp / @normalized
    if cu.nil?
      puts 'Incomplete boot log of function cpu_up.'
      return
    end
    boot_info = ApBootInfo.new(cu: cu, buc: buc, gp: @global_param)
    @boot_info_arr.push(boot_info)
  end

  def process_sched_cpu_starting(timestamp, cpu_id)
    boot_info = @boot_info_arr.shift
    if boot_info.nil?
      puts 'Incomplete boot log of function bringup_cpu.'
      return
    end
    scs = timestamp / @normalized
    boot_info.cpu_id = cpu_id
    boot_info.sched_cpu_starting_time = scs
    @apbootinfo_hash[cpu_id] = boot_info
  end

  def process_cpuhp_online_idle(timestamp, cpu_id)
    return if cpu_id.zero?

    unless @apbootinfo_hash.key?(cpu_id)
      puts 'Incomplete boot log of function sched_cpu_starting.'
      return
    end
    coi = timestamp / @normalized
    @apbootinfo_hash[cpu_id].cpuhp_online_idle_time = coi
  end
end

class BootTraceParser
  attr_reader :func_arr, :overall_stat, :options, :total_ap, :apbootinfo_hash, :global_param

  def initialize(file, options)
    @file = file
    @options = options
    @func_arr = []
    @overall_stat = Hash.new(0)
    @ftrace = FuncTrace.new(file)
    @total_ap = 0
    @is_parsed = false
    @normalized = options[:freq].zero? ? 1 : options[:freq] * 1.0 / 1_000_000
    @apbootinfo_hash = {}
    @global_param = {}
  end

  def analyze_stat
    unless @is_parsed
      puts 'please parse the log first.'
      return
    end

    bt_arr = []
    early_bt_arr = []
    bp_bt_arr = []

    (1..@total_ap).each do |i|
      bt_arr.push(apbootinfo_hash[i].boot_duration)
      early_bt_arr.push(apbootinfo_hash[i].early_boot_duration)
      bp_bt_arr.push(apbootinfo_hash[i].bp_boot_duration)
    end

    @overall_stat['average_boot_duration'] = bt_arr.average
    @overall_stat['boot_duration_std_deviation'] = bt_arr.standard_deviation
    @overall_stat['average_early_boot_duration'] = early_bt_arr.average
    @overall_stat['early_boot_duration_std_deviation'] = early_bt_arr.standard_deviation
    @overall_stat['average_bp_boot_duration'] = bp_bt_arr.average
    @overall_stat['bp_boot_duration_std_deviation'] = bp_bt_arr.standard_deviation
  end

  def parse_log
    cpu_parser = CpuBootLogParser.new(normalized: @normalized)
    @ftrace.each do |sample|
      cpu_id = sample.cpu.to_i
      timestamp = sample.timestamp.to_f
      func = sample.func.to_s

      cpu_parser.parse(func, timestamp, cpu_id)

      @func_arr.push(Array.[](cpu_id, (timestamp - cpu_parser.global_param[:smp_init_time]) / @normalized, func))
    end

    @global_param = cpu_parser.global_param
    @apbootinfo_hash = cpu_parser.apbootinfo_hash

    @apbootinfo_hash.each_key do |key|
      @apbootinfo_hash[key].global_param[:sched_init_smp_time] = @global_param[:sched_init_smp_time]
      @apbootinfo_hash[key].global_param[:run_init_process_time] = @global_param[:run_init_process_time]
    end
    @total_ap = @apbootinfo_hash.length
    @is_parsed = true
  end

  def total_boot_time
    @apbootinfo_hash[@total_ap].cpuhp_online_idle_time - @global_param[:smp_init_time] if @is_parsed
  end

  def kernel_boot_time
    @global_param[:run_init_process_time] - @global_param[:smp_init_time] if @is_parsed
  end
end

def print_analysis(parser, options)
  total_ap = parser.total_ap

  printf("%3s  %15s  %24s\n", 'Cpu.ID', 'Measurement', 'Normalized Time(' + options[:unit] + ')')
  (1..total_ap).each do |i|
    %w(boot_duration early_boot_duration bp_boot_duration boot_start bp_boot_start).each do |stat|
      printf("CPU.%03d  %-20s  %-12.1f %s\n", \
             i, stat, parser.apbootinfo_hash[i].send(stat), options[:unit])
    end
  end

  %w(total_boot_time kernel_boot_time).each do |stat|
    printf("\n%s: %15.1f %s\n\n", stat, parser.send(stat), options[:unit])
  end

  parser.overall_stat.each_key do |key|
    printf("%-35s %-10.1f %s\n", key, parser.overall_stat[key], options[:unit]) unless parser.overall_stat[key].zero?
  end
end

def print_func(parser, options)
  func_arr = parser.func_arr
  printf("%3s %20s %30s\n", 'CPU.ID', 'Normalized Time(' + options[:unit] + ')', 'Function')
  func_arr.each do |func_info|
    printf("%03d %20.1f%s %20s\n", func_info[0], func_info[1], options[:unit], func_info[2])
  end
end

def parse(options, file)
  parser = BootTraceParser.new(file, options)
  parser.parse_log

  if options[:mode] == 'analysis'
    parser.analyze_stat
    print_analysis(parser, options)
  else
    print_func(parser, options)
  end
end

options = {
  file: nil,
  mode: 'analysis',
  freq: 0,
  unit: 'cycles'
}

OptionParser.new do |opts|
  opts.banner = "Usage:
     parse-ftrace-function.rb [--mode <parse/analysis>] [--freq <Cpu-frequency>] --file <path-to-ftrace-logfile>
     If --freq is set, then the time unit will be us."

  opts.on('-m MODE', '--mode MODE', 'select mode (parse/analysis)') do |mode|
    unless %w(parse analysis).include?(mode)
      puts opts.banner
      exit
    end
    options[:mode] = mode.to_s
  end

  opts.on('-f FREQ', '--freq FREQ', 'set cpu frequency') do |freq|
    freq = freq.chop.to_f * 1_000_000_000 if freq.to_s =~ /^[0-9.]+[Gg]$/
    freq = freq.chop.to_f * 1_000_000 if freq.to_s =~ /^[0-9.]+[Mm]$/
    options[:freq] = freq.to_i
    options[:unit] = 'us'
  end

  opts.on('-h', '--help', 'help info') do
    puts opts
    exit
  end
end.parse!

options[:file] = ARGV.pop
raise 'please specify a file to be parsed.' unless options[:file]

File.open(options[:file], 'r') do |f|
  parse(options, f)
end
