Skip to content

Commit c1a709c

Browse files
feat: improve nrc assets source speed check
1 parent aef3224 commit c1a709c

1 file changed

Lines changed: 81 additions & 86 deletions

File tree

src-tauri/src/minecraft/downloads/norisk_assets_download.rs

Lines changed: 81 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use crate::state::profile_state::Profile;
99
use crate::state::State;
1010
use futures::stream::{iter, StreamExt};
1111
use log::{debug, error, info, trace, warn};
12-
use std::collections::HashSet;
12+
use std::collections::{HashMap, HashSet};
1313
use std::path::{Path, PathBuf};
1414
use std::sync::atomic::{AtomicUsize, Ordering};
1515
use std::sync::Arc;
@@ -236,12 +236,12 @@ impl NoriskClientAssetsDownloadService {
236236
)
237237
.await?;
238238

239-
match self
239+
match measure_time!("NRC cleanup", self
240240
.cleanup_orphan_assets(&target_base_dir, &all_expected_target_paths)
241-
.await
241+
.await)
242242
{
243243
Ok(deleted_count) => info!(
244-
"[NRC Assets Cleanup] Successfully cleaned up {} orphan items.",
244+
"[NRC Assets Cleanup] Cleaned up {} orphan items",
245245
deleted_count
246246
),
247247
Err(e) => {
@@ -304,8 +304,8 @@ impl NoriskClientAssetsDownloadService {
304304
.await?;
305305

306306
let assets =
307-
match NoRiskApi::norisk_assets(asset_id, norisk_token, request_uuid, is_experimental)
308-
.await
307+
match measure_time!(format!("NRC API call '{}'", asset_id), NoRiskApi::norisk_assets(asset_id, norisk_token, request_uuid, is_experimental)
308+
.await)
309309
{
310310
Ok(fetched_assets) => {
311311
info!(
@@ -397,20 +397,17 @@ impl NoriskClientAssetsDownloadService {
397397
)
398398
.await?;
399399

400-
match self
400+
match measure_time!(format!("NRC download '{}'", asset_id), self
401401
.download_nrc_assets(
402402
asset_id,
403403
&assets,
404404
is_experimental,
405405
norisk_token,
406406
Some(profile_id),
407407
)
408-
.await
408+
.await)
409409
{
410-
Ok(_) => info!(
411-
"[NRC Assets Group '{}'] Successfully downloaded assets.",
412-
asset_id
413-
),
410+
Ok(_) => {},
414411
Err(e) => {
415412
error!(
416413
"[NRC Assets Group '{}'] Failed to download assets: {}. Skipping copy.",
@@ -431,7 +428,7 @@ impl NoriskClientAssetsDownloadService {
431428
.await?;
432429

433430
// Pass target_base_dir to copy function
434-
match self
431+
match measure_time!(format!("NRC copy '{}'", asset_id), self
435432
.copy_assets_to_game_dir(
436433
asset_id,
437434
&assets,
@@ -440,12 +437,9 @@ impl NoriskClientAssetsDownloadService {
440437
game_directory,
441438
Some(profile_id),
442439
)
443-
.await
440+
.await)
444441
{
445-
Ok(_) => info!(
446-
"[NRC Assets Group '{}'] Successfully copied assets.",
447-
asset_id
448-
),
442+
Ok(_) => {},
449443
Err(e) => {
450444
error!(
451445
"[NRC Assets Group '{}'] Failed to copy assets: {}. Skipping cleanup for this group's contribution.",
@@ -512,31 +506,34 @@ impl NoriskClientAssetsDownloadService {
512506
let mut job_count = 0;
513507

514508
let state = if profile_id.is_some() {
515-
State::get().await.ok() // Change to ok() to allow optional state
509+
State::get().await.ok()
516510
} else {
517511
None
518512
};
519-
// Clone state before the closure so the original `state` remains available after the loop
520513
let state_clone_for_inspect = state.clone();
521514

515+
// Pre-scan existing objects to avoid per-file fs::try_exists calls
516+
let objects_dir = assets_path.join("objects");
517+
let existing_objects = measure_time!(format!("NRC objects scan '{}'", asset_id), {
518+
Self::scan_objects_dir(&objects_dir).await
519+
});
520+
522521
for (name, asset) in assets_list {
523522
let hash = asset.hash.clone();
524523
let size = asset.size;
525-
526-
// Store assets by hash like Mojang: objects/ab/abcdef123...
527-
let hash_prefix = &hash[0..2]; // First 2 chars
528-
let objects_dir = assets_path.join("objects").join(hash_prefix);
529-
let target_path = objects_dir.join(&hash);
530-
524+
525+
let hash_prefix = &hash[0..2];
526+
let target_path = objects_dir.join(hash_prefix).join(&hash);
527+
531528
let name_clone = name.clone();
532529
let task_counter_clone = Arc::clone(&task_counter);
533530
let completed_counter_clone = Arc::clone(&completed_counter);
534531
let total_to_download_clone = Arc::clone(&total_to_download);
535532
let asset_id_clone = asset_id.to_string();
536533
let norisk_token_clone = norisk_token.to_string();
537534

538-
// Hash-based check - if hash file exists, content is guaranteed correct
539-
if fs::try_exists(&target_path).await? {
535+
// Fast in-memory check instead of filesystem call
536+
if existing_objects.contains_key(&hash) {
540537
trace!(
541538
"[NRC Assets Download '{}'] Skipping asset {} (hash {} already exists)",
542539
asset_id_clone,
@@ -734,6 +731,38 @@ impl NoriskClientAssetsDownloadService {
734731
}
735732
}
736733

734+
/// Scans an objects directory (objects/xx/hash) and returns a HashMap of hash -> file size.
735+
async fn scan_objects_dir(objects_path: &Path) -> HashMap<String, u64> {
736+
let mut existing = HashMap::new();
737+
738+
let mut prefix_dirs = match fs::read_dir(objects_path).await {
739+
Ok(dir) => dir,
740+
Err(_) => return existing,
741+
};
742+
743+
while let Ok(Some(prefix_entry)) = prefix_dirs.next_entry().await {
744+
if !prefix_entry.path().is_dir() {
745+
continue;
746+
}
747+
748+
let mut hash_files = match fs::read_dir(prefix_entry.path()).await {
749+
Ok(dir) => dir,
750+
Err(_) => continue,
751+
};
752+
753+
while let Ok(Some(hash_entry)) = hash_files.next_entry().await {
754+
if let Ok(metadata) = hash_entry.metadata().await {
755+
if metadata.is_file() {
756+
let file_name = hash_entry.file_name().to_string_lossy().to_string();
757+
existing.insert(file_name, metadata.len());
758+
}
759+
}
760+
}
761+
}
762+
763+
existing
764+
}
765+
737766
/// Helper method to emit progress events
738767
async fn emit_progress_event(
739768
&self,
@@ -821,6 +850,12 @@ impl NoriskClientAssetsDownloadService {
821850
.await?;
822851
}
823852

853+
// Pre-scan source objects to avoid per-file fs::try_exists + fs::metadata calls
854+
let source_objects_dir = source_dir.join("objects");
855+
let source_cache = measure_time!(format!("NRC copy source scan '{}'", asset_id), {
856+
Self::scan_objects_dir(&source_objects_dir).await
857+
});
858+
824859
let batch_size = 50;
825860
let mut batch_count = 0;
826861
let total_batches = (total_assets + batch_size - 1) / batch_size;
@@ -832,92 +867,52 @@ impl NoriskClientAssetsDownloadService {
832867

833868
for (name, asset) in chunk {
834869
let hash = &asset.hash;
835-
836-
// Source is now hash-based: objects/ab/abcdef123...
837-
let hash_prefix = &hash[0..2];
838-
let source_path = source_dir.join("objects").join(hash_prefix).join(hash);
839870

840871
// Special handling for override assets
841872
let (target_path, is_override) = if name.starts_with("overrides/") {
842-
// For overrides, copy to Minecraft directory and strip the "overrides/" prefix
843873
let relative_path = name.strip_prefix("overrides/").unwrap_or(name);
844874
(minecraft_dir.join(relative_path), true)
845875
} else {
846-
// Normal assets go to the target base directory
847876
(target_base_dir.join(&name), false)
848877
};
849878

850-
if !fs::try_exists(&source_path).await? {
851-
warn!(
852-
"[NRC Assets Copy '{}'] Hash file missing: {} (for asset {})",
853-
asset_id,
854-
source_path.display(),
855-
name
856-
);
857-
continue;
858-
}
879+
// Fast in-memory source check instead of fs::try_exists + fs::metadata
880+
let source_size = match source_cache.get(hash.as_str()) {
881+
Some(&size) => size,
882+
None => {
883+
warn!(
884+
"[NRC Assets Copy '{}'] Hash file missing in cache for asset {} (hash {})",
885+
asset_id, name, hash
886+
);
887+
continue;
888+
}
889+
};
859890

860891
let needs_copy = if fs::try_exists(&target_path).await? {
861892
if is_override {
862-
// For override files, skip if the file already exists
863-
debug!(
864-
"[NRC Assets Copy '{}'] Skipping override file {} (already exists)",
865-
asset_id, name
866-
);
867893
false
868894
} else if keep_local_assets {
869-
debug!(
870-
"[NRC Assets Copy '{}'] Keeping local asset {} (keep_local_assets)",
871-
asset_id, name
872-
);
873895
false
874896
} else {
875-
// Hash-based system: check size first, then modification time
876-
let source_metadata = fs::metadata(&source_path).await?;
897+
// Compare target size against cached source size
877898
let target_metadata = fs::metadata(&target_path).await?;
878-
879-
if target_metadata.len() as i64 != asset.size {
899+
if target_metadata.len() != source_size {
880900
debug!(
881-
"[NRC Assets Copy '{}'] Target size mismatch for {} (hash {}), needs copy",
882-
asset_id, name, hash
901+
"[NRC Assets Copy '{}'] Size mismatch for {}: target={}b, source={}b",
902+
asset_id, name, target_metadata.len(), source_size
883903
);
884904
true
885905
} else {
886-
// Same size - check if hash file (source) is newer than target
887-
let source_modified = source_metadata.modified().unwrap_or(std::time::UNIX_EPOCH);
888-
let target_modified = target_metadata.modified().unwrap_or(std::time::UNIX_EPOCH);
889-
890-
if source_modified > target_modified {
891-
debug!(
892-
"[NRC Assets Copy '{}'] Hash file {} is newer than target, needs copy",
893-
asset_id, name
894-
);
895-
true
896-
} else {
897-
trace!(
898-
"[NRC Assets Copy '{}'] Skipping {} (hash {}), target is up to date",
899-
asset_id, name, hash
900-
);
901-
false
902-
}
906+
false
903907
}
904908
}
905909
} else {
906-
if is_override {
907-
debug!(
908-
"[NRC Assets Copy '{}'] Override file doesn't exist for {} (hash {}), copying to Minecraft dir",
909-
asset_id, name, hash
910-
);
911-
} else {
912-
debug!(
913-
"[NRC Assets Copy '{}'] Target doesn't exist for {} (hash {}), needs copy",
914-
asset_id, name, hash
915-
);
916-
}
917910
true
918911
};
919912

920913
if needs_copy {
914+
let hash_prefix = &hash[0..2];
915+
let source_path = source_objects_dir.join(hash_prefix).join(hash);
921916
if let Some(parent) = target_path.parent() {
922917
if !fs::try_exists(parent).await? {
923918
fs::create_dir_all(parent).await?;

0 commit comments

Comments
 (0)