Skip to main content

ab_farmer/plotter/
cpu.rs

1//! CPU plotter
2
3pub mod metrics;
4
5use crate::plotter::cpu::metrics::CpuPlotterMetrics;
6use crate::plotter::{Plotter, SectorPlottingProgress};
7use crate::thread_pool_manager::PlottingThreadPoolManager;
8use crate::utils::AsyncJoinOnDrop;
9use ab_core_primitives::ed25519::Ed25519PublicKey;
10use ab_core_primitives::sectors::SectorIndex;
11use ab_core_primitives::solutions::ShardCommitmentHash;
12use ab_data_retrieval::piece_getter::PieceGetter;
13use ab_erasure_coding::ErasureCoding;
14use ab_farmer_components::FarmerProtocolInfo;
15use ab_farmer_components::plotting::{
16    CpuRecordsEncoder, DownloadSectorOptions, EncodeSectorOptions, PlottingError, download_sector,
17    encode_sector, write_sector,
18};
19use ab_proof_of_space::Table;
20use async_lock::{Mutex as AsyncMutex, Semaphore, SemaphoreGuardArc};
21use async_trait::async_trait;
22use bytes::Bytes;
23use event_listener_primitives::{Bag, HandlerId};
24use futures::channel::mpsc;
25use futures::stream::FuturesUnordered;
26use futures::{FutureExt, Sink, SinkExt, StreamExt, select, stream};
27use prometheus_client::registry::Registry;
28use std::any::type_name;
29use std::error::Error;
30use std::future::pending;
31use std::marker::PhantomData;
32use std::num::NonZeroUsize;
33use std::pin::pin;
34use std::sync::Arc as StdArc;
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::task::Poll;
37use std::time::Instant;
38use std::{fmt, iter};
39use tokio::task::yield_now;
40use tracing::{Instrument, warn};
41use triomphe::Arc;
42
43/// Type alias used for event handlers
44pub type HandlerFn3<A, B, C> = StdArc<dyn Fn(&A, &B, &C) + Send + Sync + 'static>;
45type Handler3<A, B, C> = Bag<HandlerFn3<A, B, C>, A, B, C>;
46
47#[derive(Default, Debug)]
48struct Handlers {
49    plotting_progress: Handler3<Ed25519PublicKey, SectorIndex, SectorPlottingProgress>,
50}
51
52/// CPU plotter
53pub struct CpuPlotter<PG, PosTable> {
54    piece_getter: PG,
55    downloading_semaphore: StdArc<Semaphore>,
56    plotting_thread_pool_manager: PlottingThreadPoolManager,
57    record_encoding_concurrency: NonZeroUsize,
58    global_mutex: Arc<AsyncMutex<()>>,
59    erasure_coding: ErasureCoding,
60    handlers: Arc<Handlers>,
61    tasks_sender: mpsc::Sender<AsyncJoinOnDrop<()>>,
62    _background_tasks: AsyncJoinOnDrop<()>,
63    abort_early: Arc<AtomicBool>,
64    metrics: Option<Arc<CpuPlotterMetrics>>,
65    _phantom: PhantomData<PosTable>,
66}
67
68impl<PG, PosTable> fmt::Debug for CpuPlotter<PG, PosTable> {
69    #[inline]
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        f.debug_struct("CpuPlotter").finish_non_exhaustive()
72    }
73}
74
75impl<PG, PosTable> Drop for CpuPlotter<PG, PosTable> {
76    #[inline]
77    fn drop(&mut self) {
78        self.abort_early.store(true, Ordering::Release);
79        self.tasks_sender.close_channel();
80    }
81}
82
83#[async_trait]
84impl<PG, PosTable> Plotter for CpuPlotter<PG, PosTable>
85where
86    PG: PieceGetter + Clone + Send + Sync + 'static,
87    PosTable: Table,
88{
89    async fn has_free_capacity(&self) -> Result<bool, String> {
90        Ok(self.downloading_semaphore.try_acquire().is_some())
91    }
92
93    async fn plot_sector(
94        &self,
95        public_key: Ed25519PublicKey,
96        shard_commitments_root: ShardCommitmentHash,
97        sector_index: SectorIndex,
98        farmer_protocol_info: FarmerProtocolInfo,
99        pieces_in_sector: u16,
100        replotting: bool,
101        progress_sender: mpsc::Sender<SectorPlottingProgress>,
102    ) {
103        let start = Instant::now();
104
105        // Done outside the future below as a backpressure, ensuring that it is not possible to
106        // schedule unbounded number of plotting tasks
107        let downloading_permit = self.downloading_semaphore.acquire_arc().await;
108
109        self.plot_sector_internal(
110            start,
111            downloading_permit,
112            public_key,
113            shard_commitments_root,
114            sector_index,
115            farmer_protocol_info,
116            pieces_in_sector,
117            replotting,
118            progress_sender,
119        )
120        .await;
121    }
122
123    async fn try_plot_sector(
124        &self,
125        public_key: Ed25519PublicKey,
126        shard_commitments_root: ShardCommitmentHash,
127        sector_index: SectorIndex,
128        farmer_protocol_info: FarmerProtocolInfo,
129        pieces_in_sector: u16,
130        replotting: bool,
131        progress_sender: mpsc::Sender<SectorPlottingProgress>,
132    ) -> bool {
133        let start = Instant::now();
134
135        let Some(downloading_permit) = self.downloading_semaphore.try_acquire_arc() else {
136            return false;
137        };
138
139        self.plot_sector_internal(
140            start,
141            downloading_permit,
142            public_key,
143            shard_commitments_root,
144            sector_index,
145            farmer_protocol_info,
146            pieces_in_sector,
147            replotting,
148            progress_sender,
149        )
150        .await;
151
152        true
153    }
154}
155
156impl<PG, PosTable> CpuPlotter<PG, PosTable>
157where
158    PG: PieceGetter + Clone + Send + Sync + 'static,
159    PosTable: Table,
160{
161    /// Create a new instance
162    pub fn new(
163        piece_getter: PG,
164        downloading_semaphore: StdArc<Semaphore>,
165        plotting_thread_pool_manager: PlottingThreadPoolManager,
166        record_encoding_concurrency: NonZeroUsize,
167        global_mutex: Arc<AsyncMutex<()>>,
168        erasure_coding: ErasureCoding,
169        registry: Option<&mut Registry>,
170    ) -> Self {
171        let (tasks_sender, mut tasks_receiver) = mpsc::channel(1);
172
173        // Basically runs plotting tasks in the background and allows to abort on drop
174        let background_tasks = AsyncJoinOnDrop::new(
175            tokio::spawn(async move {
176                let background_tasks = FuturesUnordered::new();
177                let mut background_tasks = pin!(background_tasks);
178                // Just so that `FuturesUnordered` will never end
179                background_tasks.push(AsyncJoinOnDrop::new(tokio::spawn(pending::<()>()), true));
180
181                loop {
182                    select! {
183                        maybe_background_task = tasks_receiver.next().fuse() => {
184                            let Some(background_task) = maybe_background_task else {
185                                break;
186                            };
187
188                            background_tasks.push(background_task);
189                        },
190                        _ = background_tasks.select_next_some() => {
191                            // Nothing to do
192                        }
193                    }
194                }
195            }),
196            true,
197        );
198
199        let abort_early = Arc::new(AtomicBool::new(false));
200        let metrics = registry.map(|registry| {
201            Arc::new(CpuPlotterMetrics::new(
202                registry,
203                type_name::<PosTable>(),
204                plotting_thread_pool_manager.thread_pool_pairs(),
205            ))
206        });
207
208        Self {
209            piece_getter,
210            downloading_semaphore,
211            plotting_thread_pool_manager,
212            record_encoding_concurrency,
213            global_mutex,
214            erasure_coding,
215            handlers: Arc::default(),
216            tasks_sender,
217            _background_tasks: background_tasks,
218            abort_early,
219            metrics,
220            _phantom: PhantomData,
221        }
222    }
223
224    /// Subscribe to plotting progress notifications
225    pub fn on_plotting_progress(
226        &self,
227        callback: HandlerFn3<Ed25519PublicKey, SectorIndex, SectorPlottingProgress>,
228    ) -> HandlerId {
229        self.handlers.plotting_progress.add(callback)
230    }
231
232    #[expect(clippy::too_many_arguments, reason = "Internal API")]
233    async fn plot_sector_internal<PS>(
234        &self,
235        start: Instant,
236        downloading_permit: SemaphoreGuardArc,
237        public_key: Ed25519PublicKey,
238        shard_commitments_root: ShardCommitmentHash,
239        sector_index: SectorIndex,
240        farmer_protocol_info: FarmerProtocolInfo,
241        pieces_in_sector: u16,
242        replotting: bool,
243        mut progress_sender: PS,
244    ) where
245        PS: Sink<SectorPlottingProgress> + Unpin + Send + 'static,
246        PS::Error: Error,
247    {
248        if let Some(metrics) = &self.metrics {
249            metrics.sector_plotting.inc();
250        }
251
252        let progress_updater = ProgressUpdater {
253            public_key,
254            sector_index,
255            handlers: Arc::clone(&self.handlers),
256            metrics: self.metrics.clone(),
257        };
258
259        let plotting_fut = {
260            let piece_getter = self.piece_getter.clone();
261            let plotting_thread_pool_manager = self.plotting_thread_pool_manager.clone();
262            let record_encoding_concurrency = self.record_encoding_concurrency;
263            let global_mutex = Arc::clone(&self.global_mutex);
264            let erasure_coding = self.erasure_coding.clone();
265            let abort_early = Arc::clone(&self.abort_early);
266            let metrics = self.metrics.clone();
267
268            async move {
269                // Downloading
270                let downloaded_sector = {
271                    if !progress_updater
272                        .update_progress_and_events(
273                            &mut progress_sender,
274                            SectorPlottingProgress::Downloading,
275                        )
276                        .await
277                    {
278                        return;
279                    }
280
281                    // Take mutex briefly to make sure plotting is allowed right now
282                    global_mutex.lock().await;
283
284                    let downloading_start = Instant::now();
285                    let public_key_hash = &public_key.hash();
286
287                    let downloaded_sector_fut = download_sector(DownloadSectorOptions {
288                        public_key_hash,
289                        shard_commitments_root: &shard_commitments_root,
290                        sector_index,
291                        piece_getter: &piece_getter,
292                        farmer_protocol_info,
293                        erasure_coding: &erasure_coding,
294                        pieces_in_sector,
295                    });
296
297                    let downloaded_sector = match downloaded_sector_fut.await {
298                        Ok(downloaded_sector) => downloaded_sector,
299                        Err(error) => {
300                            warn!(%error, "Failed to download sector");
301
302                            progress_updater
303                                .update_progress_and_events(
304                                    &mut progress_sender,
305                                    SectorPlottingProgress::Error {
306                                        error: format!("Failed to download sector: {error}"),
307                                    },
308                                )
309                                .await;
310
311                            return;
312                        }
313                    };
314
315                    if !progress_updater
316                        .update_progress_and_events(
317                            &mut progress_sender,
318                            SectorPlottingProgress::Downloaded(downloading_start.elapsed()),
319                        )
320                        .await
321                    {
322                        return;
323                    }
324
325                    downloaded_sector
326                };
327
328                // Plotting
329                let (sector, plotted_sector) = {
330                    let thread_pools = plotting_thread_pool_manager.get_thread_pools().await;
331                    if let Some(metrics) = &metrics {
332                        metrics.plotting_capacity_used.inc();
333                    }
334
335                    // Give a chance to interrupt plotting if necessary
336                    yield_now().await;
337
338                    if !progress_updater
339                        .update_progress_and_events(
340                            &mut progress_sender,
341                            SectorPlottingProgress::Encoding,
342                        )
343                        .await
344                    {
345                        if let Some(metrics) = &metrics {
346                            metrics.plotting_capacity_used.dec();
347                        }
348                        return;
349                    }
350
351                    let encoding_start = Instant::now();
352
353                    let plotting_result = tokio::task::block_in_place(move || {
354                        let thread_pool = if replotting {
355                            &thread_pools.replotting
356                        } else {
357                            &thread_pools.plotting
358                        };
359
360                        let encoded_sector = thread_pool.install(|| {
361                            // TODO: Reuse global table generator (this comment is in many files)
362                            let generator = PosTable::generator();
363                            let generators = iter::repeat_n(
364                                generator.clone(),
365                                record_encoding_concurrency.get(),
366                            )
367                            .collect::<Vec<_>>();
368                            let mut records_encoder = CpuRecordsEncoder::<PosTable>::new(
369                                &generators,
370                                &erasure_coding,
371                                &global_mutex,
372                            );
373
374                            encode_sector(
375                                downloaded_sector,
376                                EncodeSectorOptions {
377                                    sector_index,
378                                    records_encoder: &mut records_encoder,
379                                    abort_early: &abort_early,
380                                },
381                            )
382                        })?;
383
384                        if abort_early.load(Ordering::Acquire) {
385                            return Err(PlottingError::AbortEarly);
386                        }
387
388                        drop(thread_pools);
389
390                        let mut sector = Vec::new();
391
392                        write_sector(&encoded_sector, &mut sector)?;
393
394                        Ok((sector, encoded_sector.plotted_sector))
395                    });
396
397                    if let Some(metrics) = &metrics {
398                        metrics.plotting_capacity_used.dec();
399                    }
400
401                    match plotting_result {
402                        Ok(plotting_result) => {
403                            if !progress_updater
404                                .update_progress_and_events(
405                                    &mut progress_sender,
406                                    SectorPlottingProgress::Encoded(encoding_start.elapsed()),
407                                )
408                                .await
409                            {
410                                return;
411                            }
412
413                            plotting_result
414                        }
415                        Err(PlottingError::AbortEarly) => {
416                            return;
417                        }
418                        Err(error) => {
419                            progress_updater
420                                .update_progress_and_events(
421                                    &mut progress_sender,
422                                    SectorPlottingProgress::Error {
423                                        error: format!("Failed to encode sector: {error}"),
424                                    },
425                                )
426                                .await;
427
428                            return;
429                        }
430                    }
431                };
432
433                progress_updater
434                    .update_progress_and_events(
435                        &mut progress_sender,
436                        SectorPlottingProgress::Finished {
437                            plotted_sector,
438                            time: start.elapsed(),
439                            sector: Box::pin({
440                                let mut sector = Some(Ok(Bytes::from(sector)));
441
442                                stream::poll_fn(move |_cx| {
443                                    // Just so that permit is dropped with stream itself
444                                    let _downloading_permit = &downloading_permit;
445
446                                    Poll::Ready(sector.take())
447                                })
448                            }),
449                        },
450                    )
451                    .await;
452            }
453        };
454
455        // Spawn a separate task such that `block_in_place` inside will not affect anything else
456        let plotting_task =
457            AsyncJoinOnDrop::new(tokio::spawn(plotting_fut.in_current_span()), true);
458        if let Err(error) = self.tasks_sender.clone().send(plotting_task).await {
459            warn!(%error, "Failed to send plotting task");
460
461            let progress = SectorPlottingProgress::Error {
462                error: format!("Failed to send plotting task: {error}"),
463            };
464
465            self.handlers
466                .plotting_progress
467                .call_simple(&public_key, &sector_index, &progress);
468        }
469    }
470}
471
472struct ProgressUpdater {
473    public_key: Ed25519PublicKey,
474    sector_index: SectorIndex,
475    handlers: Arc<Handlers>,
476    metrics: Option<Arc<CpuPlotterMetrics>>,
477}
478
479impl ProgressUpdater {
480    /// Returns `true` on success and `false` if progress receiver channel is gone
481    async fn update_progress_and_events<PS>(
482        &self,
483        progress_sender: &mut PS,
484        progress: SectorPlottingProgress,
485    ) -> bool
486    where
487        PS: Sink<SectorPlottingProgress> + Unpin,
488        PS::Error: Error,
489    {
490        if let Some(metrics) = &self.metrics {
491            #[expect(
492                clippy::rest_pattern_accessible_field,
493                reason = "Just counting occurrences"
494            )]
495            match &progress {
496                SectorPlottingProgress::Downloading => {
497                    metrics.sector_downloading.inc();
498                }
499                SectorPlottingProgress::Downloaded(time) => {
500                    metrics.sector_downloading_time.observe(time.as_secs_f64());
501                    metrics.sector_downloaded.inc();
502                }
503                SectorPlottingProgress::Encoding => {
504                    metrics.sector_encoding.inc();
505                }
506                SectorPlottingProgress::Encoded(time) => {
507                    metrics.sector_encoding_time.observe(time.as_secs_f64());
508                    metrics.sector_encoded.inc();
509                }
510                SectorPlottingProgress::Finished { time, .. } => {
511                    metrics.sector_plotting_time.observe(time.as_secs_f64());
512                    metrics.sector_plotted.inc();
513                }
514                SectorPlottingProgress::Error { .. } => {
515                    metrics.sector_plotting_error.inc();
516                }
517            }
518        }
519        self.handlers.plotting_progress.call_simple(
520            &self.public_key,
521            &self.sector_index,
522            &progress,
523        );
524
525        if let Err(error) = progress_sender.send(progress).await {
526            warn!(%error, "Failed to send progress update");
527
528            false
529        } else {
530            true
531        }
532    }
533}