pg-lock-tracer 0.5.5__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__ = "0.5.5"
1
+ __version__ = "0.6.0"
@@ -0,0 +1,121 @@
1
+ #include <uapi/linux/ptrace.h>
2
+
3
+ /* Placeholder for auto generated defines */
4
+ __DEFINES__
5
+
6
+ typedef struct RowLockEvent_t {
7
+ u32 pid;
8
+ u64 timestamp;
9
+ u32 event_type;
10
+
11
+ /* See RelFileNode - Oid is u32 */
12
+ u32 tablespace;
13
+ u32 database;
14
+ u32 relation;
15
+
16
+ /* LockTupleMode */
17
+ u8 locktuplemode;
18
+
19
+ /* LockWaitPolicy */
20
+ u8 lockwaitpolicy;
21
+
22
+ /* Locked tuple */
23
+ u32 blockid;
24
+ u16 offset;
25
+
26
+ /* TM_Result */
27
+ int lockresult;
28
+ } RowLockEvent;
29
+
30
+ BPF_PERF_OUTPUT(lockevents);
31
+
32
+ static void fill_and_submit(struct pt_regs *ctx, RowLockEvent *event) {
33
+ event->pid = bpf_get_current_pid_tgid();
34
+ event->timestamp = bpf_ktime_get_ns();
35
+
36
+ // sudo cat /sys/kernel/debug/tracing/trace_pipe
37
+ // bpf_trace_printk("LW lock event for trance: %s\\n", tranche);
38
+
39
+ lockevents.perf_submit(ctx, event, sizeof(RowLockEvent));
40
+ }
41
+
42
+ /*
43
+ * Acquire a tuple lock
44
+ *
45
+ * Arguments:
46
+ * 1. Relation relation (1st member RelFileNode)
47
+ * 2. ItemPointer tid
48
+ * 3. Snapshot snapshot,
49
+ * 4. TupleTableSlot *slot,
50
+ * 5. CommandId cid,
51
+ * 6. LockTupleMode mode,
52
+ * 7. LockWaitPolicy wait_policy,
53
+ * 8. uint8 flags,
54
+ * 9. TM_FailureData *tmfd
55
+ *
56
+ */
57
+ int heapam_tuple_lock(struct pt_regs *ctx) {
58
+ RowLockEvent event = {.event_type = EVENT_LOCK_TUPLE};
59
+
60
+ /*
61
+ * (gdb) ptype /o RelFileNode
62
+ * 0 | 4 Oid spcNode;
63
+ * 4 | 4 Oid dbNode;
64
+ * 8 | 4 Oid relNode;
65
+ */
66
+
67
+ char buffer_relation[12];
68
+ bpf_probe_read_user(buffer_relation, sizeof(buffer_relation),
69
+ (void *)PT_REGS_PARM1(ctx));
70
+ bpf_probe_read_kernel(&(event.tablespace), sizeof(event.tablespace),
71
+ &(buffer_relation[0]));
72
+ bpf_probe_read_kernel(&(event.database), sizeof(event.database),
73
+ &(buffer_relation[4]));
74
+ bpf_probe_read_kernel(&(event.relation), sizeof(event.relation),
75
+ &(buffer_relation[8]));
76
+
77
+ /* Locked tuple */
78
+ char buffer_item_pointer[6];
79
+ u16 bi_hi;
80
+ u16 bi_lo;
81
+
82
+ bpf_probe_read_user(buffer_item_pointer, sizeof(buffer_item_pointer),
83
+ (void *)PT_REGS_PARM2(ctx));
84
+ bpf_probe_read_kernel(&(bi_hi), sizeof(bi_hi), &(buffer_item_pointer[0]));
85
+ bpf_probe_read_kernel(&(bi_lo), sizeof(bi_lo), &(buffer_item_pointer[2]));
86
+ bpf_probe_read_kernel(&(event.offset), sizeof(event.offset),
87
+ &(buffer_item_pointer[4]));
88
+
89
+ /* See #define BlockIdGetBlockNumber(blockId) */
90
+ event.blockid = (bi_hi) << 16 | bi_lo;
91
+
92
+ /* Locking options */
93
+ bpf_probe_read_kernel(&(event.locktuplemode), sizeof(event.locktuplemode),
94
+ &(PT_REGS_PARM6(ctx)));
95
+
96
+ /* Only the first six function parameters are passed via register. All
97
+ * remaining parameters are stored on the stack.
98
+ *
99
+ * See: System V Application Binary Interface—AMD64 Architecture Processor
100
+ * Supplement.
101
+ */
102
+ void *ptr = 0;
103
+ bpf_probe_read(&ptr, sizeof(ptr), (void *)(PT_REGS_SP(ctx) + (1 * 8)));
104
+ bpf_probe_read_kernel(&(event.lockwaitpolicy), sizeof(event.lockwaitpolicy),
105
+ &ptr);
106
+
107
+ fill_and_submit(ctx, &event);
108
+ return 0;
109
+ }
110
+
111
+ /*
112
+ * Acquire a tuple lock - Function done
113
+ */
114
+ int heapam_tuple_lock_end(struct pt_regs *ctx) {
115
+ RowLockEvent event = {.event_type = EVENT_LOCK_TUPLE_END};
116
+
117
+ event.lockresult = PT_REGS_RC(ctx);
118
+
119
+ fill_and_submit(ctx, &event);
120
+ return 0;
121
+ }
pg_lock_tracer/helper.py CHANGED
@@ -6,6 +6,8 @@ import os
6
6
 
7
7
  from pathlib import Path
8
8
 
9
+ from bcc import BPF
10
+
9
11
 
10
12
  class PostgreSQLLockHelper:
11
13
 
@@ -91,6 +93,9 @@ class PostgreSQLLockHelper:
91
93
 
92
94
 
93
95
  class BPFHelper:
96
+ # The size of the kernel ring buffer
97
+ page_cnt = 2048
98
+
94
99
  @staticmethod
95
100
  def enum_to_defines(enum_instance, prefix):
96
101
  """
@@ -115,3 +120,54 @@ class BPFHelper:
115
120
 
116
121
  with program_file.open("r") as bpf_program:
117
122
  return bpf_program.read()
123
+
124
+ @staticmethod
125
+ def check_pid_exe(pids, executable):
126
+ """
127
+ Do the given PIDs belong to the executable
128
+ """
129
+ if not pids:
130
+ return
131
+
132
+ for pid in pids:
133
+ if not os.path.isdir(f"/proc/{pid}"):
134
+ raise ValueError(
135
+ f"/proc entry for pid {pid} not found, does the process exist?"
136
+ )
137
+
138
+ binary = os.readlink(f"/proc/{pid}/exe")
139
+
140
+ if binary != executable:
141
+ raise ValueError(
142
+ f"Pid {pid} does not belong to binary {executable}. Executable is {binary}"
143
+ )
144
+
145
+ @staticmethod
146
+ def register_ebpf_probe(
147
+ path, bpf_instance, function_regex, bpf_fn_name, verbose, probe_on_enter=True
148
+ ):
149
+ """
150
+ Register a BPF probe
151
+ """
152
+ addresses = set()
153
+ func_and_addr = BPF.get_user_functions_and_addresses(path, function_regex)
154
+
155
+ if not func_and_addr:
156
+ raise ValueError(f"Unable to locate function {function_regex}")
157
+
158
+ # Handle address duplicates
159
+ for function, address in func_and_addr:
160
+ if address in addresses:
161
+ continue
162
+ addresses.add(address)
163
+
164
+ if probe_on_enter:
165
+ bpf_instance.attach_uprobe(name=path, sym=function, fn_name=bpf_fn_name)
166
+ if verbose:
167
+ print(f"Attaching to {function} at address {address} on enter")
168
+ else:
169
+ bpf_instance.attach_uretprobe(
170
+ name=path, sym=function, fn_name=bpf_fn_name
171
+ )
172
+ if verbose:
173
+ print(f"Attaching to {function} at address {address} on return")
@@ -95,7 +95,6 @@ parser.add_argument(
95
95
  dest="pids",
96
96
  metavar="PID",
97
97
  help="the pid(s) to trace",
98
- required=True,
99
98
  )
100
99
  parser.add_argument(
101
100
  "-x",
@@ -352,7 +351,11 @@ class PGLockTraceOutputHuman(PGLockTraceOutput):
352
351
  """
353
352
  event = self.bpf_instance["lockevents"].event(data)
354
353
 
355
- if event.pid not in self.pids and event.event_type < Events.GLOBAL:
354
+ if (
355
+ self.pids
356
+ and event.pid not in self.pids
357
+ and event.event_type < Events.GLOBAL
358
+ ):
356
359
  return
357
360
 
358
361
  print_prefix = f"{event.timestamp} [Pid {event.pid}]"
@@ -593,18 +596,7 @@ class PGLockTracer:
593
596
  self.oid_resolvers[resolver_pid] = oid_resolver
594
597
 
595
598
  # Belong the processes to the binary?
596
- for pid in self.args.pids:
597
- if not os.path.isdir(f"/proc/{pid}"):
598
- raise ValueError(
599
- f"/proc entry for pid {pid} not found, does the process exist?"
600
- )
601
-
602
- binary = os.readlink(f"/proc/{pid}/exe")
603
-
604
- if binary != self.args.path:
605
- raise ValueError(
606
- f"Pid {pid} does not belong to binary {self.args.path}. Executable is {binary}"
607
- )
599
+ BPFHelper.check_pid_exe(self.args.pids, self.args.path)
608
600
 
609
601
  # Does the output file already exists?
610
602
  if self.args.output_file and os.path.exists(self.args.output_file):
@@ -692,91 +684,179 @@ class PGLockTracer:
692
684
 
693
685
  # Open the event queue
694
686
  self.bpf_instance["lockevents"].open_perf_buffer(
695
- self.output_class.print_event, page_cnt=2048
687
+ self.output_class.print_event, page_cnt=BPFHelper.page_cnt
696
688
  )
697
689
 
698
- def register_probe(self, function_regex, bpf_fn_name, probe_on_enter=True):
699
- """
700
- Register a BPF probe
701
- """
702
- addresses = set()
703
- func_and_addr = BPF.get_user_functions_and_addresses(
704
- self.args.path, function_regex
705
- )
706
-
707
- if not func_and_addr:
708
- raise ValueError(f"Unable to locate function {function_regex}")
709
-
710
- # Handle address duplicates
711
- for function, address in func_and_addr:
712
- if address in addresses:
713
- continue
714
- addresses.add(address)
715
-
716
- if probe_on_enter:
717
- self.bpf_instance.attach_uprobe(
718
- name=self.args.path, sym=function, fn_name=bpf_fn_name
719
- )
720
- if self.args.verbose:
721
- print(f"Attaching to {function} at address {address} on enter")
722
- else:
723
- self.bpf_instance.attach_uretprobe(
724
- name=self.args.path, sym=function, fn_name=bpf_fn_name
725
- )
726
- if self.args.verbose:
727
- print(f"Attaching to {function} at address {address} on return")
728
-
729
690
  def attach_probes(self):
730
691
  """
731
692
  Attach BPF probes
732
693
  """
733
-
734
694
  # Transaction probes
735
695
  if self.args.trace is None or TraceEvents.TRANSACTION.name in self.args.trace:
736
- self.register_probe("^StartTransaction$", "bpf_transaction_begin")
737
- self.register_probe("^CommitTransaction$", "bpf_transaction_commit")
738
- self.register_probe("^AbortTransaction$", "bpf_transaction_abort")
739
- self.register_probe("^DeadLockReport$", "bpf_deadlock")
696
+ BPFHelper.register_ebpf_probe(
697
+ self.args.path,
698
+ self.bpf_instance,
699
+ "^StartTransaction$",
700
+ "bpf_transaction_begin",
701
+ self.args.verbose,
702
+ )
703
+ BPFHelper.register_ebpf_probe(
704
+ self.args.path,
705
+ self.bpf_instance,
706
+ "^CommitTransaction$",
707
+ "bpf_transaction_commit",
708
+ self.args.verbose,
709
+ )
710
+ BPFHelper.register_ebpf_probe(
711
+ self.args.path,
712
+ self.bpf_instance,
713
+ "^AbortTransaction$",
714
+ "bpf_transaction_abort",
715
+ self.args.verbose,
716
+ )
717
+ BPFHelper.register_ebpf_probe(
718
+ self.args.path,
719
+ self.bpf_instance,
720
+ "^DeadLockReport$",
721
+ "bpf_deadlock",
722
+ self.args.verbose,
723
+ )
740
724
 
741
725
  # Query probes
742
726
  if self.args.trace is None or TraceEvents.QUERY.name in self.args.trace:
743
- self.register_probe("^exec_simple_query$", "bpf_query_begin")
744
- self.register_probe("^exec_simple_query$", "bpf_query_end", False)
727
+ BPFHelper.register_ebpf_probe(
728
+ self.args.path,
729
+ self.bpf_instance,
730
+ "^exec_simple_query$",
731
+ "bpf_query_begin",
732
+ self.args.verbose,
733
+ )
734
+ BPFHelper.register_ebpf_probe(
735
+ self.args.path,
736
+ self.bpf_instance,
737
+ "^exec_simple_query$",
738
+ "bpf_query_end",
739
+ self.args.verbose,
740
+ False,
741
+ )
745
742
 
746
743
  # Table probes
747
744
  if self.args.trace is None or TraceEvents.TABLE.name in self.args.trace:
748
- self.register_probe("^table_open$", "bpf_table_open")
749
- self.register_probe("^table_openrv$", "bpf_table_openrv")
750
- self.register_probe("^table_openrv_extended$", "bpf_table_openrv_extended")
751
- self.register_probe("^table_close$", "bpf_table_close")
745
+ BPFHelper.register_ebpf_probe(
746
+ self.args.path,
747
+ self.bpf_instance,
748
+ "^table_open$",
749
+ "bpf_table_open",
750
+ self.args.verbose,
751
+ )
752
+ BPFHelper.register_ebpf_probe(
753
+ self.args.path,
754
+ self.bpf_instance,
755
+ "^table_openrv$",
756
+ "bpf_table_openrv",
757
+ self.args.verbose,
758
+ )
759
+ BPFHelper.register_ebpf_probe(
760
+ self.args.path,
761
+ self.bpf_instance,
762
+ "^table_openrv_extended$",
763
+ "bpf_table_openrv_extended",
764
+ self.args.verbose,
765
+ )
766
+ BPFHelper.register_ebpf_probe(
767
+ self.args.path,
768
+ self.bpf_instance,
769
+ "^table_close$",
770
+ "bpf_table_close",
771
+ self.args.verbose,
772
+ )
752
773
 
753
774
  # Lock probes
754
775
  if self.args.trace is None or TraceEvents.LOCK.name in self.args.trace:
755
- self.register_probe("^LockRelationOid$", "bpf_lock_relation_oid")
756
- self.register_probe("^LockRelationOid$", "bpf_lock_relation_oid_end", False)
757
- self.register_probe("^UnlockRelationOid$", "bpf_unlock_relation_oid")
758
- self.register_probe("^GrantLock$", "bpf_lock_grant")
759
- self.register_probe(
776
+ BPFHelper.register_ebpf_probe(
777
+ self.args.path,
778
+ self.bpf_instance,
779
+ "^LockRelationOid$",
780
+ "bpf_lock_relation_oid",
781
+ self.args.verbose,
782
+ )
783
+ BPFHelper.register_ebpf_probe(
784
+ self.args.path,
785
+ self.bpf_instance,
786
+ "^LockRelationOid$",
787
+ "bpf_lock_relation_oid_end",
788
+ self.args.verbose,
789
+ False,
790
+ )
791
+ BPFHelper.register_ebpf_probe(
792
+ self.args.path,
793
+ self.bpf_instance,
794
+ "^UnlockRelationOid$",
795
+ "bpf_unlock_relation_oid",
796
+ self.args.verbose,
797
+ )
798
+ BPFHelper.register_ebpf_probe(
799
+ self.args.path,
800
+ self.bpf_instance,
801
+ "^GrantLock$",
802
+ "bpf_lock_grant",
803
+ self.args.verbose,
804
+ )
805
+ BPFHelper.register_ebpf_probe(
806
+ self.args.path,
807
+ self.bpf_instance,
760
808
  "^FastPathGrantRelationLock$",
761
809
  "bpf_lock_fastpath_grant",
810
+ self.args.verbose,
762
811
  )
763
- self.register_probe("^GrantLockLocal$", "bpf_lock_local_grant")
764
- self.register_probe("^UnGrantLock$", "bpf_lock_ungrant")
765
- self.register_probe(
812
+ BPFHelper.register_ebpf_probe(
813
+ self.args.path,
814
+ self.bpf_instance,
815
+ "^GrantLockLocal$",
816
+ "bpf_lock_local_grant",
817
+ self.args.verbose,
818
+ )
819
+ BPFHelper.register_ebpf_probe(
820
+ self.args.path,
821
+ self.bpf_instance,
822
+ "^UnGrantLock$",
823
+ "bpf_lock_ungrant",
824
+ self.args.verbose,
825
+ )
826
+ BPFHelper.register_ebpf_probe(
827
+ self.args.path,
828
+ self.bpf_instance,
766
829
  "^FastPathUnGrantRelationLock$",
767
830
  "bpf_lock_fastpath_ungrant",
831
+ self.args.verbose,
832
+ )
833
+ BPFHelper.register_ebpf_probe(
834
+ self.args.path,
835
+ self.bpf_instance,
836
+ "^RemoveLocalLock$",
837
+ "bfp_local_lock_ungrant",
838
+ self.args.verbose,
768
839
  )
769
- self.register_probe("^RemoveLocalLock$", "bfp_local_lock_ungrant")
770
840
 
771
841
  # Invalidation messages probes
772
842
  if self.args.trace is None or TraceEvents.INVALIDATION.name in self.args.trace:
773
- self.register_probe(
774
- "^AcceptInvalidationMessages$", "bpf_accept_invalidation_messages"
843
+ BPFHelper.register_ebpf_probe(
844
+ self.args.path,
845
+ self.bpf_instance,
846
+ "^AcceptInvalidationMessages$",
847
+ "bpf_accept_invalidation_messages",
848
+ self.args.verbose,
775
849
  )
776
850
 
777
851
  # Error probes
778
852
  if self.args.trace is None or TraceEvents.ERROR.name in self.args.trace:
779
- self.register_probe("^errstart$", "bpf_errstart")
853
+ BPFHelper.register_ebpf_probe(
854
+ self.args.path,
855
+ self.bpf_instance,
856
+ "^errstart$",
857
+ "bpf_errstart",
858
+ self.args.verbose,
859
+ )
780
860
 
781
861
  def run(self):
782
862
  """
@@ -12,7 +12,7 @@
12
12
  import sys
13
13
  import argparse
14
14
 
15
- from enum import IntEnum
15
+ from enum import IntEnum, unique
16
16
 
17
17
  from bcc import BPF, USDT
18
18
  from prettytable import PrettyTable
@@ -65,6 +65,7 @@ parser.add_argument(
65
65
  parser.add_argument("--statistics", action="store_true", help="print lock statistics")
66
66
 
67
67
 
68
+ @unique
68
69
  class Events(IntEnum):
69
70
  LOCK = 0
70
71
  LOCK_OR_WAIT = 1
@@ -76,6 +77,13 @@ class Events(IntEnum):
76
77
  COND_ACQUIRE_FAIL = 7
77
78
 
78
79
 
80
+ @unique
81
+ class LWLockMode(IntEnum):
82
+ LW_EXCLUSIVE = 0
83
+ LW_SHARED = 1
84
+ LW_WAIT_UNTIL_FREE = 2
85
+
86
+
79
87
  class LockStatisticsEntry:
80
88
  def __init__(self) -> None:
81
89
  # The number of non-waited requested locks
@@ -239,22 +247,6 @@ class PGLWLockTracer:
239
247
 
240
248
  return event.timestamp - self.last_lock_request_time[event.pid]
241
249
 
242
- @staticmethod
243
- def resolve_lock_mode(event):
244
- """
245
- Resolve the LW Lock modes
246
- """
247
- if event.mode == 0: # LW_EXCLUSIVE,
248
- return "LW_EXCLUSIVE"
249
-
250
- if event.mode == 1: # LW_SHARED
251
- return "LW_SHARED"
252
-
253
- if event.mode == 2: # LW_WAIT_UNTIL_FREE
254
- return "LW_WAIT_UNTIL_FREE"
255
-
256
- raise ValueError(f"Unknown event type {event.event_type}")
257
-
258
250
  def print_lock_event(self, _cpu, data, _size):
259
251
  """
260
252
  Print a new lock event.
@@ -267,7 +259,7 @@ class PGLWLockTracer:
267
259
  tranche = event.tranche.decode("utf-8")
268
260
 
269
261
  print_prefix = f"{event.timestamp} [Pid {event.pid}]"
270
- lock_mode = PGLWLockTracer.resolve_lock_mode(event)
262
+ lock_mode = LWLockMode(event.mode).name
271
263
 
272
264
  self.update_statistics(event, tranche, lock_mode)
273
265
 
@@ -331,6 +323,7 @@ class PGLWLockTracer:
331
323
  print("=======")
332
324
 
333
325
  enum_defines = BPFHelper.enum_to_defines(Events, "EVENT")
326
+
334
327
  bpf_program = BPFHelper.read_bpf_program("pg_lw_lock_tracer.c")
335
328
  bpf_program_final = bpf_program.replace("__DEFINES__", enum_defines)
336
329
 
@@ -347,7 +340,7 @@ class PGLWLockTracer:
347
340
  )
348
341
 
349
342
  self.bpf_instance["lockevents"].open_perf_buffer(
350
- self.print_lock_event, page_cnt=64
343
+ self.print_lock_event, page_cnt=BPFHelper.page_cnt
351
344
  )
352
345
 
353
346
  def print_statistics(self):
@@ -0,0 +1,372 @@
1
+ #!/usr/bin/env python3
2
+ #
3
+ # PostgreSQL row lock tracer.
4
+ #
5
+ # See https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS
6
+ #
7
+ ###############################################
8
+
9
+ import sys
10
+ import argparse
11
+
12
+ from enum import IntEnum, unique
13
+
14
+ from bcc import BPF
15
+ from prettytable import PrettyTable
16
+
17
+ from pg_lock_tracer import __version__
18
+ from pg_lock_tracer.helper import BPFHelper
19
+
20
+ EXAMPLES = """examples:
21
+ # Trace the row locks of the given PostgreSQL binary
22
+ pg_row_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
23
+
24
+ # Trace the row locks of the PID 1234
25
+ pg_row_lock_tracer -p 1234 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
26
+
27
+ # Trace the row locks of the PID 1234 and 5678
28
+ pg_row_lock_tracer -p 1234 -p 5678 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
29
+
30
+ # Trace the row locks of the PID 1234 and be verbose
31
+ pg_row_lock_tracer -p 1234 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres -v
32
+
33
+ # Trace the row locks and show statistics
34
+ pg_row_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres --statistics
35
+ """
36
+
37
+ parser = argparse.ArgumentParser(
38
+ description="",
39
+ formatter_class=argparse.RawDescriptionHelpFormatter,
40
+ epilog=EXAMPLES,
41
+ )
42
+ parser.add_argument(
43
+ "-V",
44
+ "--version",
45
+ action="version",
46
+ version=f"{parser.prog} ({__version__})",
47
+ )
48
+ parser.add_argument("-v", "--verbose", action="store_true", help="Be verbose")
49
+ parser.add_argument(
50
+ "-p",
51
+ "--pid",
52
+ type=int,
53
+ nargs="+",
54
+ dest="pids",
55
+ metavar="PID",
56
+ help="the pid(s) to trace",
57
+ )
58
+ parser.add_argument(
59
+ "-x",
60
+ "--exe",
61
+ type=str,
62
+ required=True,
63
+ dest="path",
64
+ metavar="PATH",
65
+ help="path to binary",
66
+ )
67
+ parser.add_argument(
68
+ "-d",
69
+ "--dry-run",
70
+ action="store_true",
71
+ help="compile and load the BPF program but exit afterward",
72
+ )
73
+ parser.add_argument("--statistics", action="store_true", help="print lock statistics")
74
+
75
+
76
+ @unique
77
+ class Events(IntEnum):
78
+ LOCK_TUPLE = 0
79
+ LOCK_TUPLE_END = 1
80
+
81
+
82
+ # See lockoptions.h in PostgreSQL
83
+ @unique
84
+ class TMResult(IntEnum):
85
+ TM_OK = 0
86
+ TM_INVISIBLE = 1
87
+ TM_SELFMODIFIED = 2
88
+ TM_UPDATED = 3
89
+ TM_DELETED = 4
90
+ TM_BEINGMODIFIED = 5
91
+ TM_WOULDBLOCK = 6
92
+
93
+
94
+ # See lockoptions.h in PostgreSQL
95
+ @unique
96
+ class LockWaitPolicy(IntEnum):
97
+ LOCK_WAIT_BLOCK = 0
98
+ LOCK_WAIT_SKIP = 1
99
+ LOCK_WAIT_ERROR = 2
100
+
101
+
102
+ # See lockoptions.h in PostgreSQL
103
+ @unique
104
+ class LockTupleMode(IntEnum):
105
+ LOCK_TUPLE_KEYSHARE = 0
106
+ LOCK_TUPLE_SHARE = 1
107
+ LOCK_TUPLE_NOKEYEXCLUSIVE = 2
108
+ LOCK_TUPLE_EXCLUSIVE = 3
109
+
110
+
111
+ class LockStatisticsEntry:
112
+ def __init__(self) -> None:
113
+ # The requested locks
114
+ self._lock_modes = {}
115
+
116
+ # The used lock policies
117
+ self._lock_policies = {}
118
+
119
+ # The lock results
120
+ self._lock_results = {}
121
+
122
+ @property
123
+ def lock_modes(self):
124
+ return self._lock_modes
125
+
126
+ @lock_modes.setter
127
+ def lock_modes(self, value):
128
+ self._lock_modes = value
129
+
130
+ @property
131
+ def lock_policies(self):
132
+ return self._lock_policies
133
+
134
+ @lock_policies.setter
135
+ def lock_policies(self, value):
136
+ self._lock_policies = value
137
+
138
+ @property
139
+ def lock_results(self):
140
+ return self._lock_results
141
+
142
+ @lock_results.setter
143
+ def lock_results(self, value):
144
+ self._results = value
145
+
146
+
147
+ class PGRowLockTracer:
148
+ def __init__(self, prog_args):
149
+ self.bpf_instance = None
150
+ self.args = prog_args
151
+ self.statistics = {}
152
+
153
+ # Variables for lock timing
154
+ self.last_lock_request_time = {}
155
+
156
+ # Belong the processes to the binary?
157
+ BPFHelper.check_pid_exe(self.args.pids, self.args.path)
158
+
159
+ def get_lock_wait_time(self, event):
160
+ """
161
+ Get the last lock wait time (WAIT_START updates
162
+ last_lock_request_time).
163
+ """
164
+ if event.event_type != Events.LOCK_TUPLE_END:
165
+ return None
166
+
167
+ return event.timestamp - self.last_lock_request_time[event.pid]
168
+
169
+ def update_statistics(self, event):
170
+ """
171
+ Update the statistics
172
+ """
173
+ if event.pid not in self.statistics:
174
+ self.statistics[event.pid] = LockStatisticsEntry()
175
+
176
+ statistics_entry = self.statistics.get(event.pid)
177
+
178
+ # Lock requested
179
+ if event.event_type == Events.LOCK_TUPLE:
180
+ lock_wait_policy = LockWaitPolicy(event.lockwaitpolicy)
181
+
182
+ if lock_wait_policy in statistics_entry.lock_policies:
183
+ statistics_entry.lock_policies[lock_wait_policy] += 1
184
+ else:
185
+ statistics_entry.lock_policies[lock_wait_policy] = 1
186
+
187
+ lock_tuple_mode = LockTupleMode(event.locktuplemode)
188
+
189
+ if lock_tuple_mode in statistics_entry.lock_modes:
190
+ statistics_entry.lock_modes[lock_tuple_mode] += 1
191
+ else:
192
+ statistics_entry.lock_modes[lock_tuple_mode] = 1
193
+
194
+ return
195
+
196
+ # Lock request done
197
+ if event.event_type == Events.LOCK_TUPLE_END:
198
+ lock_result = TMResult(event.lockresult)
199
+
200
+ if lock_result in statistics_entry.lock_results:
201
+ statistics_entry.lock_results[lock_result] += 1
202
+ else:
203
+ statistics_entry.lock_results[lock_result] = 1
204
+ return
205
+
206
+ return
207
+
208
+ def print_lock_event(self, _cpu, data, _size):
209
+ """
210
+ Print a new lock event.
211
+ """
212
+ event = self.bpf_instance["lockevents"].event(data)
213
+
214
+ if self.args.pids and event.pid not in self.args.pids:
215
+ return
216
+
217
+ print_prefix = f"{event.timestamp} [Pid {event.pid}]"
218
+
219
+ self.update_statistics(event)
220
+
221
+ if event.event_type == Events.LOCK_TUPLE:
222
+ self.last_lock_request_time[event.pid] = event.timestamp
223
+
224
+ locktuplemode = LockTupleMode(event.locktuplemode).name
225
+ lockwaitpolicy = LockWaitPolicy(event.lockwaitpolicy).name
226
+
227
+ print(
228
+ f"{print_prefix} LOCK_TUPLE (Tablespace {event.tablespace} "
229
+ f"database {event.database} relation {event.relation}) "
230
+ f"- (Block and offset {event.blockid} {event.offset}) "
231
+ f"- {locktuplemode} {lockwaitpolicy}"
232
+ )
233
+ elif event.event_type == Events.LOCK_TUPLE_END:
234
+ lockresult = TMResult(event.lockresult).name
235
+ needed_time = self.get_lock_wait_time(event)
236
+ print(f"{print_prefix} LOCK_TUPLE_END {lockresult} in {needed_time} ns")
237
+ else:
238
+ raise ValueError(f"Unknown event type {event.event_type}")
239
+
240
+ def init(self):
241
+ """
242
+ Init the PostgreSQL lock tracer
243
+ """
244
+ enum_defines = BPFHelper.enum_to_defines(Events, "EVENT")
245
+
246
+ bpf_program = BPFHelper.read_bpf_program("pg_row_lock_tracer.c")
247
+ bpf_program_final = bpf_program.replace("__DEFINES__", enum_defines)
248
+
249
+ if self.args.verbose:
250
+ print(bpf_program_final)
251
+
252
+ # Disable warnings like
253
+ # 'warning: '__HAVE_BUILTIN_BSWAP32__' macro redefined [-Wmacro-redefined]'
254
+ bpf_cflags = ["-Wno-macro-redefined"] if not self.args.verbose else []
255
+
256
+ print("===> Compiling BPF program")
257
+ self.bpf_instance = BPF(text=bpf_program_final, cflags=bpf_cflags)
258
+
259
+ print("===> Attaching BPF probes")
260
+ self.attach_probes()
261
+
262
+ # Open the event queue
263
+ self.bpf_instance["lockevents"].open_perf_buffer(
264
+ self.print_lock_event, page_cnt=BPFHelper.page_cnt
265
+ )
266
+
267
+ def attach_probes(self):
268
+ """
269
+ Attach BPF probes
270
+ """
271
+ BPFHelper.register_ebpf_probe(
272
+ self.args.path,
273
+ self.bpf_instance,
274
+ "^heapam_tuple_lock$",
275
+ "heapam_tuple_lock",
276
+ self.args.verbose,
277
+ )
278
+ BPFHelper.register_ebpf_probe(
279
+ self.args.path,
280
+ self.bpf_instance,
281
+ "^heapam_tuple_lock$",
282
+ "heapam_tuple_lock_end",
283
+ self.args.verbose,
284
+ False,
285
+ )
286
+
287
+ def print_statistics(self):
288
+ """
289
+ Print lock statistics
290
+ """
291
+ print("\nLock statistics:\n================")
292
+
293
+ # Wait policies
294
+ print("\nUsed wait policies:")
295
+ wait_polices = ["PID"]
296
+
297
+ for wait_policy in LockWaitPolicy:
298
+ wait_polices.append(wait_policy.name)
299
+
300
+ table = PrettyTable(wait_polices)
301
+
302
+ for pid in sorted(self.statistics):
303
+ statistics = self.statistics[pid]
304
+ pid_statistics = [pid]
305
+ for wait_policy in LockWaitPolicy:
306
+ pid_statistics.append(statistics.lock_policies.get(wait_policy, 0))
307
+ table.add_row(pid_statistics)
308
+ print(table)
309
+
310
+ # Lock modes
311
+ print("\nLock modes:")
312
+ lock_modes = ["PID"]
313
+
314
+ for lock_mode in LockTupleMode:
315
+ lock_modes.append(lock_mode.name)
316
+
317
+ table = PrettyTable(lock_modes)
318
+
319
+ for pid in sorted(self.statistics):
320
+ statistics = self.statistics[pid]
321
+ pid_statistics = [pid]
322
+ for lock_mode in LockTupleMode:
323
+ pid_statistics.append(statistics.lock_modes.get(lock_mode, 0))
324
+ table.add_row(pid_statistics)
325
+ print(table)
326
+
327
+ # Lock results
328
+ print("\nLock results:")
329
+ lock_results = ["PID"]
330
+
331
+ for lock_result in TMResult:
332
+ lock_results.append(lock_result.name)
333
+
334
+ table = PrettyTable(lock_results)
335
+
336
+ for pid in sorted(self.statistics):
337
+ statistics = self.statistics[pid]
338
+ pid_statistics = [pid]
339
+ for lock_result in TMResult:
340
+ pid_statistics.append(statistics.lock_results.get(lock_result, 0))
341
+ table.add_row(pid_statistics)
342
+ print(table)
343
+
344
+ def run(self):
345
+ """
346
+ Run the BPF program and read results
347
+ """
348
+ print("===> Ready to trace")
349
+ while True:
350
+ try:
351
+ self.bpf_instance.perf_buffer_poll()
352
+ except KeyboardInterrupt:
353
+ if self.args.statistics:
354
+ self.print_statistics()
355
+ sys.exit(0)
356
+
357
+
358
+ def main():
359
+ """
360
+ Entry point for the BPF based PostgreSQL row lock tracer.
361
+ """
362
+ args = parser.parse_args()
363
+
364
+ pg_lock_tracer = PGRowLockTracer(args)
365
+ pg_lock_tracer.init()
366
+
367
+ if not args.dry_run:
368
+ pg_lock_tracer.run()
369
+
370
+
371
+ if __name__ == "__main__":
372
+ main()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pg_lock_tracer
3
- Version: 0.5.5
3
+ Version: 0.6.0
4
4
  Summary: A BPF based lock tracer for the PostgreSQL database
5
5
  Home-page: https://github.com/jnidzwetzki/pg-lock-tracer
6
6
  Author: Jan Nidzwetzki
@@ -30,11 +30,12 @@ Requires-Dist: psycopg2
30
30
 
31
31
  This project provides tools that allow you to gain deep insights into PostgreSQL's locking activities and troubleshoot locking-related issues (e.g., performance problems or deadlocks).
32
32
 
33
- * `pg_lock_tracer` - is a lock tracer for PostgreSQL.
34
- * `pg_lw_lock_tracer` - is a tracer for PostgreSQL lightweight locks (LWLocks).
33
+ * `pg_lock_tracer` - is a PostgreSQL table level lock tracer.
34
+ * `pg_lw_lock_tracer` - is a tracer for PostgreSQL lightweight locks (LWLocks).
35
+ * `pg_row_lock_tracer` - is a tracer for PostgreSQL row locks.
35
36
  * `animate_lock_graph` - creates animated locks graphs based on the `pg_lock_tracer` output.
36
37
 
37
- __Note:__ Most of these tools employ the [BPF / eBPF](https://ebpf.io/) (_Extended Berkeley Packet Filter_) technology. At the moment, PostgreSQL 12, 13, 14, and 15 are supported (see additional information below).
38
+ __Note:__ These tools employ the [eBPF](https://ebpf.io/) (_Extended Berkeley Packet Filter_) technology. At the moment, PostgreSQL 12, 13, 14, 15, and 16 are supported (see additional information below).
38
39
 
39
40
  # pg_lock_tracer
40
41
  `pg_lock_tracer` observes the locking activity of a running PostgreSQL process (using _eBPF_ and _UProbes_). In contrast to the information that is present in the table `pg_locks` (which provides information about which locks are _currently_ requested), `pg_lock_tracer` gives you a continuous view of the locking activity and collects statistics and timings.
@@ -686,7 +687,7 @@ pg_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_2_DEBUG/bin/postgres -
686
687
  ### Animated Lock Graphs
687
688
  See the content of the [examples](examples/) directory for examples.
688
689
 
689
- # pg_lw_lock_trace
690
+ # pg_lw_lock_tracer
690
691
 
691
692
  `pg_lw_lock_trace` allows to trace lightweight locks ([LWLocks](https://github.com/postgres/postgres/blob/c8e1ba736b2b9e8c98d37a5b77c4ed31baf94147/src/backend/storage/lmgr/lwlock.c)) in a PostgreSQL process via _Userland Statically Defined Tracing_ (USDT).
692
693
 
@@ -805,8 +806,73 @@ Locks per type
805
806
  +--------------+----------+
806
807
  ```
807
808
 
808
- # Additional Information
809
+ # pg_row_lock_tracer
810
+
811
+ `pg_row_lock_tracer` allows to trace row locks (see the PostgreSQL [documentation](https://www.postgresql.org/docs/current/explicit-locking.html#LOCKING-ROWS)) of a PostgreSQL process using _eBPF_ and _UProbes_
812
+
813
+ ## Usage Examples
814
+ ```
815
+ # Trace the row locks of the given PostgreSQL binary
816
+ pg_row_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
817
+
818
+ # Trace the row locks of the PID 1234
819
+ pg_row_lock_tracer -p 1234 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
820
+
821
+ # Trace the row locks of the PID 1234 and 5678
822
+ pg_row_lock_tracer -p 1234 -p 5678 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres
823
+
824
+ # Trace the row locks of the PID 1234 and be verbose
825
+ pg_row_lock_tracer -p 1234 -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres -v
826
+
827
+ # Trace the row locks and show statistics
828
+ pg_row_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres --statistics
829
+ ```
830
+
831
+ ## Example output
832
+
833
+ SQL Query: `SELECT * FROM temperature FOR UPDATE;`
809
834
 
835
+ CLI: `sudo pg_row_lock_tracer -x /home/jan/postgresql-sandbox/bin/REL_14_9_DEBUG/bin/postgres --statistics`
836
+
837
+
838
+ Tracer output:
839
+
840
+ ```
841
+ [...]
842
+ 2783502701862408 [Pid 2604491] LOCK_TUPLE_END TM_OK in 13100 ns
843
+ 2783502701877081 [Pid 2604491] LOCK_TUPLE (Tablespace 1663 database 305234 relation 313419) - (Block and offset 7 143) - LOCK_TUPLE_EXCLUSIVE LOCK_WAIT_BLOCK
844
+ 2783502701972367 [Pid 2604491] LOCK_TUPLE_END TM_OK in 95286 ns
845
+ 2783502701988387 [Pid 2604491] LOCK_TUPLE (Tablespace 1663 database 305234 relation 313419) - (Block and offset 7 144) - LOCK_TUPLE_EXCLUSIVE LOCK_WAIT_BLOCK
846
+ 2783502702001690 [Pid 2604491] LOCK_TUPLE_END TM_OK in 13303 ns
847
+ 2783502702016387 [Pid 2604491] LOCK_TUPLE (Tablespace 1663 database 305234 relation 313419) - (Block and offset 7 145) - LOCK_TUPLE_EXCLUSIVE LOCK_WAIT_BLOCK
848
+ 2783502702029375 [Pid 2604491] LOCK_TUPLE_END TM_OK in 12988 ns
849
+ ^C
850
+ Lock statistics:
851
+ ================
852
+
853
+ Used wait policies:
854
+ +---------+-----------------+----------------+-----------------+
855
+ | PID | LOCK_WAIT_BLOCK | LOCK_WAIT_SKIP | LOCK_WAIT_ERROR |
856
+ +---------+-----------------+----------------+-----------------+
857
+ | 2604491 | 1440 | 0 | 0 |
858
+ +---------+-----------------+----------------+-----------------+
859
+
860
+ Lock modes:
861
+ +---------+---------------------+------------------+---------------------------+----------------------+
862
+ | PID | LOCK_TUPLE_KEYSHARE | LOCK_TUPLE_SHARE | LOCK_TUPLE_NOKEYEXCLUSIVE | LOCK_TUPLE_EXCLUSIVE |
863
+ +---------+---------------------+------------------+---------------------------+----------------------+
864
+ | 2604491 | 0 | 0 | 0 | 1440 |
865
+ +---------+---------------------+------------------+---------------------------+----------------------+
866
+
867
+ Lock results:
868
+ +---------+-------+--------------+-----------------+------------+------------+------------------+---------------+
869
+ | PID | TM_OK | TM_INVISIBLE | TM_SELFMODIFIED | TM_UPDATED | TM_DELETED | TM_BEINGMODIFIED | TM_WOULDBLOCK |
870
+ +---------+-------+--------------+-----------------+------------+------------+------------------+---------------+
871
+ | 2604491 | 1440 | 0 | 0 | 0 | 0 | 0 | 0 |
872
+ +---------+-------+--------------+-----------------+------------+------------+------------------+---------------+
873
+ ```
874
+
875
+ # Additional Information
810
876
 
811
877
  ## Installation
812
878
 
@@ -0,0 +1,17 @@
1
+ pg_lock_tracer/__init__.py,sha256=cID1jLnC_vj48GgMN6Yb1FA3JsQ95zNmCHmRYE8TFhY,22
2
+ pg_lock_tracer/animate_lock_graph.py,sha256=ohRKBlKILx7f2I0sBC26UYEG7mIbrNcDttmIwR8Aokk,12033
3
+ pg_lock_tracer/helper.py,sha256=4l_H6rCXShPu-snBA_OqCgKTzHUYEbgWK3zhmi6WJ9M,5271
4
+ pg_lock_tracer/oid_resolver.py,sha256=K0oF7SRZfqm092Tc_pZ-RlCpfHtnspwhjX1meTsU8Fk,3534
5
+ pg_lock_tracer/pg_lock_tracer.py,sha256=OohbLE1xSUbHiU3lSGJYACUgAVtVoK1V0Ew7kD_smlQ,30060
6
+ pg_lock_tracer/pg_lw_lock_tracer.py,sha256=Jgsr9ArWQigj-ndeAKEObVIYyNIaTKNGIkDrtA8sj9w,13204
7
+ pg_lock_tracer/pg_row_lock_tracer.py,sha256=duQalO2vf-QX4WRFd6K0WRunFlZbTHZeVkeWkr35zbo,10405
8
+ pg_lock_tracer/bpf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ pg_lock_tracer/bpf/pg_lock_tracer.c,sha256=EM9gFZtkeBROIM692s-DrD-xlW0khFRNLXPN5_nBFBg,14003
10
+ pg_lock_tracer/bpf/pg_lw_lock_tracer.c,sha256=BxEpEOCo7hLJSOjcsKkng3kZhYJ80A_6k-9nxaJ6e2A,4916
11
+ pg_lock_tracer/bpf/pg_row_lock_tracer.c,sha256=U2Uqtv68qi72bJCVNHdpO68QTVJzhUFRATHX4hCWzDY,3344
12
+ pg_lock_tracer-0.6.0.dist-info/LICENSE,sha256=opL3U54Vdmg02lhP1XzHATC9Fyy393DcZBi-HqotSlQ,11348
13
+ pg_lock_tracer-0.6.0.dist-info/METADATA,sha256=1Hc_aUDwBCatzVTNJVBsev9FAmSPN1pkQrFkMrbNatE,73024
14
+ pg_lock_tracer-0.6.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
15
+ pg_lock_tracer-0.6.0.dist-info/entry_points.txt,sha256=kCKw8Zbn3YXKdu8mBeVzIzOmGoLjD-5D40CZg8-_W9c,248
16
+ pg_lock_tracer-0.6.0.dist-info/top_level.txt,sha256=nRoLeqWT0GSR4-JDcVRuI5ltF69rs7ftfkvImNbiR1Q,15
17
+ pg_lock_tracer-0.6.0.dist-info/RECORD,,
@@ -2,3 +2,4 @@
2
2
  animate_lock_graph = pg_lock_tracer.animate_lock_graph:main
3
3
  pg_lock_tracer = pg_lock_tracer.pg_lock_tracer:main
4
4
  pg_lw_lock_tracer = pg_lock_tracer.pg_lw_lock_tracer:main
5
+ pg_row_lock_tracer = pg_lock_tracer.pg_row_lock_tracer:main
@@ -1,15 +0,0 @@
1
- pg_lock_tracer/__init__.py,sha256=78mfpLewKVki6c9UONSUdlVme_JsN9ZwIfp4Hf4jmG0,22
2
- pg_lock_tracer/animate_lock_graph.py,sha256=ohRKBlKILx7f2I0sBC26UYEG7mIbrNcDttmIwR8Aokk,12033
3
- pg_lock_tracer/helper.py,sha256=bWxYVa65q6DXBhR9PIE3DVaZ2u08rsLehnluPRsOH3Y,3475
4
- pg_lock_tracer/oid_resolver.py,sha256=K0oF7SRZfqm092Tc_pZ-RlCpfHtnspwhjX1meTsU8Fk,3534
5
- pg_lock_tracer/pg_lock_tracer.py,sha256=KSkF7onGy6M5PYjuIxxc3GMUFXqcAebROot6_yg2rxw,28308
6
- pg_lock_tracer/pg_lw_lock_tracer.py,sha256=77y_vKdLoyNPigkb8cd9WOXGrccStUUGcJV4iS_uFgE,13502
7
- pg_lock_tracer/bpf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- pg_lock_tracer/bpf/pg_lock_tracer.c,sha256=EM9gFZtkeBROIM692s-DrD-xlW0khFRNLXPN5_nBFBg,14003
9
- pg_lock_tracer/bpf/pg_lw_lock_tracer.c,sha256=BxEpEOCo7hLJSOjcsKkng3kZhYJ80A_6k-9nxaJ6e2A,4916
10
- pg_lock_tracer-0.5.5.dist-info/LICENSE,sha256=opL3U54Vdmg02lhP1XzHATC9Fyy393DcZBi-HqotSlQ,11348
11
- pg_lock_tracer-0.5.5.dist-info/METADATA,sha256=P6BMCgUqG6rgwM2ERWlXmGE-Y-e7YoKKVI1W0miitFQ,69560
12
- pg_lock_tracer-0.5.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
13
- pg_lock_tracer-0.5.5.dist-info/entry_points.txt,sha256=D5A-ZxfF80GaiGtUFQyvmXrJIxLn-CcIIw8G4E95y0c,188
14
- pg_lock_tracer-0.5.5.dist-info/top_level.txt,sha256=nRoLeqWT0GSR4-JDcVRuI5ltF69rs7ftfkvImNbiR1Q,15
15
- pg_lock_tracer-0.5.5.dist-info/RECORD,,