Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Debugging in Python 3.6: Better, Faster, Stronger

Debugging in Python 3.6: Better, Faster, Stronger

The new frame evaluation API was introduced to CPython in PEP 523 and it allows to specify a per-interpreter function pointer to handle the evaluation of frames. It might not be evident how to use this new feature in everyday life, but it’s quite easy to understand how to build a fast debugger based on it.

In this talk we are going to explain how standard way of debugging in Python works and how a new frame evaluation API may be useful for creating the fast debugger. Also we will consider why such fast debugging was not possible in the previous versions of Python. If someone hasn’t made a final decision to move to Python 3.6 this talk will provide some new reasons to do it.

Elizaveta Shashkova

July 09, 2017
Tweet

More Decks by Elizaveta Shashkova

Other Decks in Technology

Transcript

  1. Bio • Software developer of PyCharm IDE at JetBrains •

    Debugger • Saint Petersburg, Russia 2
  2. 8 def tracefunc(frame, event, arg): print(frame.f_lineno, event) return tracefunc sys.settrace(tracefunc)

    1 2 3 4 5 6 sys.settrace(tracefunc) - set system tracing function Tracing Function
  3. def foo(): friends = ["Bob", "Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 9
  4. def foo(): friends = ["Bob", "Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 10 1 call
  5. def foo(): friends = ["Bob", “Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 11 1 call 2 line
  6. def foo(): friends = ["Bob", "Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 12 1 call 2 line 3 line 4 line Hi Bob!
  7. def foo(): friends = ["Bob", "Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 13 1 call 2 line 3 line 4 line Hi Bob! 3 line 4 line Hi Tom!
  8. def foo(): friends = ["Bob", "Tom"] for f in friends:

    print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 1 2 3 4 5 6 7 8 9 Tracing Function 14 1 call 2 line 3 line 4 line Hi Bob! 3 line 4 line Hi Tom! 5 line 5 return
  9. Tracing Debugger 16 • Suspend program if breakpoint’s line equals

    frame.f_lineno • Handle events for stepping
  10. Performance def foo(): friends = ["Bob", "Tom"] for f in

    friends: print("Hi %s!” % f) return len(friends) sys.settrace(tracefunc) foo() 17 1 2 3 4 5 6 7 8 9 1 call 2 line 3 line 4 line Hi Bob! 3 line 4 line Hi Tom! 5 line 5 return
  11. Example 1 18 1 2 3 4 5 6 7

    8 9 def calculate(): sum = 0 for i in range(10 ** 7): sum += i return sum 18
  12. Example 1 19 1 2 3 4 5 6 7

    8 9 def calculate(): sum = 0 for i in range(10 ** 7): sum += i return sum def tracefunc(frame, event, arg): return tracefunc 19 19
  13. Frame Evaluation 30 def frame_eval(frame, exc): func_name = frame.f_code.co_name line_number

    = frame.f_lineno print(line_number, func_name) return _PyEval_EvalFrameDefault(frame, exc) 1 2 3 4 5 6 7 8 9
  14. Frame Evaluation 31 def frame_eval(frame, exc): func_name = frame.f_code.co_name f

    line_number = frame.f_lineno print(line_number, func_name) return _PyEval_EvalFrameDefault(frame, exc) 1 2 3 4 5 6 7 8 9
  15. Frame Evaluation 32 def frame_eval(frame, exc): func_name = frame.f_code.co_name line_number

    = frame.f_lineno f print(line_number, func_name) return _PyEval_EvalFrameDefault(frame, exc) 1 2 3 4 5 6 7 8 9
  16. Frame Evaluation 33 def frame_eval(frame, exc): func_name = frame.f_code.co_name line_number

    = frame.f_lineno print(line_number, func_name) f return _PyEval_EvalFrameDefault(frame, exc) 1 2 3 4 5 6 7 8 9
  17. Frame Evaluation 34 def frame_eval(frame, exc): func_name = frame.f_code.co_name line_number

    = frame.f_lineno print(line_number, func_name) return _PyEval_EvalFrameDefault(frame, exc) f 1 2 3 4 5 6 7 8 9
  18. def frame_eval(frame, exc): func_name = frame.f_code.co_name line_number = frame.f_lineno print(line_number,

    func_name) return _PyEval_EvalFrameDefault(frame, exc) def set_frame_eval(): state = PyThreadState_Get() state.interp.eval_frame = frame_eval 1 2 3 4 5 6 7 8 9 Frame Evaluation 35
  19. Example 36 1 2 3 4 5 6 7 8

    9 10 11 36 def first(): second() def second(): third() def third(): pass set_frame_eval() first()
  20. Example 1 first 4 second 7 third 37 1 2

    3 4 5 6 7 8 9 10 11 def first(): second() def second(): third() def third(): pass set_frame_eval() first()
  21. Custom Frame Evaluation 38 • It works! • Executed while

    entering a frame • Access to frame and code object
  22. Breakpoints 46 • Access to the whole code object •

    Insert breakpoint’s code into frame’s code
  23. Breakpoints 47 1 2 3 4 5 6 7 8

    9 def maximum(a, b): if a > b: return a else: return b
  24. Breakpoints 48 1 2 3 4 5 6 7 8

    9 def maximum(a, b): if a > b: return a # breakpoint else: return b
  25. def maximum(a, b): if a > b: return a #

    breakpoint else: return b Breakpoints breakpoint() 49 1 2 3 4 5 6 7 8 9
  26. Breakpoints 50 1 2 3 4 5 6 7 8

    9 def maximum(a, b): if a > b: breakpoint() return a # breakpoint else: return b
  27. Python Bytecode 51 1 2 3 4 5 6 7

    8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum) 51
  28. Python Bytecode 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1

    (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 52 1 2 3 4 5 6 7 8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum)
  29. Python Bytecode 53 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST

    1 (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 1 2 3 4 5 6 7 8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum) 53
  30. Python Bytecode 54 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST

    1 (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE f 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 1 2 3 4 5 6 7 8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum) 54
  31. Python Bytecode 55 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST

    1 (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 1 2 3 4 5 6 7 8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum)
  32. Python Bytecode 56 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST

    1 (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 1 2 3 4 5 6 7 8 9 def maximum(a, b): if a > b: return a else: return b import dis dis.dis(maximum)
  33. Python Bytecode 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1

    (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE 57
  34. 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4

    COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE Python Bytecode 58
  35. 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4

    COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE Python Bytecode 59
  36. 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4

    COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE Python Bytecode 60
  37. Bytecode Modification 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1

    (b) 4 COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE breakpoint() 61
  38. 63 • Insert breakpoint’s code • Update arguments and offsets

    • 200 lines in Python Bytecode Modification
  39. 2 0 LOAD_FAST 0 (a) 2 LOAD_FAST 1 (b) 4

    COMPARE_OP 4 (>) 6 POP_JUMP_IF_FALSE 12 3 8 LOAD_FAST 0 (a) 10 RETURN_VALUE 5 >> 12 LOAD_FAST 1 (b) 14 RETURN_VALUE 16 LOAD_CONST 0 (None) 18 RETURN_VALUE Bytecode Modification ?! 64 breakpoint()
  40. Breakpoint Bytecode 65 1 2 3 4 5 6 7

    8 9 def _stop_at_break(): # a lot of code here def breakpoint(): _stop_at_break() 0 LOAD_GLOBAL 0 2 CALL_FUNCTION 0 4 POP_TOP 6 LOAD_CONST 0 8 RETURN_VALUE
  41. Example 1 1 2 3 4 5 6 7 8

    9 def calculate(): sum = 0 for i in range(10 ** 7): sum += i return sum
  42. Example 2 72 1 2 3 4 5 6 7

    8 9 def foo(): pass def calculate(): sum = 0 for i in range(10 ** 7): foo() sum += i return sum
  43. PEP 523 75 • Expand PyCodeObject struct • co_extra -

    “scratch space” for the code object • Mark frames without breakpoints
  44. Mark Frames 76 1 2 3 4 5 6 7

    8 9 def frame_eval(frame, exc): flag = _PyCode_GetExtra(frame.f_code, index) if flag == NO_BREAKS_IN_FRAME: return _PyEval_EvalFrameDefault(frame, exc)
 
 # check for breakpoints ...
  45. Use cases 88 def maximum(a, b): if a > b:

    return a # breakpoint else: return b breakpoint() 1 2 3 4 5 6 7 8 9 88