Skip to content

Commit 42d7e85

Browse files
committed
Optimize callback in Pileup for reduced overhead and improved performance
1 parent 9e3da71 commit 42d7e85

2 files changed

Lines changed: 63 additions & 43 deletions

File tree

lib/hts/bam/mpileup.rb

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ def initialize(inputs, region: nil, beg: nil, end_: nil, maxcnt: nil, overlaps:
2626
end
2727

2828
n = @bams.length
29-
@state_map = {}
30-
@handles = []
31-
@iters = []
29+
@iters = []
30+
@data_blocks = [] # per-input packed pointers kept alive
3231

3332
# Prepare optional region iterators for each input
3433
@bams.each_with_index do |bam, i|
@@ -50,32 +49,38 @@ def initialize(inputs, region: nil, beg: nil, end_: nil, maxcnt: nil, overlaps:
5049
@iters << itr
5150
end
5251

53-
# Build per-input handle pointers so C passes them back to the callback
52+
# Build per-input packed pointer blocks so C passes them back to the callback.
53+
# Layout per input: [0] hts_fp (htsFile*), [1] hdr_struct (bam_hdr_t*), [2] itr (hts_itr_t* or NULL)
54+
ptr_size = FFI.type_size(:pointer)
5455
data_array = FFI::MemoryPointer.new(:pointer, n)
5556
@bams.each_with_index do |bam, i|
56-
handle = FFI::MemoryPointer.new(:char, 1) # unique address token
57-
@handles << handle
58-
@state_map[handle.address] = { bam:, itr: @iters[i] }
59-
data_array.put_pointer(i * FFI.type_size(:pointer), handle)
57+
hts_fp = bam.instance_variable_get(:@hts_file)
58+
hdr_struct = bam.header.struct
59+
itr = @iters[i]
60+
block = FFI::MemoryPointer.new(:pointer, 3)
61+
block.put_pointer(0 * ptr_size, hts_fp)
62+
block.put_pointer(1 * ptr_size, hdr_struct)
63+
block.put_pointer(2 * ptr_size, itr && !itr.null? ? itr : FFI::Pointer::NULL)
64+
@data_blocks << block
65+
data_array.put_pointer(i * ptr_size, block)
6066
end
61-
# Keep the array of per-input handles alive while the C side holds on to them
67+
# Keep the array of per-input blocks alive while the C side holds on to them
6268
@data_array = data_array
6369

6470
@cb = FFI::Function.new(:int, %i[pointer pointer]) do |data, b|
65-
st = @state_map[data.address]
66-
bam = st[:bam]
67-
itr = st[:itr]
71+
# Unpack pointers from the per-input block
72+
hts_fp = data.get_pointer(0 * ptr_size)
73+
hdr_struct = data.get_pointer(1 * ptr_size)
74+
itr = data.get_pointer(2 * ptr_size)
6875
if itr && !itr.null?
69-
slen = HTS::LibHTS.sam_itr_next(bam.instance_variable_get(:@hts_file), itr, b)
70-
if slen > 0
76+
r = HTS::LibHTS.sam_itr_next(hts_fp, itr, b)
77+
if r >= 0
7178
0
72-
elsif slen == -1
73-
-1
7479
else
75-
-2
80+
(r == -1 ? -1 : -2)
7681
end
7782
else
78-
r = HTS::LibHTS.sam_read1(bam.instance_variable_get(:@hts_file), bam.header.struct, b)
83+
r = HTS::LibHTS.sam_read1(hts_fp, hdr_struct, b)
7984
r == -1 ? -1 : 0
8085
end
8186
end
@@ -99,6 +104,8 @@ def each
99104
pos_ptr = FFI::MemoryPointer.new(:long_long)
100105
n_ptr = FFI::MemoryPointer.new(:int, n)
101106
plp_ptr = FFI::MemoryPointer.new(:pointer, n)
107+
plp1_size = HTS::LibHTS::BamPileup1.size
108+
headers = @bams.map(&:header)
102109

103110
begin
104111
while HTS::LibHTS.bam_mplp64_auto(@iter, tid_ptr, pos_ptr, n_ptr, plp_ptr) > 0
@@ -108,20 +115,25 @@ def each
108115
counts = n_ptr.read_array_of_int(n)
109116
plp_arr = plp_ptr.read_array_of_pointer(n)
110117

111-
cols = Array.new(n) do |i|
118+
cols = Array.new(n)
119+
i = 0
120+
while i < n
112121
c = counts[i]
113122
if c <= 0 || plp_arr[i].null?
114-
HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: [])
123+
cols[i] = HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: [])
115124
else
116125
base_ptr = plp_arr[i]
117-
size = HTS::LibHTS::BamPileup1.size
118-
aligns = c.times.map do |j|
119-
e_ptr = base_ptr + (j * size)
126+
aligns = Array.new(c)
127+
j = 0
128+
while j < c
129+
e_ptr = base_ptr + (j * plp1_size)
120130
entry = HTS::LibHTS::BamPileup1.new(e_ptr)
121-
HTS::Bam::Pileup::PileupRecord.new(entry, @bams[i].header)
131+
aligns[j] = HTS::Bam::Pileup::PileupRecord.new(entry, headers[i])
132+
j += 1
122133
end
123-
HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: aligns)
134+
cols[i] = HTS::Bam::Pileup::PileupColumn.new(tid: tid, pos: pos, alignments: aligns)
124135
end
136+
i += 1
125137
end
126138

127139
yield cols
@@ -142,8 +154,8 @@ def close
142154
HTS::LibHTS.hts_itr_destroy(itr) if itr && !itr.null?
143155
end
144156
@iters.clear
145-
# Keep references to callback and handles to prevent GC
146-
@_keepalive = [@cb, *@handles]
157+
# Keep references to callback and data blocks to prevent GC
158+
@_keepalive = [@cb, @data_array, *@data_blocks]
147159
# Close owned bams opened by this object
148160
@owned_bams.each do |b|
149161
b.close

lib/hts/bam/pileup.rb

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -91,22 +91,30 @@ def initialize(bam, region: nil, beg: nil, end_: nil, maxcnt: nil)
9191
raise ArgumentError, "beg and end_ must be specified together"
9292
end
9393

94-
# Build the auto callback for bam_plp_init
95-
@cb = FFI::Function.new(:int, %i[pointer pointer]) do |_data, b|
96-
if @itr && !@itr.null?
97-
slen = HTS::LibHTS.sam_itr_next(@bam.instance_variable_get(:@hts_file), @itr, b)
98-
if slen > 0
99-
0
100-
elsif slen == -1
101-
-1
102-
else
103-
-2
104-
end
105-
else
106-
r = HTS::LibHTS.sam_read1(@bam.instance_variable_get(:@hts_file), @header.struct, b)
107-
r == -1 ? -1 : 0
108-
end
109-
end
94+
# Build the auto callback for bam_plp_init (micro-optimized)
95+
# - Hoist ivar/constant lookups out of the callback to reduce per-call overhead.
96+
# - Specialize callbacks to avoid branching in the hot path.
97+
hts_fp = @bam.instance_variable_get(:@hts_file)
98+
hdr_struct = @header.struct
99+
itr_local = @itr
100+
101+
@cb = if itr_local && !itr_local.null?
102+
FFI::Function.new(:int, %i[pointer pointer]) do |_data, b|
103+
# HTSlib contract: sam_itr_next returns >= 0 on success, -1 on EOF, < -1 on error.
104+
r = HTS::LibHTS.sam_itr_next(hts_fp, itr_local, b)
105+
if r >= 0
106+
0
107+
else
108+
(r == -1 ? -1 : -2)
109+
end
110+
end
111+
else
112+
FFI::Function.new(:int, %i[pointer pointer]) do |_data, b|
113+
# HTSlib contract: sam_read1 returns >= 0 on success, -1 on EOF/error.
114+
r = HTS::LibHTS.sam_read1(hts_fp, hdr_struct, b)
115+
r == -1 ? -1 : 0
116+
end
117+
end
110118

111119
@plp = HTS::LibHTS.bam_plp_init(@cb, nil)
112120
raise "bam_plp_init failed" if @plp.null?

0 commit comments

Comments
 (0)