pub struct Attrs {
pub size: Option<u64>,
pub uid: Option<u32>,
pub gid: Option<u32>,
pub permissions: Option<u32>,
pub atime: Option<u32>,
pub mtime: Option<u32>,
}Expand description
The subset of SSH_FXP_ATTRS the origin layer needs.
size and mtime form the cache key, which is why a single READDIR can
replace a per-file STAT and keep the round trips flat.
uid deliberately does not answer “does this log belong to the account it
names”: it is a number, an author is a name, and turning one into the other
needs a passwd lookup there is no way to perform over the sftp subsystem.
owner_of_longname is what answers that.
Fields§
§size: Option<u64>§uid: Option<u32>§gid: Option<u32>§permissions: Option<u32>§atime: Option<u32>§mtime: Option<u32>Implementations§
Source§impl Attrs
impl Attrs
Sourcepub fn decode(d: &mut Dec<'_>) -> Option<Self>
pub fn decode(d: &mut Dec<'_>) -> Option<Self>
Examples found in repository?
examples/measure-roundtrips.rs (line 189)
157async fn list(s: &mut Session, dir: &str) -> Result<Vec<(String, Attrs)>> {
158 let id = s.alloc_id();
159 s.queue(OPENDIR, &Enc::new().u32(id).str(dir.as_bytes()).done())
160 .await?;
161 s.flush().await?;
162 let r = s.recv().await?;
163 ensure!(
164 r.kind == HANDLE,
165 "opendir {dir} refused (reply type {})",
166 r.kind
167 );
168 let handle = Dec::new(r.payload())
169 .str()
170 .context("opendir handle")?
171 .to_vec();
172
173 let mut out = Vec::new();
174 loop {
175 let id = s.alloc_id();
176 s.queue(READDIR, &Enc::new().u32(id).str(&handle).done())
177 .await?;
178 s.flush().await?;
179 let r = s.recv().await?;
180 if r.kind == STATUS {
181 break;
182 }
183 ensure!(r.kind == NAME, "readdir gave reply type {}", r.kind);
184 let mut d = Dec::new(r.payload());
185 let count = d.u32().context("readdir count")?;
186 for _ in 0..count {
187 let name = String::from_utf8_lossy(d.str().context("filename")?).into_owned();
188 d.str().context("longname")?;
189 let attrs = Attrs::decode(&mut d).context("attrs")?;
190 out.push((name, attrs));
191 }
192 }
193
194 let id = s.alloc_id();
195 s.queue(CLOSE, &Enc::new().u32(id).str(&handle).done())
196 .await?;
197 s.flush().await?;
198 s.recv().await?;
199 Ok(out)
200}Sourcepub fn is_dir(&self) -> bool
pub fn is_dir(&self) -> bool
Examples found in repository?
examples/measure-roundtrips.rs (line 54)
36async fn main() -> Result<()> {
37 let mut args = std::env::args().skip(1);
38 let host = args
39 .next()
40 .context("usage: measure-roundtrips <ssh-host> [remote-dir]")?;
41 let dir = args.next().unwrap_or_else(|| "/usr/include".to_string());
42
43 let (_child, w, r) = transport::open(&host)?;
44 let mut s = Sftp::handshake(w, r).await?;
45 println!("host {host} sftp v{}", s.version());
46
47 let tau = measure_tau(&mut s).await?;
48 println!("tau (one round trip) = {:.1} ms", ms(tau));
49
50 let entries = list(&mut s, &dir).await?;
51 let files: Vec<String> = entries
52 .iter()
53 .filter(|(name, a)| {
54 !a.is_dir() && !a.is_symlink() && a.size.unwrap_or(0) > 0 && name != "." && name != ".."
55 })
56 .map(|(name, _)| format!("{}/{}", dir.trim_end_matches('/'), name))
57 .collect();
58 ensure!(
59 !files.is_empty(),
60 "no regular non-empty files in {dir}; pass a different remote-dir"
61 );
62 println!(
63 "{} entries in {dir}, {} usable files",
64 entries.len(),
65 files.len()
66 );
67 println!();
68
69 let mut largest = 0usize;
70 let mut largest_open = 0.0f64;
71 let mut largest_read = 0.0f64;
72 let mut prev_read = 0.0f64;
73 let mut last_read = 0.0f64;
74 for n in BATCH_SIZES {
75 if n > files.len() {
76 continue;
77 }
78 let batch = &files[..n];
79 let (t_open, handles) = batch_open(&mut s, batch).await?;
80 let (t_read, bytes) = batch_read(&mut s, &handles).await?;
81 batch_close(&mut s, &handles).await?;
82
83 let open_tau = t_open.as_secs_f64() / tau.as_secs_f64();
84 let read_tau = t_read.as_secs_f64() / tau.as_secs_f64();
85 println!(
86 "n={n:<3} open {:>7.1} ms ({open_tau:>5.2} tau) read {:>7.1} ms ({read_tau:>5.2} tau) {bytes} B",
87 ms(t_open),
88 ms(t_read)
89 );
90 largest = n;
91 largest_open = open_tau;
92 largest_read = read_tau;
93 if n > 1 {
94 prev_read = last_read;
95 }
96 last_read = read_tau;
97 }
98
99 println!();
100 ensure!(
101 largest > 1,
102 "only one usable file; cannot distinguish pipelined from serial"
103 );
104 // The verdict rests on opens alone. An open carries a path and nothing else, so
105 // its cost is round trips and only round trips. A read also carries the file,
106 // and under an injected per-packet delay the transfer dominates: 400 KB reads
107 // as 15 tau however few round trips fetched it. Judging on reads made this
108 // check fail on how much data the chosen directory happens to hold, which is
109 // a property of the machine rather than of the code -- exactly the kind of
110 // flaky gate that teaches people to ignore a red check.
111 //
112 // Reads still say something, just not in absolute terms: if doubling the batch
113 // does not double the time, the round trips did not scale either.
114 if prev_read > 0.0 {
115 let growth = largest_read / prev_read;
116 println!(
117 "reads {largest_read:.2} tau at n={largest}, {growth:.2}x the previous batch (serial would be about 2x)"
118 );
119 }
120
121 let serial = largest as f64;
122 if largest_open < serial / PIPELINE_MARGIN {
123 println!(
124 "VERDICT pipelined: opens cost {largest_open:.2} tau at n={largest} (serial would cost about {serial:.0})"
125 );
126 Ok(())
127 } else {
128 bail!(
129 "VERDICT serial: opens cost {largest_open:.2} tau at n={largest}, near the serial cost {serial:.0}. Invariant 1 (O(1) round trips per page) is not reachable over this transport."
130 )
131 }
132}Sourcepub fn is_symlink(&self) -> bool
pub fn is_symlink(&self) -> bool
Examples found in repository?
examples/measure-roundtrips.rs (line 54)
36async fn main() -> Result<()> {
37 let mut args = std::env::args().skip(1);
38 let host = args
39 .next()
40 .context("usage: measure-roundtrips <ssh-host> [remote-dir]")?;
41 let dir = args.next().unwrap_or_else(|| "/usr/include".to_string());
42
43 let (_child, w, r) = transport::open(&host)?;
44 let mut s = Sftp::handshake(w, r).await?;
45 println!("host {host} sftp v{}", s.version());
46
47 let tau = measure_tau(&mut s).await?;
48 println!("tau (one round trip) = {:.1} ms", ms(tau));
49
50 let entries = list(&mut s, &dir).await?;
51 let files: Vec<String> = entries
52 .iter()
53 .filter(|(name, a)| {
54 !a.is_dir() && !a.is_symlink() && a.size.unwrap_or(0) > 0 && name != "." && name != ".."
55 })
56 .map(|(name, _)| format!("{}/{}", dir.trim_end_matches('/'), name))
57 .collect();
58 ensure!(
59 !files.is_empty(),
60 "no regular non-empty files in {dir}; pass a different remote-dir"
61 );
62 println!(
63 "{} entries in {dir}, {} usable files",
64 entries.len(),
65 files.len()
66 );
67 println!();
68
69 let mut largest = 0usize;
70 let mut largest_open = 0.0f64;
71 let mut largest_read = 0.0f64;
72 let mut prev_read = 0.0f64;
73 let mut last_read = 0.0f64;
74 for n in BATCH_SIZES {
75 if n > files.len() {
76 continue;
77 }
78 let batch = &files[..n];
79 let (t_open, handles) = batch_open(&mut s, batch).await?;
80 let (t_read, bytes) = batch_read(&mut s, &handles).await?;
81 batch_close(&mut s, &handles).await?;
82
83 let open_tau = t_open.as_secs_f64() / tau.as_secs_f64();
84 let read_tau = t_read.as_secs_f64() / tau.as_secs_f64();
85 println!(
86 "n={n:<3} open {:>7.1} ms ({open_tau:>5.2} tau) read {:>7.1} ms ({read_tau:>5.2} tau) {bytes} B",
87 ms(t_open),
88 ms(t_read)
89 );
90 largest = n;
91 largest_open = open_tau;
92 largest_read = read_tau;
93 if n > 1 {
94 prev_read = last_read;
95 }
96 last_read = read_tau;
97 }
98
99 println!();
100 ensure!(
101 largest > 1,
102 "only one usable file; cannot distinguish pipelined from serial"
103 );
104 // The verdict rests on opens alone. An open carries a path and nothing else, so
105 // its cost is round trips and only round trips. A read also carries the file,
106 // and under an injected per-packet delay the transfer dominates: 400 KB reads
107 // as 15 tau however few round trips fetched it. Judging on reads made this
108 // check fail on how much data the chosen directory happens to hold, which is
109 // a property of the machine rather than of the code -- exactly the kind of
110 // flaky gate that teaches people to ignore a red check.
111 //
112 // Reads still say something, just not in absolute terms: if doubling the batch
113 // does not double the time, the round trips did not scale either.
114 if prev_read > 0.0 {
115 let growth = largest_read / prev_read;
116 println!(
117 "reads {largest_read:.2} tau at n={largest}, {growth:.2}x the previous batch (serial would be about 2x)"
118 );
119 }
120
121 let serial = largest as f64;
122 if largest_open < serial / PIPELINE_MARGIN {
123 println!(
124 "VERDICT pipelined: opens cost {largest_open:.2} tau at n={largest} (serial would cost about {serial:.0})"
125 );
126 Ok(())
127 } else {
128 bail!(
129 "VERDICT serial: opens cost {largest_open:.2} tau at n={largest}, near the serial cost {serial:.0}. Invariant 1 (O(1) round trips per page) is not reachable over this transport."
130 )
131 }
132}Trait Implementations§
Auto Trait Implementations§
impl Freeze for Attrs
impl RefUnwindSafe for Attrs
impl Send for Attrs
impl Sync for Attrs
impl Unpin for Attrs
impl UnsafeUnpin for Attrs
impl UnwindSafe for Attrs
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more