Skip to content

Commit a3e7c1f

Browse files
committed
Optimize path handling to eliminate getcwd() syscalls in async operations
This commit adds path canonicalization at all database and storage initialization points to eliminate repeated getcwd() syscalls during async file operations. Path canonicalization changes: - main.rs: Canonicalize fs_root and meta_root at server startup - cas/fs.rs: Canonicalize paths in CasFS::new() and new_multi_user() - cas/shared_block_store.rs: Canonicalize path in SharedBlockStore::new() These changes ensure all paths are absolute before being passed to fjall/async-fs, avoiding ~95ns getcwd() overhead per file operation in async context. Additional improvements: - inspect.rs: Enhanced multi-user support with user stats, bucket stats, and listing - http_ui: Improved pagination and admin user support - metastore: Added helper methods for user statistics
1 parent 38d7ee3 commit a3e7c1f

File tree

9 files changed

+743
-44
lines changed

9 files changed

+743
-44
lines changed

src/cas/fs.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,15 @@ impl CasFS {
132132
) -> Self {
133133
meta_path.push("db");
134134
root.push("blocks");
135+
136+
// Canonicalize both paths to eliminate getcwd() syscalls in async operations
137+
// This is critical for performance as it avoids repeated getcwd() on every file op
138+
std::fs::create_dir_all(&root).ok();
139+
root = root.canonicalize().unwrap_or(root);
140+
141+
std::fs::create_dir_all(&meta_path).ok();
142+
meta_path = meta_path.canonicalize().unwrap_or(meta_path);
143+
135144
let meta_store = match storage_engine {
136145
StorageEngine::Fjall => {
137146
let store = FjallStore::new(meta_path, inlined_metadata_size, durability);
@@ -190,6 +199,14 @@ impl CasFS {
190199
user_meta_path.push("db");
191200
root.push("blocks");
192201

202+
// Canonicalize both paths to eliminate getcwd() syscalls in async operations
203+
// This is critical for performance as it avoids repeated getcwd() on every file op
204+
std::fs::create_dir_all(&root).ok();
205+
root = root.canonicalize().unwrap_or(root);
206+
207+
std::fs::create_dir_all(&user_meta_path).ok();
208+
user_meta_path = user_meta_path.canonicalize().unwrap_or(user_meta_path);
209+
193210
let user_meta_store = match storage_engine {
194211
StorageEngine::Fjall => {
195212
let store = FjallStore::new(user_meta_path, inlined_metadata_size, durability);

src/cas/shared_block_store.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ impl SharedBlockStore {
3434
) -> Result<Self, MetaError> {
3535
path.push("db");
3636

37+
// Canonicalize path to eliminate getcwd() syscalls in async operations
38+
// This is critical for performance as it avoids repeated getcwd() on every file op
39+
std::fs::create_dir_all(&path).ok();
40+
path = path.canonicalize().unwrap_or(path);
41+
3742
let meta_store = match storage_engine {
3843
StorageEngine::Fjall => {
3944
let store = FjallStore::new(path, inlined_metadata_size, durability);

src/http_ui/handlers.rs

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ pub struct ObjectListResponse {
4848
pub directories: Vec<DirectoryInfo>,
4949
pub objects: Vec<ObjectInfo>,
5050
pub total_count: usize,
51+
pub has_more: bool,
52+
pub next_token: Option<String>,
5153
}
5254

5355
#[derive(Serialize)]
@@ -115,26 +117,46 @@ pub async fn list_objects(
115117
Ok(true) => {}
116118
}
117119

118-
// Parse prefix from query parameters
119-
let prefix = req
120-
.uri()
121-
.query()
122-
.and_then(|q| {
123-
q.split('&')
124-
.find(|p| p.starts_with("prefix="))
125-
.and_then(|p| p.strip_prefix("prefix="))
126-
.map(|p| urlencoding::decode(p).unwrap_or_default().to_string())
127-
})
120+
// Parse query parameters
121+
let query_params = req.uri().query().unwrap_or("");
122+
123+
let prefix = query_params
124+
.split('&')
125+
.find(|p| p.starts_with("prefix="))
126+
.and_then(|p| p.strip_prefix("prefix="))
127+
.map(|p| urlencoding::decode(p).unwrap_or_default().to_string())
128128
.unwrap_or_default();
129129

130+
let limit: usize = query_params
131+
.split('&')
132+
.find(|p| p.starts_with("limit="))
133+
.and_then(|p| p.strip_prefix("limit="))
134+
.and_then(|p| p.parse().ok())
135+
.unwrap_or(100); // Default limit of 100 items
136+
137+
let start_after = query_params
138+
.split('&')
139+
.find(|p| p.starts_with("token="))
140+
.and_then(|p| p.strip_prefix("token="))
141+
.map(|p| urlencoding::decode(p).unwrap_or_default().to_string());
142+
130143
// Get bucket tree and list objects
131144
match casfs.get_bucket(bucket) {
132145
Ok(tree) => {
133146
let mut directories = HashSet::new();
134147
let mut objects = Vec::new();
148+
let mut last_key: Option<String> = None;
149+
let mut item_count = 0;
150+
let mut has_more = false;
135151

136152
// Use range_filter to get objects with the given prefix
137-
for (key, obj) in tree.range_filter(None, Some(prefix.clone()), None) {
153+
for (key, obj) in tree.range_filter(start_after.clone(), Some(prefix.clone()), None) {
154+
// Check if we've hit the limit
155+
if item_count >= limit {
156+
has_more = true;
157+
break;
158+
}
159+
138160
// Check if this key has subdirectories after the prefix
139161
let relative_key = if prefix.is_empty() {
140162
key.as_str()
@@ -146,10 +168,16 @@ pub async fn list_objects(
146168
// This is a subdirectory
147169
let dir_name = &relative_key[..slash_pos + 1];
148170
let full_prefix = format!("{}{}", prefix, dir_name);
149-
directories.insert(DirectoryInfo {
171+
let dir_info = DirectoryInfo {
150172
name: dir_name.to_string(),
151173
prefix: full_prefix,
152-
});
174+
};
175+
176+
// Only count unique directories toward the limit
177+
if directories.insert(dir_info) {
178+
item_count += 1;
179+
last_key = Some(key.clone());
180+
}
153181
} else {
154182
// This is a file at the current level
155183
objects.push(ObjectInfo {
@@ -160,6 +188,8 @@ pub async fn list_objects(
160188
is_inlined: obj.is_inlined(),
161189
block_count: obj.blocks().len(),
162190
});
191+
item_count += 1;
192+
last_key = Some(key.clone());
163193
}
164194
}
165195

@@ -170,12 +200,16 @@ pub async fn list_objects(
170200

171201
let total_count = directories.len() + objects.len();
172202

203+
let next_token = if has_more { last_key } else { None };
204+
173205
let response = ObjectListResponse {
174206
bucket: bucket.to_string(),
175207
prefix,
176208
directories,
177209
objects,
178210
total_count,
211+
has_more,
212+
next_token,
179213
};
180214

181215
if wants_html {

src/http_ui/templates.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,23 @@ pub fn objects_page(response: &ObjectListResponse) -> String {
213213
}
214214
}
215215
}
216+
217+
// Pagination controls
218+
@if response.has_more {
219+
@if let Some(ref token) = response.next_token {
220+
div class="pagination" {
221+
@if response.prefix.is_empty() {
222+
a href={ "/buckets/" (response.bucket) "?token=" (urlencoding::encode(token)) } class="btn btn-primary" {
223+
"Load More (Next 100)"
224+
}
225+
} @else {
226+
a href={ "/buckets/" (response.bucket) "?prefix=" (urlencoding::encode(&response.prefix)) "&token=" (urlencoding::encode(token)) } class="btn btn-primary" {
227+
"Load More (Next 100)"
228+
}
229+
}
230+
}
231+
}
232+
}
216233
}
217234
};
218235

@@ -1270,6 +1287,18 @@ code {
12701287
color: #666;
12711288
}
12721289
1290+
/* Pagination */
1291+
.pagination {
1292+
margin-top: 2rem;
1293+
padding-top: 1rem;
1294+
border-top: 1px solid #ddd;
1295+
text-align: center;
1296+
}
1297+
1298+
.pagination .btn {
1299+
min-width: 150px;
1300+
}
1301+
12731302
@media (prefers-color-scheme: dark) {
12741303
body {
12751304
background: #1a1a1a;
@@ -1365,5 +1394,9 @@ code {
13651394
border-top-color: #444;
13661395
color: #a0a0a0;
13671396
}
1397+
1398+
.pagination {
1399+
border-top-color: #444;
1400+
}
13681401
}
13691402
"#;

0 commit comments

Comments
 (0)