@@ -9,7 +9,7 @@ use crate::state::profile_state::Profile;
99use crate :: state:: State ;
1010use futures:: stream:: { iter, StreamExt } ;
1111use log:: { debug, error, info, trace, warn} ;
12- use std:: collections:: HashSet ;
12+ use std:: collections:: { HashMap , HashSet } ;
1313use std:: path:: { Path , PathBuf } ;
1414use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
1515use 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