1use tracing::instrument;
2
3const NUM_SHARDS: usize = 64;
6
7#[repr(align(128))]
12struct PaddedAtomicU64(std::sync::atomic::AtomicU64);
13
14static NEXT_SHARD_INDEX: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
17
18thread_local! {
19 static MY_SHARD: usize =
22 NEXT_SHARD_INDEX.fetch_add(1, std::sync::atomic::Ordering::Relaxed) % NUM_SHARDS;
23}
24
25pub struct TlsCounter {
39 shards: [PaddedAtomicU64; NUM_SHARDS],
40}
41
42impl TlsCounter {
43 #[must_use]
44 pub fn new() -> Self {
45 Self {
46 shards: std::array::from_fn(|_| PaddedAtomicU64(std::sync::atomic::AtomicU64::new(0))),
47 }
48 }
49
50 pub fn add(&self, value: u64) {
51 let shard = MY_SHARD.with(|&s| s);
52 self.shards[shard]
53 .0
54 .fetch_add(value, std::sync::atomic::Ordering::Relaxed);
55 }
56
57 pub fn inc(&self) {
58 self.add(1);
59 }
60
61 pub fn get(&self) -> u64 {
62 self.shards
63 .iter()
64 .map(|s| s.0.load(std::sync::atomic::Ordering::Relaxed))
65 .sum()
66 }
67}
68
69impl std::fmt::Debug for TlsCounter {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 f.debug_struct("TlsCounter")
72 .field("value", &self.get())
73 .finish()
74 }
75}
76
77impl Default for TlsCounter {
78 fn default() -> Self {
79 Self::new()
80 }
81}
82
83#[derive(Debug)]
84pub struct ProgressCounter {
85 started: TlsCounter,
86 finished: TlsCounter,
87}
88
89impl Default for ProgressCounter {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95pub struct ProgressGuard<'a> {
96 progress: &'a ProgressCounter,
97}
98
99impl<'a> ProgressGuard<'a> {
100 pub fn new(progress: &'a ProgressCounter) -> Self {
101 progress.started.inc();
102 Self { progress }
103 }
104}
105
106impl Drop for ProgressGuard<'_> {
107 fn drop(&mut self) {
108 self.progress.finished.inc();
109 }
110}
111
112pub struct Status {
113 pub started: u64,
114 pub finished: u64,
115}
116
117impl ProgressCounter {
118 #[must_use]
119 pub fn new() -> Self {
120 Self {
121 started: TlsCounter::new(),
122 finished: TlsCounter::new(),
123 }
124 }
125
126 pub fn guard(&self) -> ProgressGuard<'_> {
127 ProgressGuard::new(self)
128 }
129
130 #[instrument]
131 pub fn get(&self) -> Status {
132 let mut status = Status {
133 started: self.started.get(),
134 finished: self.finished.get(),
135 };
136 if status.finished > status.started {
137 tracing::debug!(
138 "Progress inversion - started: {}, finished {}",
139 status.started,
140 status.finished
141 );
142 status.started = status.finished;
143 }
144 status
145 }
146}
147
148pub struct Progress {
149 pub ops: ProgressCounter,
150 pub bytes_copied: TlsCounter,
151 pub hard_links_created: TlsCounter,
152 pub files_copied: TlsCounter,
153 pub symlinks_created: TlsCounter,
154 pub directories_created: TlsCounter,
155 pub files_changed: TlsCounter,
156 pub symlinks_changed: TlsCounter,
157 pub directories_changed: TlsCounter,
158 pub files_unchanged: TlsCounter,
159 pub symlinks_unchanged: TlsCounter,
160 pub directories_unchanged: TlsCounter,
161 pub hard_links_unchanged: TlsCounter,
162 pub files_removed: TlsCounter,
163 pub symlinks_removed: TlsCounter,
164 pub directories_removed: TlsCounter,
165 pub bytes_removed: TlsCounter,
166 pub files_skipped: TlsCounter,
167 pub symlinks_skipped: TlsCounter,
168 pub directories_skipped: TlsCounter,
169 pub specials_skipped: TlsCounter,
170 start_time: std::time::Instant,
171}
172
173impl Progress {
174 #[must_use]
175 pub fn new() -> Self {
176 Self {
177 ops: Default::default(),
178 bytes_copied: Default::default(),
179 hard_links_created: Default::default(),
180 files_copied: Default::default(),
181 symlinks_created: Default::default(),
182 directories_created: Default::default(),
183 files_changed: Default::default(),
184 symlinks_changed: Default::default(),
185 directories_changed: Default::default(),
186 files_unchanged: Default::default(),
187 symlinks_unchanged: Default::default(),
188 directories_unchanged: Default::default(),
189 hard_links_unchanged: Default::default(),
190 files_removed: Default::default(),
191 symlinks_removed: Default::default(),
192 directories_removed: Default::default(),
193 bytes_removed: Default::default(),
194 files_skipped: Default::default(),
195 symlinks_skipped: Default::default(),
196 directories_skipped: Default::default(),
197 specials_skipped: Default::default(),
198 start_time: std::time::Instant::now(),
199 }
200 }
201
202 pub fn get_duration(&self) -> std::time::Duration {
203 self.start_time.elapsed()
204 }
205}
206
207impl Default for Progress {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
214pub struct SerializableProgress {
215 pub ops_started: u64,
216 pub ops_finished: u64,
217 pub bytes_copied: u64,
218 pub hard_links_created: u64,
219 pub files_copied: u64,
220 pub symlinks_created: u64,
221 pub directories_created: u64,
222 pub files_unchanged: u64,
223 pub symlinks_unchanged: u64,
224 pub directories_unchanged: u64,
225 pub hard_links_unchanged: u64,
226 pub files_removed: u64,
227 pub symlinks_removed: u64,
228 pub directories_removed: u64,
229 pub bytes_removed: u64,
230 pub files_skipped: u64,
231 pub symlinks_skipped: u64,
232 pub directories_skipped: u64,
233 pub specials_skipped: u64,
234 pub current_time: std::time::SystemTime,
235}
236
237impl Default for SerializableProgress {
238 fn default() -> Self {
239 Self {
240 ops_started: 0,
241 ops_finished: 0,
242 bytes_copied: 0,
243 hard_links_created: 0,
244 files_copied: 0,
245 symlinks_created: 0,
246 directories_created: 0,
247 files_unchanged: 0,
248 symlinks_unchanged: 0,
249 directories_unchanged: 0,
250 hard_links_unchanged: 0,
251 files_removed: 0,
252 symlinks_removed: 0,
253 directories_removed: 0,
254 bytes_removed: 0,
255 files_skipped: 0,
256 symlinks_skipped: 0,
257 directories_skipped: 0,
258 specials_skipped: 0,
259 current_time: std::time::SystemTime::now(),
260 }
261 }
262}
263
264impl From<&Progress> for SerializableProgress {
265 fn from(progress: &Progress) -> Self {
267 let ops = progress.ops.get();
268 Self {
269 ops_started: ops.started,
270 ops_finished: ops.finished,
271 bytes_copied: progress.bytes_copied.get(),
272 hard_links_created: progress.hard_links_created.get(),
273 files_copied: progress.files_copied.get(),
274 symlinks_created: progress.symlinks_created.get(),
275 directories_created: progress.directories_created.get(),
276 files_unchanged: progress.files_unchanged.get(),
277 symlinks_unchanged: progress.symlinks_unchanged.get(),
278 directories_unchanged: progress.directories_unchanged.get(),
279 hard_links_unchanged: progress.hard_links_unchanged.get(),
280 files_removed: progress.files_removed.get(),
281 symlinks_removed: progress.symlinks_removed.get(),
282 directories_removed: progress.directories_removed.get(),
283 bytes_removed: progress.bytes_removed.get(),
284 files_skipped: progress.files_skipped.get(),
285 symlinks_skipped: progress.symlinks_skipped.get(),
286 directories_skipped: progress.directories_skipped.get(),
287 specials_skipped: progress.specials_skipped.get(),
288 current_time: std::time::SystemTime::now(),
289 }
290 }
291}
292
293pub trait LocalProgressReport: Send {
299 fn print(&mut self) -> anyhow::Result<String>;
300}
301
302#[derive(Copy, Clone, Debug)]
304pub enum LocalProgressKind {
305 Copy,
306 Remove,
307 Link,
308 Compare,
309 Filegen,
310 Modify,
311}
312
313fn ops_rates(
314 progress: &Progress,
315 last_ops: u64,
316 last_update: std::time::Instant,
317 now: std::time::Instant,
318) -> (Status, f64, f64) {
319 let ops = progress.ops.get();
320 let total_secs = progress.get_duration().as_secs_f64();
321 let curr_secs = (now - last_update).as_secs_f64();
322 let average = if total_secs > 0.0 {
323 ops.finished as f64 / total_secs
324 } else {
325 0.0
326 };
327 let current = if curr_secs > 0.0 {
328 (ops.finished - last_ops) as f64 / curr_secs
329 } else {
330 0.0
331 };
332 (ops, average, current)
333}
334
335fn bytes_rates(
336 total: u64,
337 last: u64,
338 total_secs: f64,
339 curr_secs: f64,
340) -> (bytesize::ByteSize, bytesize::ByteSize) {
341 let average = if total_secs > 0.0 {
342 total as f64 / total_secs
343 } else {
344 0.0
345 };
346 let current = if curr_secs > 0.0 {
347 (total - last) as f64 / curr_secs
348 } else {
349 0.0
350 };
351 (
352 bytesize::ByteSize(average as u64),
353 bytesize::ByteSize(current as u64),
354 )
355}
356
357pub struct CopyProgressPrinter<'a> {
361 progress: &'a Progress,
362 last_ops: u64,
363 last_bytes: u64,
364 last_update: std::time::Instant,
365}
366
367impl<'a> CopyProgressPrinter<'a> {
368 pub fn new(progress: &'a Progress) -> Self {
369 Self {
370 progress,
371 last_ops: progress.ops.get().finished,
372 last_bytes: progress.bytes_copied.get(),
373 last_update: std::time::Instant::now(),
374 }
375 }
376}
377
378impl<'a> LocalProgressReport for CopyProgressPrinter<'a> {
379 fn print(&mut self) -> anyhow::Result<String> {
380 let now = std::time::Instant::now();
381 let (ops, avg_ops, cur_ops) =
382 ops_rates(self.progress, self.last_ops, self.last_update, now);
383 let bytes = self.progress.bytes_copied.get();
384 let total_secs = self.progress.get_duration().as_secs_f64();
385 let curr_secs = (now - self.last_update).as_secs_f64();
386 let (avg_bytes, cur_bytes) = bytes_rates(bytes, self.last_bytes, total_secs, curr_secs);
387 self.last_ops = ops.finished;
388 self.last_bytes = bytes;
389 self.last_update = now;
390 Ok(format!(
391 "---------------------\n\
392 OPS:\n\
393 pending: {:>10}\n\
394 average: {:>10.2} items/s\n\
395 current: {:>10.2} items/s\n\
396 -----------------------\n\
397 COPIED:\n\
398 average: {:>10}/s\n\
399 current: {:>10}/s\n\
400 bytes: {:>10}\n\
401 files: {:>10}\n\
402 symlinks: {:>10}\n\
403 directories: {:>10}\n\
404 -----------------------\n\
405 UNCHANGED:\n\
406 files: {:>10}\n\
407 symlinks: {:>10}\n\
408 directories: {:>10}\n\
409 -----------------------\n\
410 REMOVED:\n\
411 bytes: {:>10}\n\
412 files: {:>10}\n\
413 symlinks: {:>10}\n\
414 directories: {:>10}\n\
415 -----------------------\n\
416 SKIPPED:\n\
417 files: {:>10}\n\
418 symlinks: {:>10}\n\
419 directories: {:>10}\n\
420 specials: {:>10}",
421 ops.started - ops.finished,
422 avg_ops,
423 cur_ops,
424 avg_bytes,
425 cur_bytes,
426 bytesize::ByteSize(bytes),
427 self.progress.files_copied.get(),
428 self.progress.symlinks_created.get(),
429 self.progress.directories_created.get(),
430 self.progress.files_unchanged.get(),
431 self.progress.symlinks_unchanged.get(),
432 self.progress.directories_unchanged.get(),
433 bytesize::ByteSize(self.progress.bytes_removed.get()),
434 self.progress.files_removed.get(),
435 self.progress.symlinks_removed.get(),
436 self.progress.directories_removed.get(),
437 self.progress.files_skipped.get(),
438 self.progress.symlinks_skipped.get(),
439 self.progress.directories_skipped.get(),
440 self.progress.specials_skipped.get(),
441 ))
442 }
443}
444
445pub struct RemoveProgressPrinter<'a> {
448 progress: &'a Progress,
449 last_ops: u64,
450 last_bytes_removed: u64,
451 last_update: std::time::Instant,
452}
453
454impl<'a> RemoveProgressPrinter<'a> {
455 pub fn new(progress: &'a Progress) -> Self {
456 Self {
457 progress,
458 last_ops: progress.ops.get().finished,
459 last_bytes_removed: progress.bytes_removed.get(),
460 last_update: std::time::Instant::now(),
461 }
462 }
463}
464
465impl<'a> LocalProgressReport for RemoveProgressPrinter<'a> {
466 fn print(&mut self) -> anyhow::Result<String> {
467 let now = std::time::Instant::now();
468 let (ops, avg_ops, cur_ops) =
469 ops_rates(self.progress, self.last_ops, self.last_update, now);
470 let bytes_removed = self.progress.bytes_removed.get();
471 let total_secs = self.progress.get_duration().as_secs_f64();
472 let curr_secs = (now - self.last_update).as_secs_f64();
473 let (avg_bytes, cur_bytes) = bytes_rates(
474 bytes_removed,
475 self.last_bytes_removed,
476 total_secs,
477 curr_secs,
478 );
479 self.last_ops = ops.finished;
480 self.last_bytes_removed = bytes_removed;
481 self.last_update = now;
482 Ok(format!(
483 "---------------------\n\
484 OPS:\n\
485 pending: {:>10}\n\
486 average: {:>10.2} items/s\n\
487 current: {:>10.2} items/s\n\
488 -----------------------\n\
489 REMOVED:\n\
490 average: {:>10}/s\n\
491 current: {:>10}/s\n\
492 bytes: {:>10}\n\
493 files: {:>10}\n\
494 symlinks: {:>10}\n\
495 directories: {:>10}\n\
496 -----------------------\n\
497 SKIPPED:\n\
498 files: {:>10}\n\
499 symlinks: {:>10}\n\
500 directories: {:>10}",
501 ops.started - ops.finished,
502 avg_ops,
503 cur_ops,
504 avg_bytes,
505 cur_bytes,
506 bytesize::ByteSize(bytes_removed),
507 self.progress.files_removed.get(),
508 self.progress.symlinks_removed.get(),
509 self.progress.directories_removed.get(),
510 self.progress.files_skipped.get(),
511 self.progress.symlinks_skipped.get(),
512 self.progress.directories_skipped.get(),
513 ))
514 }
515}
516
517pub struct ModifyProgressPrinter<'a> {
520 progress: &'a Progress,
521 last_ops: u64,
522 last_update: std::time::Instant,
523}
524
525impl<'a> ModifyProgressPrinter<'a> {
526 pub fn new(progress: &'a Progress) -> Self {
527 Self {
528 progress,
529 last_ops: progress.ops.get().finished,
530 last_update: std::time::Instant::now(),
531 }
532 }
533}
534
535impl<'a> LocalProgressReport for ModifyProgressPrinter<'a> {
536 fn print(&mut self) -> anyhow::Result<String> {
537 let now = std::time::Instant::now();
538 let (ops, avg_ops, cur_ops) =
539 ops_rates(self.progress, self.last_ops, self.last_update, now);
540 self.last_ops = ops.finished;
541 self.last_update = now;
542 Ok(format!(
543 "---------------------\n\
544 OPS:\n\
545 pending: {:>10}\n\
546 average: {:>10.2} items/s\n\
547 current: {:>10.2} items/s\n\
548 -----------------------\n\
549 CHANGED:\n\
550 files: {:>10}\n\
551 symlinks: {:>10}\n\
552 directories: {:>10}\n\
553 -----------------------\n\
554 UNCHANGED:\n\
555 files: {:>10}\n\
556 symlinks: {:>10}\n\
557 directories: {:>10}\n\
558 -----------------------\n\
559 SKIPPED:\n\
560 files: {:>10}\n\
561 symlinks: {:>10}\n\
562 directories: {:>10}",
563 ops.started - ops.finished,
564 avg_ops,
565 cur_ops,
566 self.progress.files_changed.get(),
567 self.progress.symlinks_changed.get(),
568 self.progress.directories_changed.get(),
569 self.progress.files_unchanged.get(),
570 self.progress.symlinks_unchanged.get(),
571 self.progress.directories_unchanged.get(),
572 self.progress.files_skipped.get(),
573 self.progress.symlinks_skipped.get(),
574 self.progress.directories_skipped.get(),
575 ))
576 }
577}
578
579pub struct LinkProgressPrinter<'a> {
583 progress: &'a Progress,
584 last_ops: u64,
585 last_bytes: u64,
586 last_update: std::time::Instant,
587}
588
589impl<'a> LinkProgressPrinter<'a> {
590 pub fn new(progress: &'a Progress) -> Self {
591 Self {
592 progress,
593 last_ops: progress.ops.get().finished,
594 last_bytes: progress.bytes_copied.get(),
595 last_update: std::time::Instant::now(),
596 }
597 }
598}
599
600impl<'a> LocalProgressReport for LinkProgressPrinter<'a> {
601 fn print(&mut self) -> anyhow::Result<String> {
602 let now = std::time::Instant::now();
603 let (ops, avg_ops, cur_ops) =
604 ops_rates(self.progress, self.last_ops, self.last_update, now);
605 let bytes = self.progress.bytes_copied.get();
606 let total_secs = self.progress.get_duration().as_secs_f64();
607 let curr_secs = (now - self.last_update).as_secs_f64();
608 let (avg_bytes, cur_bytes) = bytes_rates(bytes, self.last_bytes, total_secs, curr_secs);
609 self.last_ops = ops.finished;
610 self.last_bytes = bytes;
611 self.last_update = now;
612 Ok(format!(
613 "---------------------\n\
614 OPS:\n\
615 pending: {:>10}\n\
616 average: {:>10.2} items/s\n\
617 current: {:>10.2} items/s\n\
618 -----------------------\n\
619 LINKED / COPIED:\n\
620 average: {:>10}/s\n\
621 current: {:>10}/s\n\
622 bytes: {:>10}\n\
623 hard-links: {:>10}\n\
624 files: {:>10}\n\
625 symlinks: {:>10}\n\
626 directories: {:>10}\n\
627 -----------------------\n\
628 UNCHANGED:\n\
629 hard-links: {:>10}\n\
630 files: {:>10}\n\
631 symlinks: {:>10}\n\
632 directories: {:>10}\n\
633 -----------------------\n\
634 REMOVED:\n\
635 bytes: {:>10}\n\
636 files: {:>10}\n\
637 symlinks: {:>10}\n\
638 directories: {:>10}\n\
639 -----------------------\n\
640 SKIPPED:\n\
641 files: {:>10}\n\
642 symlinks: {:>10}\n\
643 directories: {:>10}\n\
644 specials: {:>10}",
645 ops.started - ops.finished,
646 avg_ops,
647 cur_ops,
648 avg_bytes,
649 cur_bytes,
650 bytesize::ByteSize(bytes),
651 self.progress.hard_links_created.get(),
652 self.progress.files_copied.get(),
653 self.progress.symlinks_created.get(),
654 self.progress.directories_created.get(),
655 self.progress.hard_links_unchanged.get(),
656 self.progress.files_unchanged.get(),
657 self.progress.symlinks_unchanged.get(),
658 self.progress.directories_unchanged.get(),
659 bytesize::ByteSize(self.progress.bytes_removed.get()),
660 self.progress.files_removed.get(),
661 self.progress.symlinks_removed.get(),
662 self.progress.directories_removed.get(),
663 self.progress.files_skipped.get(),
664 self.progress.symlinks_skipped.get(),
665 self.progress.directories_skipped.get(),
666 self.progress.specials_skipped.get(),
667 ))
668 }
669}
670
671pub struct CompareProgressPrinter<'a> {
674 progress: &'a Progress,
675 last_ops: u64,
676 last_update: std::time::Instant,
677}
678
679impl<'a> CompareProgressPrinter<'a> {
680 pub fn new(progress: &'a Progress) -> Self {
681 Self {
682 progress,
683 last_ops: progress.ops.get().finished,
684 last_update: std::time::Instant::now(),
685 }
686 }
687}
688
689impl<'a> LocalProgressReport for CompareProgressPrinter<'a> {
690 fn print(&mut self) -> anyhow::Result<String> {
691 let now = std::time::Instant::now();
692 let (ops, avg_ops, cur_ops) =
693 ops_rates(self.progress, self.last_ops, self.last_update, now);
694 self.last_ops = ops.finished;
695 self.last_update = now;
696 Ok(format!(
697 "---------------------\n\
698 OPS:\n\
699 pending: {:>10}\n\
700 compared: {:>9}\n\
701 average: {:>10.2} items/s\n\
702 current: {:>10.2} items/s",
703 ops.started - ops.finished,
704 ops.finished,
705 avg_ops,
706 cur_ops,
707 ))
708 }
709}
710
711pub struct FilegenProgressPrinter<'a> {
714 progress: &'a Progress,
715 last_ops: u64,
716 last_bytes: u64,
717 last_update: std::time::Instant,
718}
719
720impl<'a> FilegenProgressPrinter<'a> {
721 pub fn new(progress: &'a Progress) -> Self {
722 Self {
723 progress,
724 last_ops: progress.ops.get().finished,
725 last_bytes: progress.bytes_copied.get(),
726 last_update: std::time::Instant::now(),
727 }
728 }
729}
730
731impl<'a> LocalProgressReport for FilegenProgressPrinter<'a> {
732 fn print(&mut self) -> anyhow::Result<String> {
733 let now = std::time::Instant::now();
734 let (ops, avg_ops, cur_ops) =
735 ops_rates(self.progress, self.last_ops, self.last_update, now);
736 let bytes = self.progress.bytes_copied.get();
737 let total_secs = self.progress.get_duration().as_secs_f64();
738 let curr_secs = (now - self.last_update).as_secs_f64();
739 let (avg_bytes, cur_bytes) = bytes_rates(bytes, self.last_bytes, total_secs, curr_secs);
740 self.last_ops = ops.finished;
741 self.last_bytes = bytes;
742 self.last_update = now;
743 Ok(format!(
744 "---------------------\n\
745 OPS:\n\
746 pending: {:>10}\n\
747 average: {:>10.2} items/s\n\
748 current: {:>10.2} items/s\n\
749 -----------------------\n\
750 GENERATED:\n\
751 average: {:>10}/s\n\
752 current: {:>10}/s\n\
753 bytes: {:>10}\n\
754 files: {:>10}\n\
755 directories: {:>10}",
756 ops.started - ops.finished,
757 avg_ops,
758 cur_ops,
759 avg_bytes,
760 cur_bytes,
761 bytesize::ByteSize(bytes),
762 self.progress.files_copied.get(),
763 self.progress.directories_created.get(),
764 ))
765 }
766}
767
768#[must_use]
771pub fn make_local_printer<'a>(
772 kind: LocalProgressKind,
773 progress: &'a Progress,
774) -> Box<dyn LocalProgressReport + 'a> {
775 match kind {
776 LocalProgressKind::Copy => Box::new(CopyProgressPrinter::new(progress)),
777 LocalProgressKind::Remove => Box::new(RemoveProgressPrinter::new(progress)),
778 LocalProgressKind::Link => Box::new(LinkProgressPrinter::new(progress)),
779 LocalProgressKind::Compare => Box::new(CompareProgressPrinter::new(progress)),
780 LocalProgressKind::Filegen => Box::new(FilegenProgressPrinter::new(progress)),
781 LocalProgressKind::Modify => Box::new(ModifyProgressPrinter::new(progress)),
782 }
783}
784
785pub struct RcpdProgressPrinter {
786 start_time: std::time::Instant,
787 last_source_ops: u64,
788 last_source_bytes: u64,
789 last_source_files: u64,
790 last_dest_ops: u64,
791 last_dest_bytes: u64,
792 last_update: std::time::Instant,
793}
794
795impl RcpdProgressPrinter {
796 #[must_use]
797 pub fn new() -> Self {
798 let now = std::time::Instant::now();
799 Self {
800 start_time: now,
801 last_source_ops: 0,
802 last_source_bytes: 0,
803 last_source_files: 0,
804 last_dest_ops: 0,
805 last_dest_bytes: 0,
806 last_update: now,
807 }
808 }
809
810 fn calculate_current_rate(&self, current: u64, last: u64, duration_secs: f64) -> f64 {
811 if duration_secs > 0.0 {
812 (current - last) as f64 / duration_secs
813 } else {
814 0.0
815 }
816 }
817
818 fn calculate_average_rate(&self, total: u64, total_duration_secs: f64) -> f64 {
819 if total_duration_secs > 0.0 {
820 total as f64 / total_duration_secs
821 } else {
822 0.0
823 }
824 }
825
826 pub fn print(
827 &mut self,
828 source_progress: &SerializableProgress,
829 dest_progress: &SerializableProgress,
830 ) -> anyhow::Result<String> {
831 let time_now = std::time::Instant::now();
832 let total_duration_secs = (time_now - self.start_time).as_secs_f64();
833 let curr_duration_secs = (time_now - self.last_update).as_secs_f64();
834 let source_ops_rate_curr = self.calculate_current_rate(
836 source_progress.ops_finished,
837 self.last_source_ops,
838 curr_duration_secs,
839 );
840 let source_bytes_rate_curr = self.calculate_current_rate(
841 source_progress.bytes_copied,
842 self.last_source_bytes,
843 curr_duration_secs,
844 );
845 let source_files_rate_curr = self.calculate_current_rate(
846 source_progress.files_copied,
847 self.last_source_files,
848 curr_duration_secs,
849 );
850 let source_ops_rate_avg =
852 self.calculate_average_rate(source_progress.ops_finished, total_duration_secs);
853 let source_bytes_rate_avg =
854 self.calculate_average_rate(source_progress.bytes_copied, total_duration_secs);
855 let source_files_rate_avg =
856 self.calculate_average_rate(source_progress.files_copied, total_duration_secs);
857 let dest_ops_rate_curr = self.calculate_current_rate(
859 dest_progress.ops_finished,
860 self.last_dest_ops,
861 curr_duration_secs,
862 );
863 let dest_bytes_rate_curr = self.calculate_current_rate(
864 dest_progress.bytes_copied,
865 self.last_dest_bytes,
866 curr_duration_secs,
867 );
868 let dest_ops_rate_avg =
870 self.calculate_average_rate(dest_progress.ops_finished, total_duration_secs);
871 let dest_bytes_rate_avg =
872 self.calculate_average_rate(dest_progress.bytes_copied, total_duration_secs);
873 self.last_source_ops = source_progress.ops_finished;
875 self.last_source_bytes = source_progress.bytes_copied;
876 self.last_source_files = source_progress.files_copied;
877 self.last_dest_ops = dest_progress.ops_finished;
878 self.last_dest_bytes = dest_progress.bytes_copied;
879 self.last_update = time_now;
880 Ok(format!(
881 "==== SOURCE =======\n\
882 OPS:\n\
883 pending: {:>10}\n\
884 average: {:>10.2} items/s\n\
885 current: {:>10.2} items/s\n\
886 ---------------------\n\
887 COPIED:\n\
888 average: {:>10}/s\n\
889 current: {:>10}/s\n\
890 bytes: {:>10}\n\
891 files: {:>10}\n\
892 ---------------------\n\
893 FILES:\n\
894 average: {:>10.2} files/s\n\
895 current: {:>10.2} files/s\n\
896 ---------------------\n\
897 SKIPPED:\n\
898 files: {:>10}\n\
899 symlinks: {:>10}\n\
900 directories: {:>10}\n\
901 specials: {:>10}\n\
902 ==== DESTINATION ====\n\
903 OPS:\n\
904 pending: {:>10}\n\
905 average: {:>10.2} items/s\n\
906 current: {:>10.2} items/s\n\
907 ---------------------\n\
908 COPIED:\n\
909 average: {:>10}/s\n\
910 current: {:>10}/s\n\
911 bytes: {:>10}\n\
912 files: {:>10}\n\
913 symlinks: {:>10}\n\
914 directories: {:>10}\n\
915 hard-links: {:>10}\n\
916 ---------------------\n\
917 UNCHANGED:\n\
918 files: {:>10}\n\
919 symlinks: {:>10}\n\
920 directories: {:>10}\n\
921 hard-links: {:>10}\n\
922 ---------------------\n\
923 REMOVED:\n\
924 bytes: {:>10}\n\
925 files: {:>10}\n\
926 symlinks: {:>10}\n\
927 directories: {:>10}",
928 source_progress
930 .ops_started
931 .saturating_sub(source_progress.ops_finished), source_ops_rate_avg,
933 source_ops_rate_curr,
934 bytesize::ByteSize(source_bytes_rate_avg as u64),
935 bytesize::ByteSize(source_bytes_rate_curr as u64),
936 bytesize::ByteSize(source_progress.bytes_copied),
937 source_progress.files_copied,
938 source_files_rate_avg,
939 source_files_rate_curr,
940 source_progress.files_skipped,
942 source_progress.symlinks_skipped,
943 source_progress.directories_skipped,
944 source_progress.specials_skipped,
945 dest_progress
947 .ops_started
948 .saturating_sub(dest_progress.ops_finished), dest_ops_rate_avg,
950 dest_ops_rate_curr,
951 bytesize::ByteSize(dest_bytes_rate_avg as u64),
952 bytesize::ByteSize(dest_bytes_rate_curr as u64),
953 bytesize::ByteSize(dest_progress.bytes_copied),
954 dest_progress.files_copied,
956 dest_progress.symlinks_created,
957 dest_progress.directories_created,
958 dest_progress.hard_links_created,
959 dest_progress.files_unchanged,
961 dest_progress.symlinks_unchanged,
962 dest_progress.directories_unchanged,
963 dest_progress.hard_links_unchanged,
964 bytesize::ByteSize(dest_progress.bytes_removed),
966 dest_progress.files_removed,
967 dest_progress.symlinks_removed,
968 dest_progress.directories_removed,
969 ))
970 }
971}
972
973impl Default for RcpdProgressPrinter {
974 fn default() -> Self {
975 Self::new()
976 }
977}
978
979#[cfg(test)]
980mod tests {
981 use super::*;
982 use crate::remote_tracing::TracingMessage;
983 use anyhow::Result;
984
985 #[test]
986 fn basic_counting() -> Result<()> {
987 let tls_counter = TlsCounter::new();
988 for _ in 0..10 {
989 tls_counter.inc();
990 }
991 assert!(tls_counter.get() == 10);
992 Ok(())
993 }
994
995 #[test]
996 fn threaded_counting() -> Result<()> {
997 let tls_counter = TlsCounter::new();
998 std::thread::scope(|scope| {
999 let mut handles = Vec::new();
1000 for _ in 0..10 {
1001 handles.push(scope.spawn(|| {
1002 for _ in 0..100 {
1003 tls_counter.inc();
1004 }
1005 }));
1006 }
1007 });
1008 assert!(tls_counter.get() == 1000);
1009 Ok(())
1010 }
1011
1012 #[test]
1013 fn basic_guard() -> Result<()> {
1014 let tls_progress = ProgressCounter::new();
1015 let _guard = tls_progress.guard();
1016 Ok(())
1017 }
1018
1019 #[test]
1020 fn test_serializable_progress() -> Result<()> {
1021 let progress = Progress::new();
1022
1023 progress.files_copied.inc();
1025 progress.bytes_copied.add(1024);
1026 progress.directories_created.add(2);
1027
1028 let serializable = SerializableProgress::from(&progress);
1030 assert_eq!(serializable.files_copied, 1);
1031 assert_eq!(serializable.bytes_copied, 1024);
1032 assert_eq!(serializable.directories_created, 2);
1033
1034 let _tracing_msg = TracingMessage::Progress(serializable);
1036
1037 Ok(())
1038 }
1039
1040 #[test]
1041 fn serializable_progress_never_reports_more_finished_than_started() {
1042 let progress = Progress::new();
1043 progress.ops.finished.inc();
1044
1045 let serializable = SerializableProgress::from(&progress);
1046
1047 assert_eq!(serializable.ops_started, 1);
1048 assert_eq!(serializable.ops_finished, 1);
1049 }
1050
1051 #[test]
1052 fn test_rcpd_progress_printer() -> Result<()> {
1053 let mut printer = RcpdProgressPrinter::new();
1054
1055 let source_progress = SerializableProgress {
1057 ops_started: 100,
1058 ops_finished: 80,
1059 bytes_copied: 1024,
1060 files_copied: 5,
1061 files_skipped: 3,
1062 symlinks_skipped: 1,
1063 directories_skipped: 2,
1064 ..Default::default()
1065 };
1066
1067 let dest_progress = SerializableProgress {
1068 ops_started: 80,
1069 ops_finished: 70,
1070 bytes_copied: 1024,
1071 files_copied: 8,
1072 symlinks_created: 2,
1073 directories_created: 1,
1074 ..Default::default()
1075 };
1076
1077 let output = printer.print(&source_progress, &dest_progress)?;
1079 assert!(output.contains("SOURCE"));
1080 assert!(output.contains("DESTINATION"));
1081 assert!(output.contains("OPS:"));
1082 assert!(output.contains("pending:"));
1083 assert!(output.contains("20")); assert!(output.contains("10")); let mut sections = output.split("==== DESTINATION ====");
1086 let source_section = sections.next().unwrap();
1087 let dest_section = sections.next().unwrap_or("");
1088 let source_files_line = source_section
1089 .lines()
1090 .find(|line| line.trim_start().starts_with("files:"))
1091 .expect("source files line missing");
1092 assert!(source_files_line.trim_start().ends_with("5"));
1093 assert!(!source_files_line.contains('.'));
1094 let dest_files_line = dest_section
1095 .lines()
1096 .find(|line| line.trim_start().starts_with("files:"))
1097 .expect("dest files line missing");
1098 assert!(dest_files_line.trim_start().ends_with("8"));
1099 assert!(!dest_files_line.contains('.'));
1100 assert!(source_section.contains("SKIPPED:"));
1102 let skipped_section = source_section
1103 .split("SKIPPED:")
1104 .nth(1)
1105 .expect("SKIPPED section missing in source");
1106 let skipped_lines: Vec<&str> = skipped_section.lines().collect();
1107 let skipped_files_line = skipped_lines
1108 .iter()
1109 .find(|line| line.trim_start().starts_with("files:"))
1110 .expect("skipped files line missing");
1111 assert!(skipped_files_line.trim_start().ends_with("3"));
1112 let skipped_symlinks_line = skipped_lines
1113 .iter()
1114 .find(|line| line.trim_start().starts_with("symlinks:"))
1115 .expect("skipped symlinks line missing");
1116 assert!(skipped_symlinks_line.trim_start().ends_with("1"));
1117 let skipped_dirs_line = skipped_lines
1118 .iter()
1119 .find(|line| line.trim_start().starts_with("directories:"))
1120 .expect("skipped directories line missing");
1121 assert!(skipped_dirs_line.trim_start().ends_with("2"));
1122
1123 Ok(())
1124 }
1125
1126 #[test]
1127 fn rcpd_progress_printer_treats_inverted_counts_as_no_pending_work() -> Result<()> {
1128 let mut printer = RcpdProgressPrinter::new();
1129 let source_progress = SerializableProgress {
1130 ops_started: 2,
1131 ops_finished: 3,
1132 ..Default::default()
1133 };
1134 let dest_progress = SerializableProgress {
1135 ops_started: 4,
1136 ops_finished: 5,
1137 ..Default::default()
1138 };
1139
1140 let output = printer.print(&source_progress, &dest_progress)?;
1141 let pending: Vec<_> = output
1142 .lines()
1143 .filter_map(|line| line.trim().strip_prefix("pending:"))
1144 .map(str::trim)
1145 .collect();
1146
1147 assert_eq!(pending, vec!["0", "0"]);
1148 Ok(())
1149 }
1150
1151 #[test]
1152 fn interleaved_counter_access() -> Result<()> {
1153 let counter_a = TlsCounter::new();
1156 let counter_b = TlsCounter::new();
1157 let counter_c = TlsCounter::new();
1158 for i in 0..100 {
1159 counter_a.add(1);
1160 counter_b.add(2);
1161 counter_c.add(3);
1162 if i % 10 == 0 {
1164 assert_eq!(counter_a.get(), i + 1);
1165 assert_eq!(counter_b.get(), (i + 1) * 2);
1166 assert_eq!(counter_c.get(), (i + 1) * 3);
1167 }
1168 }
1169 assert_eq!(counter_a.get(), 100);
1171 assert_eq!(counter_b.get(), 200);
1172 assert_eq!(counter_c.get(), 300);
1173 Ok(())
1174 }
1175
1176 #[test]
1177 fn concurrent_multi_counter_access() -> Result<()> {
1178 let counter_a = std::sync::Arc::new(TlsCounter::new());
1180 let counter_b = std::sync::Arc::new(TlsCounter::new());
1181 const THREADS: usize = 4;
1182 const ITERATIONS: u64 = 1000;
1183 let handles: Vec<_> = (0..THREADS)
1184 .map(|_| {
1185 let ca = counter_a.clone();
1186 let cb = counter_b.clone();
1187 std::thread::spawn(move || {
1188 for _ in 0..ITERATIONS {
1189 ca.add(1);
1190 cb.add(2);
1191 }
1192 })
1193 })
1194 .collect();
1195 for h in handles {
1196 h.join().unwrap();
1197 }
1198 assert_eq!(counter_a.get(), THREADS as u64 * ITERATIONS);
1200 assert_eq!(counter_b.get(), THREADS as u64 * ITERATIONS * 2);
1201 Ok(())
1202 }
1203
1204 #[test]
1205 fn repeated_counter_access() -> Result<()> {
1206 let counter = TlsCounter::new();
1208 for i in 1..=1000 {
1209 counter.add(1);
1210 assert_eq!(counter.get(), i);
1211 }
1212 Ok(())
1213 }
1214
1215 #[test]
1216 fn sharding_distributes_across_threads() -> Result<()> {
1217 let counter = std::sync::Arc::new(TlsCounter::new());
1220 const THREADS: usize = 16;
1221 const ITERATIONS: u64 = 100;
1222 let handles: Vec<_> = (0..THREADS)
1223 .map(|_| {
1224 let c = counter.clone();
1225 std::thread::spawn(move || {
1226 for _ in 0..ITERATIONS {
1227 c.inc();
1228 }
1229 })
1230 })
1231 .collect();
1232 for h in handles {
1233 h.join().unwrap();
1234 }
1235 assert_eq!(counter.get(), THREADS as u64 * ITERATIONS);
1236 Ok(())
1237 }
1238
1239 #[test]
1240 fn sharding_handles_more_threads_than_shards() -> Result<()> {
1241 let counter = std::sync::Arc::new(TlsCounter::new());
1243 const THREADS: usize = 128; const ITERATIONS: u64 = 100;
1245 let handles: Vec<_> = (0..THREADS)
1246 .map(|_| {
1247 let c = counter.clone();
1248 std::thread::spawn(move || {
1249 for _ in 0..ITERATIONS {
1250 c.inc();
1251 }
1252 })
1253 })
1254 .collect();
1255 for h in handles {
1256 h.join().unwrap();
1257 }
1258 assert_eq!(counter.get(), THREADS as u64 * ITERATIONS);
1259 Ok(())
1260 }
1261
1262 #[test]
1263 fn counter_independence() -> Result<()> {
1264 let counters: Vec<_> = (0..10).map(|_| TlsCounter::new()).collect();
1266 for (i, counter) in counters.iter().enumerate() {
1267 counter.add((i + 1) as u64 * 100);
1268 }
1269 for (i, counter) in counters.iter().enumerate() {
1270 assert_eq!(counter.get(), (i + 1) as u64 * 100);
1271 }
1272 Ok(())
1273 }
1274
1275 #[test]
1276 fn copy_printer_omits_hard_links() -> Result<()> {
1277 let progress = Progress::new();
1278 let mut printer = CopyProgressPrinter::new(&progress);
1279 let output = printer.print()?;
1280 assert!(output.contains("COPIED:"));
1281 assert!(output.contains("UNCHANGED:"));
1282 assert!(output.contains("REMOVED:"));
1283 assert!(output.contains("SKIPPED:"));
1284 assert!(!output.contains("hard-links"));
1285 Ok(())
1286 }
1287
1288 #[test]
1289 fn remove_printer_hides_copy_and_unchanged() -> Result<()> {
1290 let progress = Progress::new();
1291 let mut printer = RemoveProgressPrinter::new(&progress);
1292 let output = printer.print()?;
1293 assert!(output.contains("REMOVED:"));
1294 assert!(output.contains("SKIPPED:"));
1295 assert!(!output.contains("COPIED:"));
1296 assert!(!output.contains("UNCHANGED:"));
1297 assert!(!output.contains("hard-links"));
1298 assert!(!output.contains("specials:"));
1299 Ok(())
1300 }
1301
1302 #[test]
1303 fn link_printer_shows_hard_links() -> Result<()> {
1304 let progress = Progress::new();
1305 let mut printer = LinkProgressPrinter::new(&progress);
1306 let output = printer.print()?;
1307 assert!(output.contains("LINKED / COPIED:"));
1308 assert!(output.contains("UNCHANGED:"));
1309 assert!(output.contains("REMOVED:"));
1310 assert!(output.contains("SKIPPED:"));
1311 assert!(output.contains("hard-links"));
1312 Ok(())
1313 }
1314
1315 #[test]
1316 fn compare_printer_only_shows_ops() -> Result<()> {
1317 let progress = Progress::new();
1318 let mut printer = CompareProgressPrinter::new(&progress);
1319 let output = printer.print()?;
1320 assert!(output.contains("OPS:"));
1321 assert!(!output.contains("COPIED:"));
1322 assert!(!output.contains("UNCHANGED:"));
1323 assert!(!output.contains("REMOVED:"));
1324 assert!(!output.contains("SKIPPED:"));
1325 assert!(!output.contains("GENERATED:"));
1326 Ok(())
1327 }
1328
1329 #[test]
1330 fn filegen_printer_shows_generated() -> Result<()> {
1331 let progress = Progress::new();
1332 let mut printer = FilegenProgressPrinter::new(&progress);
1333 let output = printer.print()?;
1334 assert!(output.contains("OPS:"));
1335 assert!(output.contains("GENERATED:"));
1336 assert!(!output.contains("COPIED:"));
1337 assert!(!output.contains("UNCHANGED:"));
1338 assert!(!output.contains("REMOVED:"));
1339 assert!(!output.contains("SKIPPED:"));
1340 assert!(!output.contains("symlinks:"));
1341 Ok(())
1342 }
1343
1344 #[test]
1345 fn modify_printer_renders_changed_section() {
1346 let progress = Progress::new();
1347 progress.files_changed.inc();
1348 progress.directories_unchanged.inc();
1349 let mut printer = ModifyProgressPrinter::new(&progress);
1350 let out = printer.print().unwrap();
1351 assert!(out.contains("CHANGED:"));
1352 assert!(out.contains("UNCHANGED:"));
1353 assert!(out.contains("SKIPPED:"));
1354 }
1355}