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, Error: Error> + Unpin + Send + 'static,
246    {
247        if let Some(metrics) = &self.metrics {
248            metrics.sector_plotting.inc();
249        }
250
251        let progress_updater = ProgressUpdater {
252            public_key,
253            sector_index,
254            handlers: Arc::clone(&self.handlers),
255            metrics: self.metrics.clone(),
256        };
257
258        let plotting_fut = {
259            let piece_getter = self.piece_getter.clone();
260            let plotting_thread_pool_manager = self.plotting_thread_pool_manager.clone();
261            let record_encoding_concurrency = self.record_encoding_concurrency;
262            let global_mutex = Arc::clone(&self.global_mutex);
263            let erasure_coding = self.erasure_coding.clone();
264            let abort_early = Arc::clone(&self.abort_early);
265            let metrics = self.metrics.clone();
266
267            async move {
268                // Downloading
269                let downloaded_sector = {
270                    if !progress_updater
271                        .update_progress_and_events(
272                            &mut progress_sender,
273                            SectorPlottingProgress::Downloading,
274                        )
275                        .await
276                    {
277                        return;
278                    }
279
280                    // Take mutex briefly to make sure plotting is allowed right now
281                    global_mutex.lock().await;
282
283                    let downloading_start = Instant::now();
284                    let public_key_hash = &public_key.hash();
285
286                    let downloaded_sector_fut = download_sector(DownloadSectorOptions {
287                        public_key_hash,
288                        shard_commitments_root: &shard_commitments_root,
289                        sector_index,
290                        piece_getter: &piece_getter,
291                        farmer_protocol_info,
292                        erasure_coding: &erasure_coding,
293                        pieces_in_sector,
294                    });
295
296                    let downloaded_sector = match downloaded_sector_fut.await {
297                        Ok(downloaded_sector) => downloaded_sector,
298                        Err(error) => {
299                            warn!(%error, "Failed to download sector");
300
301                            progress_updater
302                                .update_progress_and_events(
303                                    &mut progress_sender,
304                                    SectorPlottingProgress::Error {
305                                        error: format!("Failed to download sector: {error}"),
306                                    },
307                                )
308                                .await;
309
310                            return;
311                        }
312                    };
313
314                    if !progress_updater
315                        .update_progress_and_events(
316                            &mut progress_sender,
317                            SectorPlottingProgress::Downloaded(downloading_start.elapsed()),
318                        )
319                        .await
320                    {
321                        return;
322                    }
323
324                    downloaded_sector
325                };
326
327                // Plotting
328                let (sector, plotted_sector) = {
329                    let thread_pools = plotting_thread_pool_manager.get_thread_pools().await;
330                    if let Some(metrics) = &metrics {
331                        metrics.plotting_capacity_used.inc();
332                    }
333
334                    // Give a chance to interrupt plotting if necessary
335                    yield_now().await;
336
337                    if !progress_updater
338                        .update_progress_and_events(
339                            &mut progress_sender,
340                            SectorPlottingProgress::Encoding,
341                        )
342                        .await
343                    {
344                        if let Some(metrics) = &metrics {
345                            metrics.plotting_capacity_used.dec();
346                        }
347                        return;
348                    }
349
350                    let encoding_start = Instant::now();
351
352                    let plotting_result = tokio::task::block_in_place(move || {
353                        let thread_pool = if replotting {
354                            &thread_pools.replotting
355                        } else {
356                            &thread_pools.plotting
357                        };
358
359                        let encoded_sector = thread_pool.install(|| {
360                            // TODO: Reuse global table generator (this comment is in many files)
361                            let generator = PosTable::generator();
362                            let generators = iter::repeat_n(
363                                generator.clone(),
364                                record_encoding_concurrency.get(),
365                            )
366                            .collect::<Vec<_>>();
367                            let mut records_encoder = CpuRecordsEncoder::<PosTable>::new(
368                                &generators,
369                                &erasure_coding,
370                                &global_mutex,
371                            );
372
373                            encode_sector(
374                                downloaded_sector,
375                                EncodeSectorOptions {
376                                    sector_index,
377                                    records_encoder: &mut records_encoder,
378                                    abort_early: &abort_early,
379                                },
380                            )
381                        })?;
382
383                        drop(thread_pools);
384
385                        let mut sector = Vec::new();
386
387                        write_sector(&encoded_sector, &mut sector)?;
388
389                        Ok((sector, encoded_sector.plotted_sector))
390                    });
391
392                    if let Some(metrics) = &metrics {
393                        metrics.plotting_capacity_used.dec();
394                    }
395
396                    match plotting_result {
397                        Ok(plotting_result) => {
398                            if !progress_updater
399                                .update_progress_and_events(
400                                    &mut progress_sender,
401                                    SectorPlottingProgress::Encoded(encoding_start.elapsed()),
402                                )
403                                .await
404                            {
405                                return;
406                            }
407
408                            plotting_result
409                        }
410                        Err(PlottingError::AbortEarly) => {
411                            return;
412                        }
413                        Err(error) => {
414                            progress_updater
415                                .update_progress_and_events(
416                                    &mut progress_sender,
417                                    SectorPlottingProgress::Error {
418                                        error: format!("Failed to encode sector: {error}"),
419                                    },
420                                )
421                                .await;
422
423                            return;
424                        }
425                    }
426                };
427
428                progress_updater
429                    .update_progress_and_events(
430                        &mut progress_sender,
431                        SectorPlottingProgress::Finished {
432                            plotted_sector,
433                            time: start.elapsed(),
434                            sector: Box::pin({
435                                let mut sector = Some(Ok(Bytes::from(sector)));
436
437                                stream::poll_fn(move |_cx| {
438                                    // Just so that permit is dropped with stream itself
439                                    let _downloading_permit = &downloading_permit;
440
441                                    Poll::Ready(sector.take())
442                                })
443                            }),
444                        },
445                    )
446                    .await;
447            }
448        };
449
450        // Spawn a separate task such that `block_in_place` inside will not affect anything else
451        let plotting_task =
452            AsyncJoinOnDrop::new(tokio::spawn(plotting_fut.in_current_span()), true);
453        if let Err(error) = self.tasks_sender.clone().send(plotting_task).await {
454            warn!(%error, "Failed to send plotting task");
455
456            let progress = SectorPlottingProgress::Error {
457                error: format!("Failed to send plotting task: {error}"),
458            };
459
460            self.handlers
461                .plotting_progress
462                .call_simple(&public_key, &sector_index, &progress);
463        }
464    }
465}
466
467struct ProgressUpdater {
468    public_key: Ed25519PublicKey,
469    sector_index: SectorIndex,
470    handlers: Arc<Handlers>,
471    metrics: Option<Arc<CpuPlotterMetrics>>,
472}
473
474impl ProgressUpdater {
475    /// Returns `true` on success and `false` if progress receiver channel is gone
476    async fn update_progress_and_events<PS>(
477        &self,
478        progress_sender: &mut PS,
479        progress: SectorPlottingProgress,
480    ) -> bool
481    where
482        PS: Sink<SectorPlottingProgress, Error: Error> + Unpin,
483    {
484        if let Some(metrics) = &self.metrics {
485            #[expect(
486                clippy::rest_pattern_accessible_field,
487                reason = "Just counting occurrences"
488            )]
489            match &progress {
490                SectorPlottingProgress::Downloading => {
491                    metrics.sector_downloading.inc();
492                }
493                SectorPlottingProgress::Downloaded(time) => {
494                    metrics.sector_downloading_time.observe(time.as_secs_f64());
495                    metrics.sector_downloaded.inc();
496                }
497                SectorPlottingProgress::Encoding => {
498                    metrics.sector_encoding.inc();
499                }
500                SectorPlottingProgress::Encoded(time) => {
501                    metrics.sector_encoding_time.observe(time.as_secs_f64());
502                    metrics.sector_encoded.inc();
503                }
504                SectorPlottingProgress::Finished { time, .. } => {
505                    metrics.sector_plotting_time.observe(time.as_secs_f64());
506                    metrics.sector_plotted.inc();
507                }
508                SectorPlottingProgress::Error { .. } => {
509                    metrics.sector_plotting_error.inc();
510                }
511            }
512        }
513        self.handlers.plotting_progress.call_simple(
514            &self.public_key,
515            &self.sector_index,
516            &progress,
517        );
518
519        if let Err(error) = progress_sender.send(progress).await {
520            warn!(%error, "Failed to send progress update");
521
522            false
523        } else {
524            true
525        }
526    }
527}