lib/debug/dap_custom/traceInspector.rb



module DEBUGGER__
  module DAP_TraceInspector
    class MultiTracer < Tracer
      def initialize ui, evts, trace_params, max_log_size: nil, **kw
        @evts = evts
        @log = []
        @trace_params = trace_params
        if max_log_size
          @max_log_size = max_log_size
        else
          @max_log_size = 50000
        end
        @dropped_trace_cnt = 0
        super(ui, **kw)
        @type = 'multi'
        @name = 'TraceInspector'
      end
  
      attr_accessor :dropped_trace_cnt
      attr_reader :log
  
      def setup
        @tracer = TracePoint.new(*@evts){|tp|
          next if skip?(tp)
  
          case tp.event
          when :call, :c_call, :b_call
            if @trace_params
              params = parameters_info tp
            end
            append(call_trace_log(tp, params: params))
          when :return, :c_return, :b_return
            return_str = DEBUGGER__.safe_inspect(tp.return_value, short: true, max_length: 4096)
            append(call_trace_log(tp, return_str: return_str))
          when :line
            append(line_trace_log(tp))
          end
        }
      end

      def parameters_info tp
        b = tp.binding
        tp.parameters.map{|_type, name|
          begin
            { name: name, value: DEBUGGER__.safe_inspect(b.local_variable_get(name), short: true, max_length: 4096) }
          rescue NameError, TypeError
            nil
          end
        }.compact
      end
  
      def call_identifier_str tp
        if tp.defined_class
          minfo(tp)
        else
          "block"
        end
      end
  
      def append log
        if @log.size >= @max_log_size
          @dropped_trace_cnt += 1
          @log.shift
        end
        @log << log
      end
  
      def call_trace_log tp, return_str: nil, params: nil
        log = {
          depth: DEBUGGER__.frame_depth,
          name: call_identifier_str(tp),
          threadId: Thread.current.instance_variable_get(:@__thread_client_id),
          location: {
            path: tp.path,
            line: tp.lineno
          }
        }
        log[:returnValue] = return_str if return_str
        log[:parameters] = params if params && params.size > 0
        log
      end
  
      def line_trace_log tp
        {
          depth: DEBUGGER__.frame_depth,
          threadId: Thread.current.instance_variable_get(:@__thread_client_id),
          location: {
            path: tp.path,
            line: tp.lineno
          }
        }
      end
  
      def skip? tp
        super || !@evts.include?(tp.event)
      end

      def skip_with_pattern?(tp)
        super && !tp.method_id&.match?(@pattern)
      end
    end

    class Custom_Recorder < ThreadClient::Recorder
      def initialize max_log_size: nil
        if max_log_size
          @max_log_size = max_log_size
        else
          @max_log_size = 50000
        end
        @dropped_trace_cnt = 0
        super()
      end

      attr_accessor :dropped_trace_cnt

      def append frames
        if @log.size >= @max_log_size
          @dropped_trace_cnt += 1
          @log.shift
        end
        @log << frames
      end
    end

    module Custom_UI_DAP
      def custom_dap_request_rdbgTraceInspector(req)
        @q_msg << req
      end
    end

    module Custom_Session
      def process_trace_cmd req
        cmd = req.dig('arguments', 'subCommand')
        case cmd
        when 'enable'
          events = req.dig('arguments', 'events')
          evts = []
          trace_params = false
          filter = req.dig('arguments', 'filterRegExp')
          max_log_size = req.dig('arguments', 'maxLogSize')
          events.each{|evt|
            case evt
            when 'traceLine'
              evts << :line
            when 'traceCall'
              evts << :call
              evts << :b_call
            when 'traceReturn'
              evts << :return
              evts << :b_return
            when 'traceParams'
              trace_params = true
            when 'traceClanguageCall'
              evts << :c_call
            when 'traceClanguageReturn'
              evts << :c_return
            else
              raise "unknown trace type #{evt}"
            end
          }
          add_tracer MultiTracer.new @ui, evts, trace_params, max_log_size: max_log_size, pattern: filter
          @ui.respond req, {}
        when 'disable'
          if t = find_multi_trace
            t.disable
          end
          @ui.respond req, {}
        when 'collect'
          logs = []
          if t = find_multi_trace
            logs = t.log
            if t.dropped_trace_cnt > 0
              @ui.puts "Return #{logs.size} traces and #{t.dropped_trace_cnt} traces are dropped"
            else
              @ui.puts "Return #{logs.size} traces"
            end
            t.dropped_trace_cnt = 0
          end
          @ui.respond req, logs: logs
        else
          raise "Unknown trace sub command #{cmd}"
        end
        return :retry
      end

      def find_multi_trace
        @tracers.values.each{|t|
          if t.type == 'multi'
            return t
          end
        }
        return nil
      end

      def process_record_cmd req
        cmd = req.dig('arguments', 'subCommand')
        case cmd
        when 'enable'
          @tc << [:dap, :rdbgTraceInspector, req]
        when 'disable'
          @tc << [:dap, :rdbgTraceInspector, req]
        when 'step'
          tid = req.dig('arguments', 'threadId')
          count = req.dig('arguments', 'count')
          if tc = find_waiting_tc(tid)
            @ui.respond req, {}
            tc << [:step, :in, count]
          else
            fail_response req
          end
        when 'stepBack'
          tid = req.dig('arguments', 'threadId')
          count = req.dig('arguments', 'count')
          if tc = find_waiting_tc(tid)
            @ui.respond req, {}
            tc << [:step, :back, count]
          else
            fail_response req
          end
        when 'collect'
          tid = req.dig('arguments', 'threadId')
          if tc = find_waiting_tc(tid)
            tc << [:dap, :rdbgTraceInspector, req]
          else
            fail_response req
          end
        else
          raise "Unknown record sub command #{cmd}"
        end
      end

      def custom_dap_request_rdbgTraceInspector(req)
        cmd = req.dig('arguments', 'command')
        case cmd
        when 'trace'
          process_trace_cmd req
        when 'record'
          process_record_cmd req
        else
          raise "Unknown command #{cmd}"
        end
      end

      def custom_dap_request_event_rdbgTraceInspector(req, result)
        cmd = req.dig('arguments', 'command')
        case cmd
        when 'record'
          process_event_record_cmd(req, result)
        else
          raise "Unknown command #{cmd}"
        end
      end

      def process_event_record_cmd(req, result)
        cmd = req.dig('arguments', 'subCommand')
        case cmd
        when 'enable'
          @ui.respond req, {}
        when 'disable'
          @ui.respond req, {}
        when 'collect'
          cnt = result.delete :dropped_trace_cnt
          if cnt > 0
            @ui.puts "Return #{result[:logs].size} traces and #{cnt} traces are dropped"
          else
            @ui.puts "Return #{result[:logs].size} traces"
          end
          @ui.respond req, result
        else
          raise "Unknown command #{cmd}"
        end
      end
    end

    module Custom_ThreadClient
      def custom_dap_request_rdbgTraceInspector(req)
        cmd = req.dig('arguments', 'command')
        case cmd
        when 'record'
          process_record_cmd(req)
        else
          raise "Unknown command #{cmd}"
        end
      end

      def process_record_cmd(req)
        cmd = req.dig('arguments', 'subCommand')
        case cmd
        when 'enable'
          size = req.dig('arguments', 'maxLogSize')
          @recorder = Custom_Recorder.new max_log_size: size
          @recorder.enable
          event! :protocol_result, :rdbgTraceInspector, req
        when 'disable'
          if @recorder&.enabled?
            @recorder.disable
          end
          @recorder = nil
          event! :protocol_result, :rdbgTraceInspector, req
        when 'collect'
          logs = []
          log_index = nil
          trace_cnt = 0
          unless @recorder.nil?
            log_index = @recorder.log_index
            @recorder.log.each{|frames|
              crt_frame = frames[0]
              log = {
                name: crt_frame.name,
                location: {
                  path: crt_frame.location.path,
                  line: crt_frame.location.lineno,
                },
                depth: crt_frame.frame_depth
              }
              if params = crt_frame.iseq_parameters_info
                log[:parameters] = params
              end
              if return_str = crt_frame.return_str
                log[:returnValue] = return_str
              end
              logs << log
            }
            trace_cnt = @recorder.dropped_trace_cnt
            @recorder.dropped_trace_cnt = 0
          end
          event! :protocol_result, :rdbgTraceInspector, req, logs: logs, stoppedIndex: log_index, dropped_trace_cnt: trace_cnt
        else
          raise "Unknown command #{cmd}"
        end
      end
    end

    ::DEBUGGER__::SESSION.extend_feature session: Custom_Session, thread_client: Custom_ThreadClient, ui: Custom_UI_DAP
  end
end