File: _line_profiler.pyx

package info (click to toggle)
python-line-profiler 5.0.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,256 kB
  • sloc: python: 8,119; sh: 810; ansic: 297; makefile: 14
file content (1473 lines) | stat: -rw-r--r-- 57,674 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
# cython: language_level=3
# cython: infer_types=True
# cython: legacy_implicit_noexcept=True
# distutils: language=c++
# distutils: include_dirs = python25.pxd
r"""
This is the Cython backend used in :py:mod:`line_profiler.line_profiler`.

Ignore:
    # Standalone compile instructions for developers
    # Assuming the cwd is the repo root.
    cythonize --annotate --inplace \
        ./line_profiler/_line_profiler.pyx \
        ./line_profiler/timers.c
"""
from collections.abc import Callable
from functools import wraps
from sys import byteorder
import sys
cimport cython
from cpython.version cimport PY_VERSION_HEX
from libc.stdint cimport int64_t

from libcpp.unordered_map cimport unordered_map
import functools
import threading
import opcode
import os
import types
from warnings import warn
from weakref import WeakSet

from line_profiler._diagnostics import (
    WRAP_TRACE, SET_FRAME_LOCAL_TRACE, USE_LEGACY_TRACE)


NOP_VALUE: int = opcode.opmap['NOP']

# The Op code should be 2 bytes as stated in
# https://docs.python.org/3/library/dis.html
# if sys.version_info[0:2] >= (3, 11):
NOP_BYTES_LEN: int = 2
NOP_BYTES: bytes = NOP_VALUE.to_bytes(NOP_BYTES_LEN, byteorder=byteorder)

# This should be true for Python >=3.11a1
HAS_CO_QUALNAME: bool = hasattr(types.CodeType, 'co_qualname')

# Can't line-profile Cython in 3.12 since the old C API was upended
# without an appropriate replacement (which only came in 3.13);
# see also:
# https://cython.readthedocs.io/en/latest/src/tutorial/profiling_tutorial.html
_CAN_USE_SYS_MONITORING = PY_VERSION_HEX >= 0x030c00b1
CANNOT_LINE_TRACE_CYTHON = (
    _CAN_USE_SYS_MONITORING and PY_VERSION_HEX < 0x030d00b1)

if not (USE_LEGACY_TRACE or _CAN_USE_SYS_MONITORING):
    # Shouldn't happen since we're already checking the existence of
    # `sys.monitoring` in `line_profiler._diagnostics`, but just to be
    # absolutely sure...
    warn("`sys.monitoring`-based line profiling selected but unavailable "
         f"in Python {sys.version}; falling back to the legacy trace system")
    USE_LEGACY_TRACE = True

# long long int is at least 64 bytes assuming c99
ctypedef unsigned long long int uint64
ctypedef long long int int64

cdef extern from "Python_wrapper.h":
    ctypedef struct PyObject
    ctypedef struct PyCodeObject
    ctypedef struct PyFrameObject
    ctypedef long long PY_LONG_LONG
    ctypedef int (*Py_tracefunc)(
        object self, PyFrameObject *py_frame, int what, PyObject *arg)

    cdef PyCodeObject* PyFrame_GetCode(PyFrameObject* frame)
    cdef int PyCode_Addr2Line(PyCodeObject *co, int byte_offset)

    cdef void PyEval_SetTrace(Py_tracefunc func, object arg)
    cdef PyObject* PyObject_Call(
        PyObject *callable, PyObject *args, PyObject *kwargs) except *

    # They're actually #defines, but whatever.
    cdef int PyTrace_CALL
    cdef int PyTrace_EXCEPTION
    cdef int PyTrace_LINE
    cdef int PyTrace_RETURN
    cdef int PyTrace_OPCODE
    cdef int PyTrace_C_CALL
    cdef int PyTrace_C_EXCEPTION
    cdef int PyTrace_C_RETURN

    cdef int PyFrame_GetLineNumber(PyFrameObject *frame)

cdef extern from "c_trace_callbacks.c":  # Legacy tracing
    ctypedef unsigned long long Py_uintptr_t

    ctypedef struct TraceCallback:
        Py_tracefunc c_tracefunc
        PyObject *c_traceobj

    cdef TraceCallback *alloc_callback() except *
    cdef void free_callback(TraceCallback *callback)
    cdef void populate_callback(TraceCallback *callback)
    cdef void restore_callback(TraceCallback *callback)
    cdef int call_callback(
        PyObject *disabler, TraceCallback *callback,
        PyFrameObject *py_frame, int what, PyObject *arg)
    cdef void set_local_trace(PyObject *manager, PyFrameObject *py_frame)
    cdef Py_uintptr_t monitoring_restart_version()

cdef extern from "timers.c":
    PY_LONG_LONG hpTimer()
    double hpTimerUnit()

cdef struct LineTime:
    int64 code
    int lineno
    PY_LONG_LONG total_time
    long nhits

cdef struct LastTime:
    int f_lineno
    PY_LONG_LONG time


cdef inline int64 compute_line_hash(uint64 block_hash, uint64 linenum):
    """
    Compute the hash used to store each line timing in an unordered_map.
    This is fairly simple, and could use some improvement since linenum
    isn't technically random, however it seems to be good enough and
    fast enough for any practical purposes.
    """
    # linenum doesn't need to be int64 but it's really a temporary value
    # so it doesn't matter
    return block_hash ^ linenum


cdef inline object multibyte_rstrip(bytes bytecode):
    """
    Returns:
        result (tuple[bytes, int])
        - First item is the bare unpadded bytecode
        - Second item is the number of :py:data:`NOP_BYTES`
          ``bytecode`` has been padded with
    """
    npad: int = 0
    nop_len: int = -NOP_BYTES_LEN
    nop_bytes: bytes = NOP_BYTES
    unpadded: bytes = bytecode
    while unpadded.endswith(nop_bytes):
        unpadded = unpadded[:nop_len]
        npad += 1
    return (unpadded, npad)


cdef inline object get_current_callback(int tool_id, int event_id):
    """
    Note:
        Unfortunately there's no public API for directly retrieving
        the current callback, no matter on the Python side or the C
        side.  This may become a performance bottleneck...
    """
    mon = sys.monitoring
    cdef object register = mon.register_callback
    cdef object result = register(tool_id, event_id, None)
    if result is not None:
        register(tool_id, event_id, result)
    return result


def label(code):
    """
    Return a ``(filename, first_lineno, _name)`` tuple for a given code
    object.

    This is the similar labelling as used by the :py:mod:`cProfile`
    module in Python 2.5.

    Note:
        In Python >= 3.11 we return qualname for ``_name``.
        In older versions of Python we just return name.
    """
    if isinstance(code, str):
        return ('~', 0, code)  # built-in functions ('~' sorts at the end)
    else:
        if HAS_CO_QUALNAME:
            return (code.co_filename, code.co_firstlineno, code.co_qualname)
        else:
            return (code.co_filename, code.co_firstlineno, code.co_name)


def find_cython_source_file(cython_func):
    """
    Resolve the absolute path to a Cython function's source file.

    Returns:
        result (str | None)
            Cython source file if found, else :py:data:`None`.
    """
    try:
        compiled_module = cython_func.__globals__['__file__']
    except KeyError:  # Shouldn't happen...
        return None
    rel_source_file = cython_func.__code__.co_filename
    if os.path.isabs(rel_source_file):
        if os.path.isfile(rel_source_file):
            return rel_source_file
        return None
    prefix = os.path.dirname(compiled_module)
    while True:
        source_file = os.path.join(prefix, rel_source_file)
        if os.path.isfile(source_file):
            return source_file
        next_prefix = os.path.dirname(prefix)
        if next_prefix == prefix:  # At the file-system root
            return None
        prefix = next_prefix


def disable_line_events(trace_func: Callable) -> Callable:
    """
    Returns:
        trace_func (Callable)
            If it is a wrapper created by
            :py:attr:`_LineProfilerManager.wrap_local_f_trace`;
            ``trace_func.disable_line_events`` is also set to true
        wrapper (Callable)
            Otherwise, a thin wrapper around ``trace_func()`` which
            withholds line events.

    Note:
        This is for when a frame-local :py:attr:`~frame.f_trace`
        disables :py:attr:`~frame.f_trace_lines` -- we would like to
        keep line events enabled (so that line profiling works) while
        "unsubscribing" the trace function from it.
    """
    @wraps(trace_func)
    def wrapper(frame, event, arg):
        if event == 'line':
            return
        return trace_func(frame, event, arg)

    try:  # Disable the wrapper directly
        if hasattr(trace_func, '__line_profiler_manager__'):
            trace_func.disable_line_events = True
            return trace_func
    except AttributeError:
        pass
    return wrapper


cpdef _code_replace(func, co_code):
    """
    Implements :py:mod:`~code.replace` for Python < 3.8
    """
    try:
        code = func.__code__
    except AttributeError:
        code = func.__func__.__code__
    if hasattr(code, 'replace'):
        # python 3.8+
        code = code.replace(co_code=co_code)
    else:
        # python <3.8
        co = code
        code = type(code)(co.co_argcount, co.co_kwonlyargcount,
                        co.co_nlocals, co.co_stacksize, co.co_flags,
                        co_code, co.co_consts, co.co_names,
                        co.co_varnames, co.co_filename, co.co_name,
                        co.co_firstlineno, co.co_lnotab, co.co_freevars,
                        co.co_cellvars)
    return code


cpdef int _patch_events(int events, int before, int after):
    """
    Patch ``events`` based on the differences between ``before`` and
    ``after``.

    Example:
        >>> events = 0b110000
        >>> before = 0b101101
        >>> after = 0b_001011  # Additions: 0b10, deletions: 0b100100
        >>> assert _patch_events(events, before, after) == 0b010010
    """
    cdef int all_set_bits, plus, minus
    all_set_bits = before | after
    plus = all_set_bits - before
    minus = all_set_bits - after
    return ((events | minus) - minus) | plus


# Note: this is a regular Python class to allow easy pickling.
class LineStats(object):
    """
    Object to encapsulate line-profile statistics.

    Attributes:

        timings (dict[tuple[str, int, str], \
list[tuple[int, int, int]]]):
            Mapping from ``(filename, first_lineno, function_name)`` of
            the profiled function to a list of
            ``(lineno, nhits, total_time)`` tuples for each profiled
            line. ``total_time`` is an integer in the native units of
            the timer.

        unit (float):
            The number of seconds per timer unit.
    """
    def __init__(self, timings, unit):
        self.timings = timings
        self.unit = unit


cdef class _SysMonitoringState:
    """
    Another helper object for managing the thread-local state.

    Note:
        Documentations are for reference only, and all APIs are to be
        considered private and subject to change.
    """
    cdef int tool_id
    cdef object name  # type: str | None
    # type: dict[int, Callable | None], int = event id
    cdef dict callbacks
    # type: dict[int, set[tuple[code, Unpack[tuple]]]],
    # int = event id, tuple = <locational info>
    cdef dict disabled
    cdef int events
    cdef Py_uintptr_t restart_version

    if _CAN_USE_SYS_MONITORING:
        line_tracing_event_set = (  # type: ClassVar[FrozenSet[int]]
            frozenset({sys.monitoring.events.LINE,
                       sys.monitoring.events.PY_RETURN,
                       sys.monitoring.events.PY_YIELD,
                       sys.monitoring.events.RAISE,
                       sys.monitoring.events.RERAISE}))
        line_tracing_events = (sys.monitoring.events.LINE
                               | sys.monitoring.events.PY_RETURN
                               | sys.monitoring.events.PY_YIELD
                               | sys.monitoring.events.RAISE
                               | sys.monitoring.events.RERAISE)
    else:
        line_tracing_event_set = frozenset({})
        line_tracing_events = 0

    def __init__(self, tool_id: int):
        self.tool_id = tool_id
        self.name = None
        self.callbacks = {}
        self.disabled = {}
        self.events = 0  # NO_EVENTS
        self.restart_version = monitoring_restart_version()

    cpdef register(self, object handle_line,
                   object handle_return, object handle_yield,
                   object handle_raise, object handle_reraise):
        # Note: only activating `sys.monitoring` line events for the
        # profiled code objects in `LineProfiler.add_function()` may
        # seem like an obvious optimization, but:
        # - That adds complexity and muddies the logic, because
        #   `.set_local_events()` can only be called if the tool id is
        #   in use (e.g. activated via `.use_tool_id()`), and
        # - That doesn't result in much (< 2%) performance improvement
        #   in tests
        mon = sys.monitoring

        # Set prior state
        self.name = mon.get_tool(self.tool_id)
        if self.name is None:
            self.events = mon.events.NO_EVENTS
        else:
            self.events = mon.get_events(self.tool_id)
            mon.free_tool_id(self.tool_id)
        mon.use_tool_id(self.tool_id, 'line_profiler')
        mon.set_events(self.tool_id, self.events | self.line_tracing_events)

        # Register tracebacks
        for event_id, callback in [
                (mon.events.LINE, handle_line),
                (mon.events.PY_RETURN, handle_return),
                (mon.events.PY_YIELD, handle_yield),
                (mon.events.RAISE, handle_raise),
                (mon.events.RERAISE, handle_reraise)]:
            self.callbacks[event_id] = mon.register_callback(
                self.tool_id, event_id, callback)

    cpdef deregister(self):
        mon = sys.monitoring
        cdef dict wrapped_callbacks = self.callbacks

        # Restore prior state
        mon.free_tool_id(self.tool_id)
        if self.name is not None:
            mon.use_tool_id(self.tool_id, self.name)
            mon.set_events(self.tool_id, self.events)
            self.name = None
            self.events = mon.events.NO_EVENTS

        # Reset tracebacks
        while wrapped_callbacks:
            mon.register_callback(self.tool_id, *wrapped_callbacks.popitem())

    cdef void call_callback(self, int event_id, object code,
                            object loc_args, object other_args):
        """
        Call the appropriate stored callback.  Also take care of the
        restoration of :py:mod:`sys.monitoring` callbacks, tool-ID lock,
        and events should they be unset.

        Note:
            * This is deliberately made a non-traceable C method so that
              we don't fall info infinite recursion.
            * ``loc_args`` and ``other_args`` should be tuples.
        """
        mon = sys.monitoring
        cdef PyObject *result
        cdef object callback  # type: Callable | None
        cdef object callback_after  # type: Callable | None
        cdef object code_location  # type: tuple[code, Unpack[tuple]]
        cdef object arg_tuple  # type: tuple[code, Unpack[tuple]]
        cdef object disabled  # type: set[tuple[code, Unpack[tuple]]]
        cdef int ev_id, events_before
        cdef Py_uintptr_t version = monitoring_restart_version()
        cdef dict callbacks_before = {}

        # If we've restarted events, clear the `.disabled` registry
        if version != self.restart_version:
            self.restart_version = version
            self.disabled.clear()

        # Call the wrapped callback where suitable
        callback = self.callbacks.get(event_id)
        if callback is None:  # No cached callback
            return
        code_location = (code,) + loc_args
        disabled = self.disabled.setdefault(event_id, set())
        if code_location in disabled:  # Events 'disabled' for the loc
            return
        if not (self.events  # Callback should not receive the event
                | mon.get_local_events(self.tool_id, code)) & event_id:
            return

        for ev_id in self.line_tracing_event_set:
            callbacks_before[ev_id] = get_current_callback(self.tool_id, ev_id)

        arg_tuple = code_location + other_args
        try:
            events_before = mon.get_events(self.tool_id)
            result = PyObject_Call(
                <PyObject *>callback, <PyObject *>arg_tuple, NULL)
        else:
            # Since we can't actually disable the event (or line
            # profiling will be interrupted), just mark the location so
            # that we stop calling the cached callback until the next
            # time `sys.monitoring.restart_events()` is called
            if result == <PyObject *>(mon.DISABLE):
                disabled.add(code_location)
        finally:
            # Update the events
            self.events = _patch_events(
                self.events, events_before, mon.get_events(self.tool_id))
            # If the wrapped callback has changed:
            register = mon.register_callback
            for ev_id, callback in callbacks_before.items():
                # - Restore the `sys.monitoring` callback
                callback_after = register(self.tool_id, ev_id, callback)
                # - Remember the updated callback in `self.callbacks`
                if callback is not callback_after:
                    self.callbacks[ev_id] = callback_after
            # Reset the tool ID lock if released
            if not mon.get_tool(self.tool_id):
                mon.use_tool_id(self.tool_id, 'line_profiler')
            # Restore the `sys.monitoring` events if unset
            mon.set_events(self.tool_id,
                           self.events | self.line_tracing_events)


cdef class _LineProfilerManager:
    """
    Helper object for managing the thread-local state.
    Supports being called with the same signature as a legacy trace
    function (see :py:func:`sys.settrace`).

    Other methods of interest:

    :py:meth:`~.handle_line_event`
        Callback for |LINE|_ events
    :py:meth:`~.handle_return_event`
        Callback for |PY_RETURN|_ events
    :py:meth:`~.handle_yield_event`
        Callback for |PY_YIELD|_ events
    :py:meth:`~.handle_raise_event`
        Callback for |RAISE|_ events
    :py:meth:`~.handle_reraise_event`
        Callback for |RERAISE|_ events

    Note:
        Documentations are for reference only, and all APIs are to be
        considered private and subject to change.

    .. |LINE| replace:: :py:attr:`!sys.monitoring.events.LINE`
    .. |PY_RETURN| replace:: :py:attr:`!sys.monitoring.events.PY_RETURN`
    .. |PY_YIELD| replace:: :py:attr:`!sys.monitoring.events.PY_YIELD`
    .. |RAISE| replace:: :py:attr:`!sys.monitoring.events.RAISE`
    .. |RERAISE| replace:: :py:attr:`!sys.monitoring.events.RERAISE`
    .. _LINE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-LINE
    .. _PY_RETURN: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-PY_RETURN
    .. _PY_YIELD: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-PY_YIELD
    .. _RAISE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-RAISE
    .. _RERAISE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-RERAISE
    """
    cdef TraceCallback *legacy_callback
    cdef _SysMonitoringState mon_state
    cdef public object active_instances  # type: set[LineProfiler]
    cdef int _wrap_trace
    cdef int _set_frame_local_trace
    cdef int recursion_guard

    def __init__(
            self, tool_id: int, wrap_trace: bool, set_frame_local_trace: bool):
        """
        Arguments:
            tool_id (int)
                Tool ID for use with :py:mod:`sys.monitoring`.
            wrap_trace (bool)
                Whether to wrap around legacy and
                :py:mod:`sys.monitoring` trace functions and call them.
            set_frame_local_trace (bool)
                If using the legacy trace system, whether to insert the
                instance as a frame's :py:attr:`~frame.f_trace` upon
                entering a function scope.

        See also:
            :py:class:`~.LineProfiler`
        """
        self.legacy_callback = NULL
        self.mon_state = _SysMonitoringState(tool_id)

        self.active_instances = set()
        self.wrap_trace = wrap_trace
        self.set_frame_local_trace = set_frame_local_trace
        self.recursion_guard = 0

    @cython.profile(False)
    def __call__(self, frame: types.FrameType, event: str, arg):
        """
        Calls |legacy_trace_callback|_.  If :py:func:`sys.gettrace`
        returns this instance, replaces the default C-level trace
        function |trace_trampoline|_ with |legacy_trace_callback|_ to
        reduce overhead.

        Returns:
            self (_LineProfilerManager):
                This instance.

        .. |legacy_trace_callback| replace:: \
:c:func:`!legacy_trace_callback`
        .. |trace_trampoline| replace:: :c:func:`!trace_trampoline`
        .. _legacy_trace_callback: https://github.com/pyutils/\
line_profiler/blob/main/line_profiler/_line_profiler.pyx
        .. _trace_trampoline: https://github.com/python/cpython/blob/\
6cb20a219a860eaf687b2d968b41c480c7461909/Python/sysmodule.c#L1124
        """
        cdef int what = {'call': PyTrace_CALL,
                         'exception': PyTrace_EXCEPTION,
                         'line': PyTrace_LINE,
                         'return': PyTrace_RETURN,
                         'opcode': PyTrace_OPCODE}[event]
        if not self.recursion_guard:
            # Prevent recursion (e.g. when `.wrap_trace` and
            # `.set_frame_local_trace` are both true)
            legacy_trace_callback(self, <PyFrameObject *>frame,
                                  what, <PyObject *>arg)
        # Set the C-level trace callback back to
        # `legacy_trace_callback()` where appropriate, so that future
        # calls can bypass this `.__call__()` method
        if sys.gettrace() is self:
            PyEval_SetTrace(legacy_trace_callback, self)
        return self

    def wrap_local_f_trace(self, trace_func: Callable) -> Callable:
        """
        Arguments:
            trace_func (Callable[[frame, str, Any], Any])
                Frame-local trace function, presumably set by another
                global trace function.

        Returns:
            wrapper (Callable[[frame, str, Any], Any])
                Thin wrapper around ``trace_func()`` which calls it,
                calls the instance, then returns the return value of
                ``trace_func()``.  This helps prevent other frame-local
                trace functions from displacing the instance when its
                :py:attr:`~.set_frame_local_trace` is true.

        Note:
            * The ``.__line_profiler_manager__`` attribute of the
              returned wrapper is set to the instance.
            * Line events are not passed to the wrapped callable if
              ``wrapper.disable_line_events`` is set to true.
        """
        @wraps(trace_func)
        def wrapper(frame, event, arg):
            if wrapper.disable_line_events and event == 'line':
                result = None
            else:
                result = trace_func(frame, event, arg)
            self(frame, event, arg)
            return result

        wrapper.__line_profiler_manager__ = self
        wrapper.disable_line_events = False
        try:  # Unwrap the wrapper
            if trace_func.__line_profiler_manager__ is self:
                trace_func = trace_func.__wrapped__
        except AttributeError:
            pass
        return wrapper

    # If we allowed these `sys.monitoring` callbacks to be profiled
    # (i.e. to emit line events), we may fall into an infinite recusion;
    # so disable profiling for them pre-emptively

    @cython.profile(False)
    cpdef handle_line_event(self, object code, int lineno):
        """
        Line-event (|LINE|_) callback passed to
        :py:func:`sys.monitoring.register_callback`.

        .. |LINE| replace:: :py:attr:`!sys.monitoring.events.LINE`
        .. _LINE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-LINE
        """
        self._base_callback(
            1, sys.monitoring.events.LINE, code, lineno, (lineno,), ())

    @cython.profile(False)
    cpdef handle_return_event(
            self, object code, int instruction_offset, object retval):
        """
        Return-event (|PY_RETURN|_) callback passed to
        :py:func:`sys.monitoring.register_callback`.

        .. |PY_RETURN| replace:: \
:py:attr:`!sys.monitoring.events.PY_RETURN`
        .. _PY_RETURN: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-PY_RETURN
        """
        self._handle_exit_event(
            sys.monitoring.events.PY_RETURN, code, instruction_offset, retval)

    @cython.profile(False)
    cpdef handle_yield_event(
            self, object code, int instruction_offset, object retval):
        """
        Yield-event (|PY_YIELD|_) callback passed to
        :py:func:`sys.monitoring.register_callback`.

        .. |PY_YIELD| replace:: \
:py:attr:`!sys.monitoring.events.PY_YIELD`
        .. _PY_YIELD: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-PY_YIELD
        """
        self._handle_exit_event(
            sys.monitoring.events.PY_YIELD, code, instruction_offset, retval)

    @cython.profile(False)
    cpdef handle_raise_event(
            self, object code, int instruction_offset, object exception):
        """
        Raise-event (|RAISE|_) callback passed to
        :py:func:`sys.monitoring.register_callback`.

        .. |RAISE| replace:: :py:attr:`!sys.monitoring.events.RAISE`
        .. _RAISE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-RAISE
        """
        self._handle_exit_event(
            sys.monitoring.events.RAISE, code, instruction_offset, exception)

    @cython.profile(False)
    cpdef handle_reraise_event(
            self, object code, int instruction_offset, object exception):
        """
        Re-raise-event (|RERAISE|_) callback passed to
        :py:func:`sys.monitoring.register_callback`.

        .. |RERAISE| replace:: :py:attr:`!sys.monitoring.events.RERAISE`
        .. _RERAISE: https://docs.python.org/3/library/\
sys.monitoring.html#monitoring-event-RERAISE
        """
        self._handle_exit_event(
            sys.monitoring.events.RERAISE, code, instruction_offset, exception)

    cdef void _handle_exit_event(
            self, int event_id, object code, int offset, object obj):
        """
        Base for the frame-exit-event (e.g. via returning or yielding)
        callbacks passed to :py:func:`sys.monitoring.register_callback`.

        Note:
            This is deliberately made a non-traceable C method so that
            we don't fall info infinite recursion.
        """
        cdef int lineno = PyCode_Addr2Line(<PyCodeObject*>code, offset)
        self._base_callback(0, event_id, code, lineno, (offset,), (obj,))

    cdef void _base_callback(
            self, int is_line_event, int event_id, object code, int lineno,
            object loc_args, object other_args):
        """
        Base for the various callbacks passed to
        :py:func:`sys.monitoring.register_callback`.

        Note:
            * This is deliberately made a non-traceable C method so that
              we don't fall info infinite recursion.
            * ``loc_args`` and ``other_args`` should be tuples.
        """
        inner_trace_callback(
            is_line_event, self.active_instances, code, lineno)
        if self._wrap_trace:
            self.mon_state.call_callback(event_id, code, loc_args, other_args)

    cpdef _handle_enable_event(self, prof):
        cdef TraceCallback* legacy_callback
        instances = self.active_instances
        already_active = bool(instances)
        instances.add(prof)
        if already_active:
            return
        if USE_LEGACY_TRACE:
            legacy_callback = alloc_callback()
            populate_callback(legacy_callback)
            self.legacy_callback = legacy_callback
            PyEval_SetTrace(legacy_trace_callback, self)
        else:
            self.mon_state.register(self.handle_line_event,
                                    self.handle_return_event,
                                    self.handle_yield_event,
                                    self.handle_raise_event,
                                    self.handle_reraise_event)

    cpdef _handle_disable_event(self, prof):
        cdef TraceCallback* legacy_callback
        instances = self.active_instances
        instances.discard(prof)
        if instances:
            return
        # Only use the legacy trace-callback system if Python < 3.12 or
        # if explicitly requested with `LINE_PROFILER_CORE=legacy`;
        # otherwise, use `sys.monitoring`
        # see: https://docs.python.org/3/library/sys.monitoring.html
        if USE_LEGACY_TRACE:
            legacy_callback = self.legacy_callback
            restore_callback(legacy_callback)
            free_callback(legacy_callback)
            self.legacy_callback = NULL
        else:
            self.mon_state.deregister()

    property wrap_trace:
        def __get__(self):
            return bool(self._wrap_trace)
        def __set__(self, wrap_trace):
            self._wrap_trace = 1 if wrap_trace else 0

    property set_frame_local_trace:
        def __get__(self):
            return bool(self._set_frame_local_trace)
        def __set__(self, set_frame_local_trace):
            # Note: as noted in `LineProfiler.__doc__`, there is no
            # point in tempering with `.f_trace` when using
            # `sys.monitoring`... so just set it to false
            self._set_frame_local_trace = (
                1 if set_frame_local_trace and USE_LEGACY_TRACE else 0)


cdef class LineProfiler:
    """
    Time the execution of lines of Python code.

    This is the Cython base class for
    :py:class:`line_profiler.line_profiler.LineProfiler`.

    Arguments:
        *functions (function)
            Function objects to be profiled.
        wrap_trace (bool | None)
            What to do for existing :py:mod:`sys` trace callbacks when
            an instance is :py:meth:`.enable`-ed:

            :py:data:`True`:
                *Wrap around* said callbacks: when our profiling trace
                callbacks run, they call the corresponding existing
                callbacks (where applicable).
            :py:data:`False`:
                *Suspend* said callbacks as long as
                :py:class:`LineProfiler` instances are enabled.
            :py:data:`None` (default):
                For the first instance created, resolves to

                :py:data:`True`
                    If the environment variable
                    :envvar:`LINE_PROFILER_WRAP_TRACE` is set to any of
                    ``{'1', 'on', 'true', 'yes'}`` (case-insensitive).

                :py:data:`False`
                    Otherwise.

                If other instances already exist, the value is inherited
                therefrom.

            In any case, when all instances are :py:meth:`.disable`-ed,
            the :py:mod:`sys` trace system is restored to the state from
            when the first instance was :py:meth:`.enable`-ed.
            See the :ref:`caveats <warning-trace-caveats>` and also the
            :ref:`extra explanation <note-wrap_trace>`.
        set_frame_local_trace (bool | None)
            When using the
            :ref:`"legacy" trace system <note-backends>`), what to do
            when entering a function or code block (i.e. an event of
            type :c:data:`PyTrace_CALL` or ``'call'`` is encountered)
            when an instance is :py:meth:`.enable`-ed:

            :py:data:`True`:
                Set the frame's :py:attr:`~frame.f_trace` to
                an object associated with the profiler.
            :py:data:`False`:
                Don't do so.
            :py:data:`None` (default):
                For the first instance created, resolves to

                :py:data:`True`
                    If the environment variable
                    :envvar:`LINE_PROFILER_SET_FRAME_LOCAL_TRACE` is set
                    to any of ``{'1', 'on', 'true', 'yes'}``
                    (case-insensitive).

                :py:data:`False`
                    Otherwise.

                If other instances already exist, the value is inherited
                therefrom.

            See the :ref:`caveats <warning-trace-caveats>` and also the
            :ref:`extra explanation <note-set_frame_local_trace>`.

    Example:
        >>> import copy
        >>> import line_profiler
        >>> # Create a LineProfiler instance
        >>> self = line_profiler.LineProfiler()
        >>> # Wrap a function
        >>> copy_fn = self(copy.copy)
        >>> # Call the function
        >>> copy_fn(self)
        >>> # Inspect internal properties
        >>> self.functions
        >>> self.c_last_time
        >>> self.c_code_map
        >>> self.code_map
        >>> self.last_time
        >>> # Print stats
        >>> self.print_stats()

    .. _warning-trace-caveats:

    Warning:
        * Setting :py:attr:`.wrap_trace` and/or
          :py:attr:`.set_frame_local_trace` helps with using
          :py:class:`LineProfiler` cooperatively with other tools, like
          coverage and debugging tools, especially when using the
          :ref:`"legacy" trace system <note-backends>`.  However, these
          parameters should be considered **experimental** and to be
          used at one's own risk -- because tools generally assume that
          they have sole control over system-wide tracing (if using
          "legacy" tracing), or at least over the
          :py:mod:`sys.monitoring` tool ID it acquired.
        * When setting :py:attr:`.wrap_trace` and
          :py:attr:`.set_frame_local_trace`, they are set process-wide
          for all instances.

    .. _note-backends:

    Note:
        There are two "cores"/"backends" for :py:class:`LineProfiler`
        between which users can choose:

        ``'new'``, ``'sys.monitoring'``, or ``'sysmon'``
            Use :py:mod:`sys.monitoring` events and callbacks.  Only
            available on (and is the default for) Python 3.12 and newer.
        ``'old'``, ``'legacy'``, or ``'ctrace'``
            Use the `"legacy" trace system`_ (:py:func:`sys.gettrace`,
            :py:func:`sys.settrace`, and :c:func:`PyEval_SetTrace`).
            Default for Python < 3.12.

        Where both cores are available, the user can choose between the
        two by supplying a suitable value to the environment variable
        :envvar:`LINE_PROFILER_CORE`.

    .. _note-wrap_trace:

    Note:
        More on :py:attr:`.wrap_trace`:

        * In general, Python allows for trace callbacks to unset
          themselves, either intentionally (via
          ``sys.settrace(None)`` or
          ``sys.monitoring.register_callback(..., None)``) or if they
          error out.  If the wrapped/cached trace callbacks do so,
          profiling would continue, but:

          * The cached callbacks are cleared and are no longer called,
            and
          * The trace callbacks are unset when all profiler instances
            are :py:meth:`.disable`-ed.
        * If a wrapped/cached frame-local
          :ref:`"legacy" trace callable <note-backends>`
          (:py:attr:`~frame.f_trace`) sets
          :py:attr:`~frame.f_trace_lines` to false in a frame to
          disable local line events, :py:attr:`~.frame.f_trace_lines`
          is restored (so that profiling can continue), but said
          callable will no longer receive said events.
        * Likewise, wrapped/cached :py:mod:`sys.monitoring` callbacks
          can also disable events:

          * At *specific code locations* by returning
            :py:data:`sys.monitoring.DISABLE`;
          * By calling :py:func:`sys.monitoring.set_events` and
            changing the *global event set*; or
          * By calling :py:func:`sys.monitoring.register_callback` and
            *replacing itself* with alternative callbacks (or
            :py:data:`None`).

          When that happens, said disabling acts are again suitably
          intercepted so that line profiling continues, but:

          * Said callbacks will no longer receive the corresponding
            events, and
          * The :py:mod:`sys.monitoring` callbacks and event set are
            updated correspondingly when all profiler instances are
            :py:meth:`.disable`-ed.

          Note that:

          * As with when line profiling is not used, if 
            :py:func:`sys.monitoring.restart_events` is called, the list
            of code locations where events are suppressed is cleared,
            and the wrapped/cached callbacks will once again receive
            events from the
            previously-:py:data:`~sys.monitoring.DISABLE`-d locations.
          * Callbacks which only listen to and alter code-object-local
            events (via :py:func:`sys.monitoring.set_local_events`) do
            not interfere with line profiling, and such changes are
            therefore not intercepted.

    .. _note-set_frame_local_trace:

    Note:
        More on :py:attr:`.set_frame_local_trace`:

        * Since frame-local trace functions is no longer a useful
          concept in the new :py:mod:`sys.monitoring`-based system
          (see also the :ref:`Note on "cores" <note-backends>`), the
          parameter/attribute always resolves to :py:data:`False` when
          using the new :py:class:`LineProfiler` core.
        * With the :ref:`"legacy" trace system <note-backends>`, when
          :py:class:`LineProfiler` instances are :py:meth:`.enable`-ed,
          :py:func:`sys.gettrace` returns an object which manages
          profiling on the thread between all active profiler instances.
          Said object has the same call signature as callables that
          :py:func:`sys.settrace` takes, so that pure-Python code which
          temporarily overrides the trace callable (e.g.
          :py:meth:`doctest.DocTestRunner.run`) can function with
          profiling.  After the object is restored with
          :py:func:`sys.settrace` by said code:

          * If :py:attr:`set_frame_local_trace` is true, line
            profiling resumes *immediately*, because the object has
            already been set to the frame's :py:attr:`~frame.f_trace`.
          * However, if :py:attr:`set_frame_local_trace` is false,
            line profiling only resumes *upon entering another code
            block* (e.g. by calling a callable), because trace
            callables set via :py:func:`sys.settrace` is only called
            for ``'call'`` events (see the `C implementation`_ of
            :py:mod:`sys`).

    .. _C implementation: https://github.com/python/cpython/blob/\
6cb20a219a860eaf687b2d968b41c480c7461909/Python/sysmodule.c#L1124
    .. _"legacy" trace system: https://github.com/python/cpython/blob/\
3.13/Python/legacy_tracing.c
    """
    cdef unordered_map[int64, unordered_map[int64, LineTime]] _c_code_map
    # Mapping between thread-id and map of LastTime
    cdef unordered_map[int64, unordered_map[int64, LastTime]] _c_last_time
    cdef public list functions
    cdef public dict code_hash_map, dupes_map
    cdef public double timer_unit
    cdef public object threaddata

    # These are shared between instances and threads
    if _CAN_USE_SYS_MONITORING:
        # Note: just in case we ever need to override this, e.g. for
        # testing
        tool_id = sys.monitoring.PROFILER_ID  # type: ClassVar[int]
    else:
        # Note: the value doesn't matter here... but set it to be
        # consistent (because the value is public API)
        tool_id = 2
    # type: ClassVar[dict[int, _LineProfilerManager]], int = thread id
    _managers = {}
    # type: ClassVar[dict[bytes, int]], bytes = bytecode
    _all_paddings = {}
    # type: ClassVar[dict[int, weakref.WeakSet[LineProfiler]]],
    # int = func id
    _all_instances_by_funcs = {}

    def __init__(self, *functions,
                 wrap_trace=None, set_frame_local_trace=None):
        self.functions = []
        self.code_hash_map = {}
        self.dupes_map = {}
        self.timer_unit = hpTimerUnit()
        # Create a data store for thread-local objects
        # https://docs.python.org/3/library/threading.html#thread-local-data
        self.threaddata = threading.local()
        if wrap_trace is not None:
            self.wrap_trace = wrap_trace
        if set_frame_local_trace is not None:
            self.set_frame_local_trace = set_frame_local_trace

        for func in functions:
            self.add_function(func)

    cpdef add_function(self, func):
        """
        Record line profiling information for the given Python function.

        Note:
            This is a low-level method and is intended for |function|_;
            users should in general use
            :py:meth:`line_profiler.LineProfiler.add_callable` for
            adding general callables and callable wrappers (e.g.
            :py:class:`property`).

        .. |function| replace:: :py:class:`types.FunctionType`
        .. _function: https://docs.python.org/3/reference/\
datamodel.html#user-defined-functions
        """
        if hasattr(func, "__wrapped__"):
            warn(
                "Adding a function with a `.__wrapped__` attribute. "
                "You may want to profile the wrapped function by adding "
                f"`{func.__name__}.__wrapped__` instead."
            )
        try:
            code = func.__code__
            func_id = id(func)
        except AttributeError:
            try:
                code = func.__func__.__code__
                func_id = id(func.__func__)
            except AttributeError:
                warn(
                    f"Could not extract a code object for the object {func!r}")
                return

        # Note: if we are to alter the code object, other profilers
        # which previously added this function would still expect the
        # old bytecode, and thus will not see anything when the function
        # is executed;
        # hence:
        # - When doing bytecode padding, take into account all instances
        #   which refers to the same base bytecode to ensure
        #   disambiguation
        # - Update all existing instances referring to the old code
        #   object
        # Since no code padding is/can be done with Cython mock
        # "code objects", it is *probably* okay to only do the special
        # handling on the non-Cython branch.
        # XXX: tests for the above assertion if necessary
        co_code: bytes = code.co_code
        code_hashes = []
        if any(co_code):  # Normal Python functions
            # Figure out how much padding we need and strip the bytecode
            base_co_code: bytes
            npad_code: int
            base_co_code, npad_code = multibyte_rstrip(co_code)
            try:
                npad = self._all_paddings[base_co_code]
            except KeyError:
                npad = 0
            self._all_paddings[base_co_code] = max(npad, npad_code) + 1
            try:
                profilers_to_update = self._all_instances_by_funcs[func_id]
                profilers_to_update.add(self)
            except KeyError:
                profilers_to_update = WeakSet({self})
                self._all_instances_by_funcs[func_id] = profilers_to_update
            # Maintain `.dupes_map` (legacy)
            try:
                self.dupes_map[base_co_code].append(code)
            except KeyError:
                self.dupes_map[base_co_code] = [code]
            if npad > npad_code:
                # Code hash already exists, so there must be a duplicate
                # function (on some instance);
                # (re-)pad with no-op
                co_code = base_co_code + NOP_BYTES * npad
                code = _code_replace(func, co_code=co_code)
                try:
                    func.__code__ = code
                except AttributeError as e:
                    func.__func__.__code__ = code
            else:  # No re-padding -> no need to update the other profs
                profilers_to_update = {self}
            # TODO: Since each line can be many bytecodes, this is kinda
            # inefficient
            # See if this can be sped up by not needing to iterate over
            # every byte
            for offset, _ in enumerate(co_code):
                code_hashes.append(
                    compute_line_hash(
                        hash(co_code),
                        PyCode_Addr2Line(<PyCodeObject*>code, offset)))
        else:  # Cython functions have empty/zero bytecodes
            if CANNOT_LINE_TRACE_CYTHON:
                return

            from line_profiler.line_profiler import get_code_block

            lineno = code.co_firstlineno
            if hasattr(func, '__code__'):
                cython_func = func
            else:
                cython_func = func.__func__
            cython_source = find_cython_source_file(cython_func)
            if not cython_source:  # Can't find the source
                return
            nlines = len(get_code_block(cython_source, lineno))
            block_hash = hash(code)
            for lineno in range(lineno, lineno + nlines):
                code_hash = compute_line_hash(block_hash, lineno)
                code_hashes.append(code_hash)
            # We can't replace the code object on Cython functions, but
            # we can *store* a copy with the correct metadata
            code = code.replace(co_filename=cython_source)
            profilers_to_update = {self}
        # Update `._c_code_map` and `.code_hash_map` with the new line
        # hashes on `self` (and other instances profiling the same
        # function if we padded the bytecode)
        for instance in profilers_to_update:
            prof = <LineProfiler>instance
            try:
                line_hashes = prof.code_hash_map[code]
            except KeyError:
                line_hashes = prof.code_hash_map[code] = []
            for code_hash in code_hashes:
                line_hash = <int64>code_hash
                if not prof._c_code_map.count(line_hash):
                    line_hashes.append(line_hash)
                    prof._c_code_map[line_hash]

        self.functions.append(func)

    property enable_count:
        def __get__(self):
            if not hasattr(self.threaddata, 'enable_count'):
                self.threaddata.enable_count = 0
            return self.threaddata.enable_count
        def __set__(self, value):
            self.threaddata.enable_count = value

    # These three are shared between instances, but thread-local
    # (Ideally speaking they could've been class attributes...)

    property wrap_trace:
        def __get__(self):
            return self._manager.wrap_trace
        def __set__(self, wrap_trace):
            # Make sure we have a manager
            manager = self._manager
            # Sync values between all thread states
            for manager in self._managers.values():
                manager.wrap_trace = wrap_trace

    property set_frame_local_trace:
        def __get__(self):
            return self._manager.set_frame_local_trace
        def __set__(self, set_frame_local_trace):
            # Make sure we have a manager
            manager = self._manager
            # Sync values between all thread states
            for manager in self._managers.values():
                manager.set_frame_local_trace = set_frame_local_trace

    property _manager:
        def __get__(self):
            thread_id = threading.get_ident()
            try:
                return self._managers[thread_id]
            except KeyError:
                pass
            # First profiler instance on the thread, get the correct
            # `wrap_trace` and `set_frame_local_trace` values and set up
            # a `_LineProfilerManager`
            try:
                manager, *_ = self._managers.values()
            except ValueError:
                # First thread in the interpretor: load default values
                # from the environment (at package startup time)
                wrap_trace = WRAP_TRACE
                set_frame_local_trace = SET_FRAME_LOCAL_TRACE
            else:
                # Fetch the values from an existing manager
                wrap_trace = manager.wrap_trace
                set_frame_local_trace = manager.set_frame_local_trace
            self._managers[thread_id] = manager = _LineProfilerManager(
                self.tool_id, wrap_trace, set_frame_local_trace)
            return manager

    def enable_by_count(self):
        """ Enable the profiler if it hasn't been enabled before.
        """
        if self.enable_count == 0:
            self.enable()
        self.enable_count += 1

    def disable_by_count(self):
        """
        Disable the profiler if the number of disable requests matches
        (or exceeds) the number of enable requests.
        """
        if self.enable_count > 0:
            self.enable_count -= 1
            if self.enable_count == 0:
                self.disable()

    def __enter__(self):
        self.enable_by_count()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.disable_by_count()

    def enable(self):
        self._manager._handle_enable_event(self)

    @property
    def c_code_map(self):
        """
        A Python view of the internal C lookup table.
        """
        return <dict>self._c_code_map

    @property
    def c_last_time(self):
        """
        Raises:
            KeyError
                If no profiling data is available on the current thread.
        """
        try:
            return (<dict>self._c_last_time)[threading.get_ident()]
        except KeyError as e:
            # We haven't actually profiled anything yet
            raise (KeyError('No profiling data on the current thread '
                            '(`threading.get_ident()` = '
                            f'{threading.get_ident()})')
                   .with_traceback(e.__traceback__)) from None

    @property
    def code_map(self):
        """
        :py:mod:`line_profiler` 4.0 no longer directly maintains
        :py:attr:`~.code_map`, but this will construct something similar
        for backwards compatibility.
        """
        c_code_map = self.c_code_map
        code_hash_map = self.code_hash_map
        py_code_map = {}
        for code, code_hashes in code_hash_map.items():
            py_code_map.setdefault(code, {})
            for code_hash in code_hashes:
                c_entries = c_code_map[code_hash]
                py_entries = {}
                for key, c_entry in c_entries.items():
                    py_entry = c_entry.copy()
                    py_entry['code'] = code
                    py_entries[key] = py_entry
                py_code_map[code].update(py_entries)
        return py_code_map

    @property
    def last_time(self):
        """
        :py:mod:`line_profiler` 4.0 no longer directly maintains
        :py:attr:`~.last_time`, but this will construct something similar
        for backwards compatibility.
        """
        c_last_time = self.c_last_time
        py_last_time = {}
        for code in self.code_hash_map:
            block_hash = hash(code.co_code)
            if block_hash in c_last_time:
                py_last_time[code] = c_last_time[block_hash]
        return py_last_time

    cpdef disable(self):
        self._c_last_time[threading.get_ident()].clear()
        self._manager._handle_disable_event(self)

    def get_stats(self):
        """
        Returns:
            :py:class:`LineStats` object containing the timings.
        """
        cdef dict cmap = self._c_code_map

        all_entries = {}
        for code in self.code_hash_map:
            entries = []
            for entry in self.code_hash_map[code]:
                entries.extend(cmap[entry].values())
            key = label(code)

            # Merge duplicate line numbers, which occur for branch
            # entrypoints like `if`
            entries_by_lineno = all_entries.setdefault(key, {})

            for line_dict in entries:
                 _, lineno, total_time, nhits = line_dict.values()
                 orig_nhits, orig_total_time = entries_by_lineno.get(
                     lineno, (0, 0))
                 entries_by_lineno[lineno] = (orig_nhits + nhits,
                                              orig_total_time + total_time)

        # Aggregate the timing data
        stats = {
            key: sorted((line, nhits, time)
                        for line, (nhits, time) in entries_by_lineno.items())
            for key, entries_by_lineno in all_entries.items()}
        return LineStats(stats, self.timer_unit)


@cython.boundscheck(False)
@cython.wraparound(False)
cdef inline inner_trace_callback(
        int is_line_event, object instances, object code, int lineno):
    """
    The basic building block for the trace callbacks.
    """
    cdef object prof_
    cdef object bytecode = code.co_code
    cdef LineProfiler prof
    cdef LastTime old
    cdef int key
    cdef PY_LONG_LONG time
    cdef int has_time = 0
    cdef int64 code_hash
    cdef int64 block_hash
    cdef unordered_map[int64, LineTime] line_entries

    if any(bytecode):
        block_hash = hash(bytecode)
    else:  # Cython functions have empty/zero bytecodes
        block_hash = hash(code)
    code_hash = compute_line_hash(block_hash, lineno)

    for prof_ in instances:
        prof = <LineProfiler>prof_
        if not prof._c_code_map.count(code_hash):
            continue
        if not has_time:
            time = hpTimer()
            has_time = 1
        ident = threading.get_ident()
        if prof._c_last_time[ident].count(block_hash):
            old = prof._c_last_time[ident][block_hash]
            line_entries = prof._c_code_map[code_hash]
            key = old.f_lineno
            if not line_entries.count(key):
                prof._c_code_map[code_hash][key] = LineTime(
                    code_hash, key, 0, 0)
            prof._c_code_map[code_hash][key].nhits += 1
            prof._c_code_map[code_hash][key].total_time += time - old.time
        if is_line_event:
            # Get the time again. This way, we don't record much time
            # wasted in this function.
            prof._c_last_time[ident][block_hash] = LastTime(lineno, hpTimer())
        elif prof._c_last_time[ident].count(block_hash):
            # We are returning from a function, not executing a line.
            # Delete the last_time record. It may have already been
            # deleted if we are profiling a generator that is being
            # pumped past its end.
            prof._c_last_time[ident].erase(
                prof._c_last_time[ident].find(block_hash))


cdef extern int legacy_trace_callback(
        object manager, PyFrameObject *py_frame, int what, PyObject *arg):
    """
    The :c:func:`PyEval_SetTrace` callback.

    References:
       https://github.com/python/cpython/blob/de2a4036/Include/cpython/\
pystate.h#L16
    """
    cdef _LineProfilerManager manager_ = <_LineProfilerManager>manager
    cdef int result
    cdef int recursion_guard = manager_.recursion_guard

    if what == PyTrace_CALL:
        # Any code using the `sys.gettrace()`-`sys.settrace()` paradigm
        # (e.g. to temporarily suspend or alter tracing) will cause line
        # events to not be passed to the global trace callback (i.e.
        # `manager`) for the rest of the frame, e.g.
        #     >>> callback = sys.gettrace()
        #     >>> sys.settrace(None)
        #     >>> try:  # Tracing suspended here
        #     ...     ...
        #     ... finally:
        #     ...     # Tracing object restored here, but line event
        #     ...     # tracing is disabled before the next function
        #     ...     # call because that's what the default trace
        #     ...     # trampoline works
        #     ...     sys.settrace(callback)
        # To circumvent this, set the local `.f_trace` upon entering a
        # frame (if not already set), so that tracing can restart upon
        # the restoration with `sys.settrace()`
        if manager_._set_frame_local_trace:
            set_local_trace(<PyObject *>manager_, py_frame)
    elif what == PyTrace_LINE or what == PyTrace_RETURN:
        # Normally we'd need to DECREF the return from
        # `PyFrame_GetCode()`, but Cython does that for us
        inner_trace_callback((what == PyTrace_LINE),
                             manager_.active_instances,
                             <object>PyFrame_GetCode(py_frame),
                             PyFrame_GetLineNumber(py_frame))

    # Call the trace callback that we're wrapping around where
    # appropriate
    if manager_._wrap_trace:
        # Due to how the frame-local callback could be set to the active
        # `_LineProfilerManager` or a wrapper object (see
        # `set_local_trace()`), wrap the callback call to make sure that
        # we don't recurse back here
        manager_.recursion_guard = 1
        try:
            result = call_callback(
                <PyObject *>disable_line_events, manager_.legacy_callback,
                py_frame, what, arg)
        finally:
            manager_.recursion_guard = recursion_guard
    else:
        result = 0

    # Prevent other trace functions from overwritting `manager`;
    # if there is a frame-local trace function, create a wrapper calling
    # both it and `manager`
    if manager_._set_frame_local_trace:
        set_local_trace(<PyObject *>manager_, py_frame)
    return result