Skip to main content

blvm_consensus/
checkqueue.rs

1//! Script verification queue: dedicated N-1 worker threads plus master as Nth.
2//!
3//! Producer adds checks per-tx; workers pull batches and verify. Master joins until
4//! queue empty. Batch size from ibd_tuning (default 128, env/config override).
5
6#![cfg(all(feature = "production", feature = "rayon"))]
7
8use crate::activation::{ForkActivationTable, IsForkActive};
9use crate::error::{ConsensusError, Result};
10use crate::script::verify_script_with_context_full;
11use crate::types::{Block, ForkId, Natural, Network};
12use crate::witness::Witness;
13use crate::witness::is_witness_empty;
14use crossbeam_queue::SegQueue;
15
16use std::cell::RefCell;
17use std::sync::atomic::AtomicUsize;
18use std::sync::{Arc, Condvar, Mutex};
19use std::thread::{self, JoinHandle};
20
21fn lock_poison_recovery<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
22    mutex.lock().unwrap_or_else(|e| e.into_inner())
23}
24
25fn condvar_wait_poison_recovery<'a, T>(
26    cv: &Condvar,
27    guard: std::sync::MutexGuard<'a, T>,
28) -> std::sync::MutexGuard<'a, T> {
29    cv.wait(guard).unwrap_or_else(|e| e.into_inner())
30}
31
32/// Default batch size when not overridden.
33const DEFAULT_BATCH_SIZE: usize = 128;
34
35/// Thread-local HashMaps reused across batches to avoid per-batch allocations.
36/// all_refs cannot be reused because it holds refs into buffer which is batch-scoped.
37thread_local! {
38    static PREVOUT_BUF: RefCell<Vec<i64>> = const { RefCell::new(Vec::new()) };
39}
40
41/// Script check with embedded per-input data. Workers use these directly without
42/// HashMap grouping or shared-buffer indirection (self-contained script checks).
43#[derive(Clone, Debug)]
44pub struct ScriptCheck {
45    pub tx_ctx_idx: usize,
46    pub input_idx: usize,
47    pub spk_offset: u32,
48    pub spk_len: u32,
49    pub prevout_value: i64,
50}
51
52/// Per-tx context shared by all inputs of that tx.
53/// prevout_script_pubkeys and prevout_values in block-level buffers;
54/// context holds (start, count) ranges to avoid per-tx Vec allocations.
55#[derive(Clone, Debug)]
56pub struct TxScriptContext {
57    pub tx_index: usize,
58    /// (start, count) range into BlockSessionContext.prevout_values_buffer
59    pub prevout_values_range: (usize, usize),
60    /// (start, count) range into BlockSessionContext.script_pubkey_indices_buffer
61    pub script_pubkey_indices_range: (usize, usize),
62    pub flags: u32,
63    #[cfg(feature = "production")]
64    pub bip143: Option<crate::transaction_hash::Bip143PrecomputedHashes>,
65    pub loop_idx: usize,
66    pub fee: i64,
67    pub ecdsa_index_base: usize,
68    /// Roadmap: (scriptCode, nHashType) -> hash cache. Helps multisig.
69    #[cfg(feature = "production")]
70    pub sighash_midstate_cache: Option<crate::transaction_hash::SighashMidstateCache>,
71}
72
73/// Session context for one block; set at start_session, used by workers until complete.
74/// When schnorr_collector is None, verify in-place (no batch collection).
75/// TxScriptContext stored in Arc to avoid cloning full context per check (workers get Arc::clone).
76/// Block-level buffers are immutable (Arc<Vec<...>>) — no locks during worker execution.
77pub struct BlockSessionContext {
78    pub block: Arc<Block>,
79    /// Block-level buffer for all prevout values; TxScriptContext.prevout_values_range indexes into this.
80    pub prevout_values_buffer: Arc<Vec<i64>>,
81    /// Block-level buffer for (start, len) pairs into script_pubkey_buffer; TxScriptContext.script_pubkey_indices_range indexes into this.
82    pub script_pubkey_indices_buffer: Arc<Vec<(usize, usize)>>,
83    /// Block-level buffer for all prevout script_pubkeys; (start, len) from script_pubkey_indices_buffer index into this.
84    pub script_pubkey_buffer: Arc<Vec<u8>>,
85    /// Block-level witness data; workers index via `session.witness_buffer[ctx.tx_index][input_idx]`.
86    pub witness_buffer: Arc<Vec<Vec<Witness>>>,
87    pub tx_contexts: Arc<Vec<TxScriptContext>>,
88    #[cfg(feature = "production")]
89    pub ecdsa_sub_counters: Arc<Vec<AtomicUsize>>,
90    #[cfg(feature = "production")]
91    pub schnorr_collector: Option<Arc<crate::bip348::SchnorrSignatureCollector>>,
92    pub height: Natural,
93    pub median_time_past: Option<u64>,
94    pub network: Network,
95    /// Fork heights for this validation session (same table as `BlockValidationContext::activation`).
96    pub activation: ForkActivationTable,
97    /// Lock-free: workers push batch_results; master drains at complete(). Reduces Mutex contention.
98    pub results: Arc<SegQueue<Vec<(usize, bool)>>>,
99    /// Precomputed sighashes for P2PKH inputs, indexed by ecdsa_index_base + input_idx.
100    /// None = not precomputed (worker computes on demand).
101    #[cfg(feature = "production")]
102    pub precomputed_sighashes: Arc<Vec<Option<[u8; 32]>>>,
103    /// Precomputed HASH160(pubkey) for P2PKH inputs, indexed same as precomputed_sighashes.
104    #[cfg(feature = "production")]
105    pub precomputed_p2pkh_hashes: Arc<Vec<Option<[u8; 20]>>>,
106}
107
108struct QueueState {
109    checks: Vec<ScriptCheck>,
110    n_todo: usize,
111    n_total: usize,
112    n_idle: usize,
113    /// Total checks submitted in the current block session; used to pre-size the result Vec in `complete()`.
114    n_submitted: usize,
115    error_result: Option<ConsensusError>,
116    request_stop: bool,
117    session: Option<Arc<BlockSessionContext>>,
118}
119
120/// Script verification queue: N-1 dedicated workers + master joins.
121pub struct ScriptCheckQueue {
122    state: Arc<Mutex<QueueState>>,
123    worker_cv: Arc<Condvar>,
124    master_cv: Arc<Condvar>,
125    control_mutex: Mutex<()>,
126    _workers: Vec<JoinHandle<()>>,
127    batch_size: usize,
128}
129
130impl ScriptCheckQueue {
131    /// Create queue with `worker_count` dedicated threads (par-1; master joins for total par).
132    /// `batch_size`: from ibd_tuning (default 128). Use `None` for default.
133    pub fn new(worker_count: usize, batch_size: Option<usize>) -> Self {
134        let batch_size = batch_size
135            .filter(|&b| b > 0 && b <= 1024)
136            .unwrap_or(DEFAULT_BATCH_SIZE);
137        let state = Arc::new(Mutex::new(QueueState {
138            checks: Vec::new(),
139            n_todo: 0,
140            n_total: 0,
141            n_idle: 0,
142            n_submitted: 0,
143            error_result: None,
144            request_stop: false,
145            session: None,
146        }));
147        let worker_cv = Arc::new(Condvar::new());
148        let master_cv = Arc::new(Condvar::new());
149
150        let mut workers = Vec::with_capacity(worker_count);
151        for n in 0..worker_count {
152            let state_clone = Arc::clone(&state);
153            let cv_clone = Arc::clone(&worker_cv);
154            let master_clone = Arc::clone(&master_cv);
155            let bs = batch_size;
156            workers.push(
157                thread::Builder::new()
158                    .name(format!("scriptch.{n}"))
159                    .spawn(move || {
160                        Self::worker_loop(state_clone, &cv_clone, &master_clone, bs);
161                    })
162                    .expect("scriptch thread spawn"),
163            );
164        }
165
166        Self {
167            state,
168            worker_cv,
169            master_cv,
170            control_mutex: Mutex::new(()),
171            _workers: workers,
172            batch_size,
173        }
174    }
175
176    /// Run a single check with pre-built refs (used when refs are cached per tx_ctx).
177    /// p2pkh_hash: when Some, P2PKH fast path skips HASH160 (batch path).
178    /// When script_pubkey and prevout_values are Some, skips per-check lock acquisitions (batch path).
179    pub fn run_check_with_refs(
180        check: &ScriptCheck,
181        session: &BlockSessionContext,
182        ctx: &TxScriptContext,
183        refs: &[&[u8]],
184        buffer: &[u8],
185        #[cfg(feature = "production")] p2pkh_hash: Option<[u8; 20]>,
186        script_pubkey_prefetched: Option<&[u8]>,
187        prevout_values_prefetched: Option<&[i64]>,
188    ) -> std::result::Result<bool, ConsensusError> {
189        let tx = &session.block.transactions[ctx.tx_index];
190        let script_pubkey: &[u8] = match script_pubkey_prefetched {
191            Some(s) => s,
192            None => {
193                let spi = session.script_pubkey_indices_buffer.as_slice();
194                let (base, count) = ctx.script_pubkey_indices_range;
195                let (start, len) = if check.input_idx < count {
196                    spi[base + check.input_idx]
197                } else {
198                    (0, 0)
199                };
200                &buffer[start..start + len]
201            }
202        };
203        let witness_for_script = if !session
204            .activation
205            .is_fork_active(ForkId::SegWit, session.height)
206        {
207            None
208        } else {
209            session
210                .witness_buffer
211                .get(ctx.tx_index)
212                .and_then(|w| w.get(check.input_idx))
213                .and_then(|w| if is_witness_empty(w) { None } else { Some(w) })
214        };
215        let ecdsa_global_idx = ctx.ecdsa_index_base + check.input_idx;
216
217        #[cfg(feature = "production")]
218        let sighash_cache = ctx.sighash_midstate_cache.as_ref();
219
220        #[cfg(feature = "production")]
221        let precomputed_sighash = session
222            .precomputed_sighashes
223            .get(ecdsa_global_idx)
224            .and_then(|s| *s);
225        #[cfg(feature = "production")]
226        let precomputed_p2pkh = match p2pkh_hash {
227            Some(h) => Some(h),
228            None => session
229                .precomputed_p2pkh_hashes
230                .get(ecdsa_global_idx)
231                .and_then(|h| *h),
232        };
233
234        let do_verify = |prevout_values: &[i64]| {
235            verify_script_with_context_full(
236                &tx.inputs[check.input_idx].script_sig,
237                script_pubkey,
238                witness_for_script,
239                ctx.flags,
240                tx,
241                check.input_idx,
242                prevout_values,
243                refs,
244                Some(session.height),
245                session.median_time_past,
246                session.network,
247                crate::script::SigVersion::Base,
248                #[cfg(feature = "production")]
249                session.schnorr_collector.as_deref(),
250                #[cfg(not(feature = "production"))]
251                None,
252                #[cfg(feature = "production")]
253                ctx.bip143.as_ref(),
254                #[cfg(not(feature = "production"))]
255                None,
256                #[cfg(feature = "production")]
257                precomputed_sighash,
258                #[cfg(feature = "production")]
259                sighash_cache,
260                #[cfg(feature = "production")]
261                precomputed_p2pkh,
262            )
263            .map_err(|e| {
264                ConsensusError::BlockValidation(
265                    format!(
266                        "Script verification failed at tx {} input {}: {}",
267                        ctx.tx_index, check.input_idx, e
268                    )
269                    .into(),
270                )
271            })
272        };
273
274        match prevout_values_prefetched {
275            Some(p) => do_verify(p),
276            None => {
277                let pv = session.prevout_values_buffer.as_slice();
278                let (base, count) = ctx.prevout_values_range;
279                let slice = &pv[base..][..count];
280                PREVOUT_BUF.with(|cell| {
281                    let mut v = cell.borrow_mut();
282                    v.clear();
283                    v.extend_from_slice(slice);
284                    do_verify(&v)
285                })
286            }
287        }
288    }
289
290    fn run_check<'a>(
291        check: &ScriptCheck,
292        session: &'a BlockSessionContext,
293        refs_buf: &mut Vec<&'a [u8]>,
294    ) -> std::result::Result<bool, ConsensusError> {
295        let ctx = session
296            .tx_contexts
297            .get(check.tx_ctx_idx)
298            .ok_or_else(|| ConsensusError::BlockValidation("tx_ctx_idx out of range".into()))?;
299        let buffer = session.script_pubkey_buffer.as_slice();
300        let spi = session.script_pubkey_indices_buffer.as_slice();
301        let (base, count) = ctx.script_pubkey_indices_range;
302        refs_buf.clear();
303        refs_buf.extend((0..count).map(|j| {
304            let (s, l) = spi[base + j];
305            buffer[s..s + l].as_ref()
306        }));
307        Self::run_check_with_refs(
308            check, session, ctx, refs_buf, buffer, None, // run_check has no batch context
309            None, // script_pubkey_prefetched
310            None, // prevout_values_prefetched
311        )
312    }
313
314    fn worker_loop(
315        state: Arc<Mutex<QueueState>>,
316        worker_cv: &Condvar,
317        master_cv: &Condvar,
318        batch_size: usize,
319    ) {
320        let mut n_now: usize = 0;
321        let mut local_error: Option<ConsensusError> = None;
322        let mut batch_buf: Vec<ScriptCheck> = Vec::with_capacity(batch_size);
323
324        loop {
325            let (session_opt, _batch_len) = {
326                let mut guard = lock_poison_recovery(state.as_ref());
327                if n_now > 0 {
328                    if let Some(ref err) = local_error {
329                        if guard.error_result.is_none() {
330                            guard.error_result = Some(err.clone());
331                        }
332                    }
333                    guard.n_todo -= n_now;
334                    if guard.n_todo == 0 {
335                        master_cv.notify_one();
336                    }
337                    n_now = 0;
338                    local_error = None;
339                } else {
340                    guard.n_total += 1;
341                }
342
343                loop {
344                    if guard.request_stop {
345                        return;
346                    }
347                    if guard.checks.is_empty() {
348                        guard.n_idle += 1;
349                        guard = condvar_wait_poison_recovery(worker_cv, guard);
350                        guard.n_idle -= 1;
351                        continue;
352                    }
353                    break;
354                }
355
356                let n_total = guard.n_total;
357                let n_idle = guard.n_idle;
358                let divisor = (n_total + n_idle + 1).max(1);
359                n_now = (guard.checks.len() / divisor).clamp(1, batch_size);
360                let drain_len = n_now.min(guard.checks.len());
361                batch_buf.clear();
362                let drain_start = guard.checks.len() - drain_len;
363                batch_buf.extend(guard.checks.drain(drain_start..));
364                let session = guard.session.clone();
365                (session, ())
366            };
367
368            if batch_buf.is_empty() {
369                continue;
370            }
371
372            let session = match session_opt.as_ref() {
373                Some(s) => Arc::clone(s),
374                None => continue,
375            };
376
377            let mut batch_results = Vec::with_capacity(batch_buf.len());
378            #[cfg(all(feature = "production", feature = "profile"))]
379            let t_run_check = std::time::Instant::now();
380            {
381                batch_buf.sort_unstable_by_key(|c| c.tx_ctx_idx);
382                let buffer = session.script_pubkey_buffer.as_slice();
383                let spi = session.script_pubkey_indices_buffer.as_slice();
384                let pv = session.prevout_values_buffer.as_slice();
385                let mut refs_buf: Vec<&[u8]> = Vec::with_capacity(64);
386                let mut cached_ctx_idx: usize = usize::MAX;
387                for c in batch_buf.iter() {
388                    let ctx = match session.tx_contexts.get(c.tx_ctx_idx) {
389                        Some(ctx) => ctx,
390                        None => {
391                            local_error = Some(ConsensusError::BlockValidation(
392                                "tx_ctx_idx out of range".into(),
393                            ));
394                            break;
395                        }
396                    };
397                    let s = c.spk_offset as usize;
398                    let l = c.spk_len as usize;
399                    let script_pubkey = if s + l <= buffer.len() {
400                        &buffer[s..s + l]
401                    } else {
402                        &[]
403                    };
404                    let (pv_base, pv_count) = ctx.prevout_values_range;
405                    let prevout_slice = &pv[pv_base..][..pv_count];
406                    if c.tx_ctx_idx != cached_ctx_idx {
407                        refs_buf.clear();
408                        let (spi_base, spi_count) = ctx.script_pubkey_indices_range;
409                        for j in 0..spi_count {
410                            let (start, len) = spi[spi_base + j];
411                            refs_buf.push(if start + len <= buffer.len() {
412                                &buffer[start..start + len]
413                            } else {
414                                &[]
415                            });
416                        }
417                        cached_ctx_idx = c.tx_ctx_idx;
418                    }
419                    match Self::run_check_with_refs(
420                        c,
421                        session.as_ref(),
422                        ctx,
423                        &refs_buf,
424                        buffer,
425                        None,
426                        Some(script_pubkey),
427                        Some(prevout_slice),
428                    ) {
429                        Ok(valid) => batch_results.push((c.tx_ctx_idx, valid)),
430                        Err(e) => {
431                            local_error = Some(e);
432                            break;
433                        }
434                    }
435                }
436            }
437            #[cfg(all(feature = "production", feature = "profile"))]
438            crate::script_profile::add_worker_run_check_loop_ns(
439                t_run_check.elapsed().as_nanos() as u64
440            );
441            if !batch_results.is_empty() {
442                #[cfg(all(feature = "production", feature = "profile"))]
443                let t_results = std::time::Instant::now();
444                session.results.push(batch_results);
445                #[cfg(all(feature = "production", feature = "profile"))]
446                crate::script_profile::add_worker_results_extend_ns(
447                    t_results.elapsed().as_nanos() as u64
448                );
449            }
450        }
451    }
452
453    /// Start a block session. Must be called before any Add. Session holds shared context.
454    pub fn start_session(&self, session: BlockSessionContext) {
455        let mut guard = lock_poison_recovery(&self.state);
456        guard.session = Some(Arc::new(session));
457        guard.checks.clear();
458        guard.n_todo = 0;
459        guard.n_submitted = 0;
460        guard.error_result = None;
461    }
462
463    /// Add checks to the queue; workers wake and process.
464    pub fn add(&self, checks: Vec<ScriptCheck>) {
465        let n = checks.len();
466        if n == 0 {
467            return;
468        }
469        {
470            let mut guard = lock_poison_recovery(&self.state);
471            guard.checks.extend(checks);
472            guard.n_todo += n;
473            guard.n_submitted += n;
474        }
475        if n == 1 {
476            self.worker_cv.notify_one();
477        } else {
478            self.worker_cv.notify_all();
479        }
480    }
481
482    /// Add checks from a slice without consuming. Used with block-level pre-allocated Vec (Q).
483    pub fn add_from_slice(&self, checks: &[ScriptCheck]) {
484        let n = checks.len();
485        if n == 0 {
486            return;
487        }
488        {
489            let mut guard = lock_poison_recovery(&self.state);
490            guard.checks.extend(checks.iter().cloned());
491            guard.n_todo += n;
492            guard.n_submitted += n;
493        }
494        if n == 1 {
495            self.worker_cv.notify_one();
496        } else {
497            self.worker_cv.notify_all();
498        }
499    }
500
501    /// Run checks sequentially on the current thread (fallback when parallel retry fails).
502    pub fn run_checks_sequential(
503        checks: &[ScriptCheck],
504        session: &BlockSessionContext,
505    ) -> Result<Vec<(usize, bool)>> {
506        let mut results = Vec::with_capacity(checks.len());
507        let mut refs_buf = Vec::with_capacity(256);
508        for c in checks {
509            let valid = Self::run_check(c, session, &mut refs_buf)?;
510            results.push((c.tx_ctx_idx, valid));
511        }
512        Ok(results)
513    }
514
515    /// Master joins until queue empty; returns collected (tx_ctx_idx, valid) results.
516    pub fn complete(&self) -> Result<Vec<(usize, bool)>> {
517        let _control = lock_poison_recovery(&self.control_mutex);
518        let state = Arc::clone(&self.state);
519        let worker_cv = Arc::clone(&self.worker_cv);
520        let master_cv = Arc::clone(&self.master_cv);
521        let batch_size = self.batch_size;
522
523        let mut n_now: usize = 0;
524        let mut local_error: Option<ConsensusError> = None;
525        let mut session_opt: Option<Arc<BlockSessionContext>> = None;
526        let mut batch_buf: Vec<ScriptCheck> = Vec::with_capacity(batch_size);
527
528        loop {
529            let done = {
530                let mut guard = lock_poison_recovery(state.as_ref());
531                if n_now > 0 {
532                    if let Some(ref err) = local_error {
533                        if guard.error_result.is_none() {
534                            guard.error_result = Some(err.clone());
535                        }
536                    }
537                    guard.n_todo -= n_now;
538                    n_now = 0;
539                    local_error = None;
540                } else {
541                    guard.n_total += 1;
542                }
543
544                loop {
545                    if guard.n_todo == 0 {
546                        guard.n_total -= 1;
547                        let n_expected = guard.n_submitted;
548                        guard.n_submitted = 0;
549                        let results = guard
550                            .session
551                            .as_ref()
552                            .map(|s| {
553                                let mut out = Vec::with_capacity(n_expected.max(64));
554                                while let Some(batch) = s.results.pop() {
555                                    out.extend(batch);
556                                }
557                                out
558                            })
559                            .unwrap_or_default();
560                        guard.session = None;
561                        if let Some(ref e) = guard.error_result {
562                            return Err(e.clone());
563                        }
564                        return Ok(results);
565                    }
566                    if guard.checks.is_empty() {
567                        guard.n_idle += 1;
568                        guard = condvar_wait_poison_recovery(master_cv.as_ref(), guard);
569                        guard.n_idle -= 1;
570                        continue;
571                    }
572                    break;
573                }
574
575                let n_total = guard.n_total;
576                let n_idle = guard.n_idle;
577                let divisor = (n_total + n_idle + 1).max(1);
578                n_now = (guard.checks.len() / divisor).clamp(1, batch_size);
579                let drain_len = n_now.min(guard.checks.len());
580                batch_buf.clear();
581                let drain_start = guard.checks.len() - drain_len;
582                batch_buf.extend(guard.checks.drain(drain_start..));
583                session_opt = guard.session.clone();
584                false
585            };
586
587            if batch_buf.is_empty() {
588                continue;
589            }
590
591            let session = match session_opt.as_ref() {
592                Some(s) => Arc::clone(s),
593                None => continue,
594            };
595
596            let mut batch_results = Vec::with_capacity(batch_buf.len());
597            #[cfg(all(feature = "production", feature = "profile"))]
598            let t_run_check = std::time::Instant::now();
599            {
600                batch_buf.sort_unstable_by_key(|c| c.tx_ctx_idx);
601                let buffer = session.script_pubkey_buffer.as_slice();
602                let spi = session.script_pubkey_indices_buffer.as_slice();
603                let pv = session.prevout_values_buffer.as_slice();
604                let mut refs_buf: Vec<&[u8]> = Vec::with_capacity(64);
605                let mut cached_ctx_idx: usize = usize::MAX;
606                for c in batch_buf.iter() {
607                    let ctx = match session.tx_contexts.get(c.tx_ctx_idx) {
608                        Some(ctx) => ctx,
609                        None => {
610                            local_error = Some(ConsensusError::BlockValidation(
611                                "tx_ctx_idx out of range".into(),
612                            ));
613                            break;
614                        }
615                    };
616                    let s = c.spk_offset as usize;
617                    let l = c.spk_len as usize;
618                    let script_pubkey = if s + l <= buffer.len() {
619                        &buffer[s..s + l]
620                    } else {
621                        &[]
622                    };
623                    let (pv_base, pv_count) = ctx.prevout_values_range;
624                    let prevout_slice = &pv[pv_base..][..pv_count];
625                    if c.tx_ctx_idx != cached_ctx_idx {
626                        refs_buf.clear();
627                        let (spi_base, spi_count) = ctx.script_pubkey_indices_range;
628                        for j in 0..spi_count {
629                            let (start, len) = spi[spi_base + j];
630                            refs_buf.push(if start + len <= buffer.len() {
631                                &buffer[start..start + len]
632                            } else {
633                                &[]
634                            });
635                        }
636                        cached_ctx_idx = c.tx_ctx_idx;
637                    }
638                    match Self::run_check_with_refs(
639                        c,
640                        session.as_ref(),
641                        ctx,
642                        &refs_buf,
643                        buffer,
644                        None,
645                        Some(script_pubkey),
646                        Some(prevout_slice),
647                    ) {
648                        Ok(valid) => batch_results.push((c.tx_ctx_idx, valid)),
649                        Err(e) => {
650                            local_error = Some(e);
651                            break;
652                        }
653                    }
654                }
655            }
656            #[cfg(all(feature = "production", feature = "profile"))]
657            crate::script_profile::add_worker_run_check_loop_ns(
658                t_run_check.elapsed().as_nanos() as u64
659            );
660            if !batch_results.is_empty() {
661                #[cfg(all(feature = "production", feature = "profile"))]
662                let t_results = std::time::Instant::now();
663                session.results.push(batch_results);
664                #[cfg(all(feature = "production", feature = "profile"))]
665                crate::script_profile::add_worker_results_extend_ns(
666                    t_results.elapsed().as_nanos() as u64
667                );
668            }
669        }
670    }
671}