Computer >> 컴퓨터 >  >> 프로그램 작성 >> Ruby

TracePoint를 사용하여 Ruby에서 디버깅 접근 방식 변경

Ruby는 항상 개발자에게 생산성을 제공하는 것으로 알려져 있습니다. 코드 작성 시 생산성을 높이는 우아한 구문, 풍부한 메타 프로그래밍 지원 등과 같은 기능과 함께 TracePoint라는 또 다른 비밀 무기도 있습니다. 더 빨리 "디버그"하는 데 도움이 됩니다.

이 게시물에서는 디버깅에 대해 알게 된 두 가지 흥미로운 사실을 보여주기 위해 간단한 예를 사용하겠습니다.

  1. 대부분의 경우 버그 자체를 찾는 것은 어렵지 않지만 프로그램이 어떻게 작동하는지 자세히 이해하는 것은 어렵습니다. 이에 대해 깊이 이해하면 일반적으로 버그를 즉시 발견할 수 있습니다.
  2. 메서드 호출 수준까지 프로그램을 관찰하는 것은 시간이 많이 소요되며 디버깅 프로세스의 주요 병목 현상입니다.

그런 다음 TracePoint 프로그램이 수행하는 작업을 "알려줌"으로써 디버깅에 접근하는 방식을 변경할 수 있습니다.

디버깅은 프로그램과 디자인을 이해하는 것입니다.

plus_1이라는 Ruby 프로그램이 있다고 가정해 보겠습니다. 제대로 작동하지 않습니다. 이것을 어떻게 디버깅합니까?

# plus_1.rb
def plus_1(n)
  n + 2
end
 
input = ARGV[0].to_i
puts(plus_1(input))
$ ruby plus_1.rb 1
3

이상적으로는 3단계로 버그를 해결할 수 있어야 합니다.

  1. 디자인에서 기대하는 바 알아보기
  2. 현재 구현 이해
  3. 버그 추적

디자인에서 기대치 배우기

여기서 예상되는 동작은 무엇입니까? plus_1 1을 추가해야 합니다. 명령줄에서 입력한 인수에 대한 것입니다. 하지만 우리는 이것을 어떻게 "알"까요?

실제 사례에서 테스트 사례, 문서, 모형을 읽고 다른 사람에게 피드백을 요청하는 등의 방법으로 기대치를 이해할 수 있습니다. 우리의 이해는 프로그램이 어떻게 "설계"되었는지에 달려 있습니다.

이 단계는 디버깅 프로세스에서 가장 중요한 부분입니다. 프로그램이 어떻게 작동해야 하는지 이해하지 못하면 디버그할 수 없습니다.

그러나 팀 조정, 개발 워크플로 등과 같이 이 단계의 일부가 될 수 있는 많은 요소가 있습니다. TracePoint 이 문제는 도와드릴 수 없으므로 오늘은 이러한 문제에 대해 다루지 않겠습니다.

현재 구현 이해

프로그램의 예상 동작을 이해했다면 현재로서는 어떻게 작동하는지 알아야 합니다.

대부분의 경우 프로그램 작동 방식을 완전히 이해하려면 다음 정보가 필요합니다.

  • 프로그램 실행 중 호출되는 메소드
  • 메소드 호출의 호출 및 반환 순서
  • 각 메소드 호출에 전달된 인수
  • 각 메서드 호출에서 반환된 값
  • 각 메소드 호출 중에 발생한 모든 부작용, 예:데이터 변형 또는 데이터베이스 요청

위의 정보를 사용하여 예를 설명하겠습니다.

# plus_1.rb
def plus_1(n)
  n + 2
end
 
input = ARGV[0].to_i
puts(plus_1(input))
$ ruby plus_1.rb 1
3
  1. plus_1이라는 메서드를 정의합니다.
  2. 입력 검색("1" ) ARGV
  3. to_i 호출 "1"에서 , 1 반환
  4. 1 할당 지역 변수 input
  5. plus_1 호출 input이 있는 메소드 (1 ) 그 주장으로. 매개변수 n 이제 1 값을 전달합니다.
  6. + 호출 1의 메소드 인수 2 사용 , 결과를 반환합니다. 3
  7. 반환 3 5단계
  8. puts 호출
  9. to_s 호출 3에서 , "3" 반환
  10. "3" 통과 puts에 8단계에서 호출하여 문자열을 Stdout에 인쇄하는 부작용을 유발합니다. 그런 다음 nil을 반환합니다. .

설명이 100% 정확하지는 않지만 간단한 설명으로 충분합니다.

버그 해결

이제 프로그램이 어떻게 작동해야 하고 실제로 어떻게 작동하는지 배웠으므로 버그 찾기를 시작할 수 있습니다. 우리가 가지고 있는 정보를 가지고 위쪽(10단계부터 시작) 또는 아래쪽(1단계부터) 메서드 호출을 따라 버그를 검색할 수 있습니다. 이 경우 처음에 3을 반환한 메서드로 다시 추적하여 이를 수행할 수 있습니다. 이것은 1 + 2입니다. step 6에서 .

이것은 현실과 거리가 멀다!

물론 실제 디버깅이 예제에서 보여지는 것처럼 간단하지 않다는 것을 우리 모두 알고 있습니다. 실제 프로그램과 우리의 예 사이의 중요한 차이점은 크기입니다. 5줄 프로그램을 설명하기 위해 10단계를 사용했습니다. 작은 Rails 앱에는 몇 단계가 필요할까요? 실제 프로그램을 예제처럼 자세히 분석하는 것은 기본적으로 불가능합니다. 프로그램에 대한 자세한 이해 없이는 명백한 경로를 통해 버그를 추적할 수 없으므로 가정을 해야 합니다. 또는 추측.

정보는 비싸다

이미 눈치채셨겠지만 디버깅의 핵심 요소는 얼마나 많은 정보를 가지고 있는지입니다. 그러나 그 많은 정보를 검색하려면 무엇이 필요합니까? 보자:

# plus_1_with_tracing.rb
def plus_1(n)
  puts("n = #{n}")
  n + 2
end
 
raw_input = ARGV[0]
puts("raw_input: #{raw_input}")
input = raw_input.to_i
puts("input: #{input}")
 
result = plus_1(input)
puts("result of plus_1 #{result}")
 
puts(result)
$ ruby plus_1_with_tracing.rb 1
raw_input: 1
input: 1
n = 1
result of plus_1: 3
3

보시다시피 여기에서는 일부 변수의 값과 puts의 평가 순서라는 두 가지 유형의 정보만 얻습니다. (프로그램의 실행 순서를 의미합니다).

이 정보의 비용은 얼마입니까?

 def plus_1(n)
+  puts("n = #{n}")
   n + 2
 end
 
-input = ARGV[0].to_i
-puts(plus_1(input))
+raw_input = ARGV[0]
+puts("raw_input: #{raw_input}")
+input = raw_input.to_i
+puts("input: #{input}")
+
+result = plus_1(input)
+puts("result of plus_1: #{result}")
+
+puts(result)

4개의 puts을 추가해야 할 뿐만 아니라 그러나 값을 별도로 인쇄하려면 일부 값의 중간 상태에 액세스하기 위해 논리를 분할해야 합니다. 이 경우 8줄의 변경으로 내부 상태에 대해 4개의 추가 출력을 얻었습니다. 평균적으로 1줄의 출력에 대해 2줄의 변경 사항입니다! 그리고 변경 횟수는 프로그램의 크기에 따라 선형적으로 증가하므로 O(n)와 비교할 수 있습니다. 작업.

디버깅 비용이 많이 드는 이유는 무엇입니까?

우리 프로그램은 유지보수성, 성능, 단순성 등 많은 목표를 염두에 두고 작성할 수 있지만 일반적으로 "추적성"을 위한 것은 아닙니다. 연결된 메서드 호출 분할.

  • 정보를 더 많이 얻을수록 코드에 더 많은 추가/변경이 필요합니다.

그러나 일단 얻은 정보의 양이 일정 수준에 도달하면 효율적으로 처리할 수 없습니다. 따라서 정보를 필터링하거나 이해를 돕기 위해 레이블을 지정해야 합니다.

  • 정보가 정확할수록 코드에 더 많은 추가/변경이 필요합니다.

마지막으로, 작업은 버그(예:컨트롤러 대 모델 로직)에 따라 매우 다를 수 있는 코드베이스를 만지는 작업을 포함하기 때문에 자동화하기 어렵습니다. 코드베이스가 추적 친화적이더라도(예:"데미터의 법칙"을 엄격히 따름) 대부분의 경우 다른 변수/메서드 이름을 수동으로 입력해야 합니다.

(사실, Ruby에는 __method__와 같이 이를 방지하기 위한 몇 가지 트릭이 있습니다. . 하지만 여기에서 일을 복잡하게 만들지 맙시다.)

TracePoint:구세주

그러나 Ruby는 비용을 크게 줄일 수 있는 탁월한 도구를 제공합니다. TracePoint . 나는 당신의 대부분이 이미 그것에 대해 들어봤거나 이전에 사용했을 것입니다. 하지만 내 경험상 이 강력한 도구를 일상적인 디버깅 작업에서 사용하는 사람은 많지 않습니다.

정보를 빠르게 수집하는 방법을 알려드리겠습니다. 이번에는 기존 로직을 건드릴 필요가 없습니다. 그 앞에 몇 가지 코드만 있으면 됩니다.

TracePoint.trace(:call, :return, :c_call, :c_return) do |tp|
  event = tp.event.to_s.sub(/(.+(call|return))/, '\2').rjust(6, " ")
  message = "#{event} of #{tp.defined_class}##{tp.callee_id} on #{tp.self.inspect}"
  # if you call `return` on any non-return events, it'll raise error
  message += " => #{tp.return_value.inspect}" if tp.event == :return || tp.event == :c_return
  puts(message)
end
 
def plus_1(n)
  n + 2
end
 
input = ARGV[0].to_i
puts(plus_1(input))

코드를 실행하면 다음이 표시됩니다.

return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1>
  call of Module#method_added on Object
return of Module#method_added on Object => nil
  call of String#to_i on "1"
return of String#to_i on "1" => 1
  call of Object#plus_1 on main
return of Object#plus_1 on main => 3
  call of Kernel#puts on main
  call of IO#puts on #<IO:<STDOUT>>
  call of Integer#to_s on 3
return of Integer#to_s on 3 => "3"
  call of IO#write on #<IO:<STDOUT>>
3
return of IO#write on #<IO:<STDOUT>> => 2
return of IO#puts on #<IO:<STDOUT>> => nil
return of Kernel#puts on main => nil

우리 코드는 이제 훨씬 더 읽기 쉽습니다. 놀랍지 않습니까? 그것은 많은 세부 사항과 함께 대부분의 프로그램 실행을 인쇄합니다! 이전 실행 분석과 매핑할 수도 있습니다.

  1. plus_1이라는 메서드를 정의합니다.
  2. 입력 검색("1" ) ARGV
  3. to_i 호출 "1"에서 , 1 반환
  4. 1 할당 지역 변수 input
  5. plus_1 호출 input이 있는 메소드 (1 ) 그 주장으로. 매개변수 n 이제 1 값을 전달합니다.
  6. + 호출 1의 메소드 인수 2 사용 , 결과를 반환합니다. 3
  7. 반환 3 5단계
  8. puts 호출
  9. to_s 호출 3에서 , "3" 반환
  10. "3" 통과 puts에 8단계에서 호출하여 문자열을 Stdout에 인쇄하는 부작용을 유발합니다. 그런 다음 nil을 반환합니다. .
# ignore this, it's TracePoint tracing itself ;D
return of #<Class:TracePoint>#trace on TracePoint => #<TracePoint:c_return `trace'@plus_1_with_trace_point.rb:1>

  call of Module#method_added on Object         # 1. Defines a method called `plus_1`.
return of Module#method_added on Object => nil
  call of String#to_i on "1"                    # 3-1. Calls `to_i` on `"1"`
return of String#to_i on "1" => 1               # 3-2. which returns `1`
  call of Object#plus_1 on main                 # 5. Calls `plus_1` method with `input`(`1`) as its argument.
return of Object#plus_1 on main => 3            # 7. Returns `3` for step 5
  call of Kernel#puts on main                   # 8. Calls `puts`
  call of IO#puts on #<IO:<STDOUT>>
  call of Integer#to_s on 3                     # 9. Calls `to_s` on `3`, which returns `"3"`
return of Integer#to_s on 3 => "3"
  call of IO#write on #<IO:<STDOUT>>            # 10-1. Passes `"3"` to the `puts` call from step 8
                                                # 10-2. which triggers a side effect that prints the string to Stdout
3 # original output
return of IO#write on #<IO:<STDOUT>> => 2
return of IO#puts on #<IO:<STDOUT>> => nil
return of Kernel#puts on main => nil            # 10-3. And then it returns `nil`.

앞서 말한 것보다 더 자세하게 말할 수도 있습니다! 그러나 출력에서 ​​2, 4, 6단계가 누락되었음을 알 수 있습니다. 불행히도 TracePoint로 추적할 수 없습니다. 다음과 같은 이유로:

  • <올 시작="2">
  • 입력 검색("1" ) ARGV
    • ARGV 다음 [] 현재 call/c_call로 간주되지 않습니다.
  • <올 시작="4">
  • 1 할당 지역 변수 input
    • 현재 변수 할당에 대한 이벤트가 없습니다. line으로 (일종의) 추적할 수 있습니다. 이벤트 + 정규식이지만 정확하지 않습니다.
  • <올 시작="6">
  • + 호출 1의 메소드 인수 2 사용 , 결과를 반환합니다. 3
    • 내장 +와 같은 특정 메소드 호출 또는 속성 접근자 메서드는 현재 추적할 수 없습니다.

O(n)에서 O(log n)까지

이전 예에서 볼 수 있듯이 TracePoint를 적절히 사용하면 , 우리는 프로그램이 하는 일을 "알려주도록" 만들 수 있습니다. 이제 필요한 줄 수 때문에 TracePoint 프로그램의 크기에 따라 선형적으로 증가하지 않습니다. 전체 프로세스가 O(log(n)) 작업.

다음 단계

이 기사에서는 디버깅의 주요 어려움을 설명했습니다. TracePoint 게임 체인저가 될 수 있습니다. 하지만 TracePoint를 시도하면 지금 당장은 도움이 되기보다 실망스러울 것입니다.

TracePoint에서 가져온 정보의 양 , 당신은 곧 소음에 휩싸일 것입니다. 새로운 과제는 소음을 걸러내고 귀중한 정보를 남기는 것입니다. 예를 들어, 대부분의 경우 특정 모델이나 서비스 개체에만 관심이 있습니다. 이러한 경우 다음과 같이 수신자의 클래스로 호출을 필터링할 수 있습니다.

TracePoint.trace(:call) do |tp|
  next unless tp.self.is_a?(Order)
  # tracing logic
end

명심해야 할 또 다른 사항은 TracePoint 수만 번 평가할 수 있습니다. 이 규모에서 필터링 논리를 구현하는 방법은 앱의 성능에 큰 영향을 미칠 수 있습니다. 예를 들어 다음은 권장하지 않습니다.

TracePoint.trace(:call) do |tp|
  trace = caller[0]
  next unless trace.match?("app")
  # tracing logic
end

이 2가지 문제에 대해 일반적인 Ruby/Rails 애플리케이션에 유용한 상용구와 함께 발견한 몇 가지 트릭과 문제를 알려주는 또 다른 기사를 준비했습니다.

이 개념이 흥미롭다면 구현의 번거로움을 모두 숨길 수 있는 taping_device라는 보석도 만들었습니다.

결론

디버거와 추적은 둘 다 디버깅을 위한 훌륭한 도구이며 우리는 이를 수년 동안 사용해 왔습니다. 그러나 이 기사에서 설명했듯이 디버깅 프로세스 중에 이를 사용하려면 많은 수동 작업이 필요합니다. 그러나 TracePoint의 도움으로 , 그 중 많은 부분을 자동화하여 디버깅 성능을 높일 수 있습니다. 이제 TracePoint를 추가할 수 있기를 바랍니다. 디버깅 도구 상자로 이동하여 시도해 보십시오.