From c5d6cdc2f9d8a8d4f0b766f44522141a9850d255 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Sat, 21 Mar 2026 15:33:57 +0800 Subject: [PATCH] perf(purge/uninstall): eliminate per-item subprocess forks in scan phase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit purge: - is_safe_project_artifact: replace echo|tr|wc depth calc with pure bash string arithmetic (2 forks → 0 per item) - process_scan_results: replace dirname subprocess with ${item%/*} - is_recently_modified: accept pre-computed epoch to avoid redundant get_epoch_seconds call (N date forks → 1 before loop) - fd pattern construction: replace 45 per-target sed forks with single printf|sed pass (45 forks → 2) - pre-compute _cached_project_paths alongside existing name/basename cache so display loop avoids get_project_path subshell per item uninstall/app-selector: - default size display: N/A → -- for apps without cached size data (cleaner UX on first scan; real sizes populate from background refresh) - Pass 1 scan: replace basename/dirname subshells with parameter expansion --- bin/uninstall.sh | 14 ++++++-------- lib/clean/project.sh | 34 ++++++++++++++++++---------------- lib/ui/app_selector.sh | 4 ++-- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/bin/uninstall.sh b/bin/uninstall.sh index 5c96661..e5bee83 100755 --- a/bin/uninstall.sh +++ b/bin/uninstall.sh @@ -469,12 +469,11 @@ scan_applications() { while IFS= read -r -d '' app_path; do if [[ ! -e "$app_path" ]]; then continue; fi - local app_name - app_name=$(basename "$app_path" .app) + local app_name="${app_path##*/}" + app_name="${app_name%.app}" # Skip nested apps inside another .app bundle. - local parent_dir - parent_dir=$(dirname "$app_path") + local parent_dir="${app_path%/*}" if [[ "$parent_dir" == *".app" || "$parent_dir" == *".app/"* ]]; then continue fi @@ -485,9 +484,8 @@ scan_applications() { if [[ -n "$link_target" ]]; then local resolved_target="$link_target" if [[ "$link_target" != /* ]]; then - local link_dir - link_dir=$(dirname "$app_path") - resolved_target=$(cd "$link_dir" 2> /dev/null && cd "$(dirname "$link_target")" 2> /dev/null && pwd)/$(basename "$link_target") 2> /dev/null || echo "" + local link_dir="${app_path%/*}" + resolved_target=$(cd "$link_dir" 2> /dev/null && cd "${link_target%/*}" 2> /dev/null && pwd)/"${link_target##*/}" 2> /dev/null || echo "" fi case "$resolved_target" in /System/* | /usr/bin/* | /usr/lib/* | /bin/* | /sbin/* | /private/etc/*) @@ -661,7 +659,7 @@ scan_applications() { fi local final_size_kb=0 - local final_size="N/A" + local final_size="--" if [[ "$cached_size_kb" =~ ^[0-9]+$ && $cached_size_kb -gt 0 ]]; then final_size_kb="$cached_size_kb" final_size=$(bytes_to_human "$((cached_size_kb * 1024))") diff --git a/lib/clean/project.sh b/lib/clean/project.sh index a4cb55f..62ba2a8 100644 --- a/lib/clean/project.sh +++ b/lib/clean/project.sh @@ -285,7 +285,8 @@ is_safe_project_artifact() { # Must not be a direct child of the search root. local relative_path="${path#"$search_path"/}" - local depth=$(echo "$relative_path" | LC_ALL=C tr -cd '/' | wc -c) + local _rel_stripped="${relative_path//\//}" + local depth=$((${#relative_path} - ${#_rel_stripped})) if [[ $depth -lt 1 ]]; then # Allow direct-child artifacts only when the search path is itself # a project root (single-project mode). @@ -429,7 +430,7 @@ scan_purge_targets() { if [[ -n "$item" ]] && is_safe_project_artifact "$item" "$search_path"; then echo "$item" # Update scanning path to show current project directory - local project_dir=$(dirname "$item") + local project_dir="${item%/*}" echo "$project_dir" > "$stats_dir/purge_scanning" 2> /dev/null || true fi done < "$input_file" | filter_nested_artifacts | filter_protected_artifacts > "$output_file" @@ -446,15 +447,11 @@ scan_purge_targets() { debug_log "MO_USE_FIND=1: Forcing find instead of fd" use_find=true elif command -v fd > /dev/null 2>&1; then - # Escape regex special characters in target names for fd patterns - local escaped_targets=() - for target in "${PURGE_TARGETS[@]}"; do - escaped_targets+=("^$(printf '%s' "$target" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g')\$") - done - local pattern="($( - IFS='|' - echo "${escaped_targets[*]}" - ))" + # Escape regex special characters in target names for fd patterns (single sed pass) + local _escaped_lines + _escaped_lines=$(printf '%s\n' "${PURGE_TARGETS[@]}" | sed -e 's/[][(){}.^$*+?|\\]/\\&/g') + local pattern + pattern="($(printf '%s\n' "$_escaped_lines" | sed -e 's/^/^/' -e 's/$/$/' | paste -sd '|' -))" local fd_args=( "--absolute-path" "--hidden" @@ -546,14 +543,16 @@ filter_protected_artifacts() { # Check if a path was modified recently (safety check). is_recently_modified() { local path="$1" + local current_time="${2:-}" local age_days=$MIN_AGE_DAYS if [[ ! -e "$path" ]]; then return 1 fi local mod_time mod_time=$(get_file_mtime "$path") - local current_time - current_time=$(get_epoch_seconds) + if [[ -z "$current_time" || ! "$current_time" =~ ^[0-9]+$ ]]; then + current_time=$(get_epoch_seconds) + fi local age_seconds=$((current_time - mod_time)) local age_in_days=$((age_seconds / 86400)) if [[ $age_in_days -lt $age_days ]]; then @@ -1048,8 +1047,10 @@ clean_project_artifacts() { return 2 # Special code: nothing to clean fi # Mark recently modified items (for default selection state) + local _now_epoch + _now_epoch=$(get_epoch_seconds) for item in "${all_found_items[@]}"; do - if is_recently_modified "$item"; then + if is_recently_modified "$item" "$_now_epoch"; then recently_modified+=("$item") fi # Add all items to safe_to_clean, let user choose @@ -1332,10 +1333,12 @@ clean_project_artifacts() { # can avoid repeated filesystem traversals during the O(N^2) duplicate check. local -a _cached_basenames=() local -a _cached_project_names=() + local -a _cached_project_paths=() local _pre_idx for _pre_idx in "${!safe_to_clean[@]}"; do _cached_basenames[_pre_idx]="${safe_to_clean[$_pre_idx]##*/}" _cached_project_names[_pre_idx]=$(get_project_name "${safe_to_clean[$_pre_idx]}") + _cached_project_paths[_pre_idx]=$(get_project_path "${safe_to_clean[$_pre_idx]}") done # Build menu options - one line per artifact @@ -1346,8 +1349,7 @@ clean_project_artifacts() { local -a item_display_paths=() local _sz_idx=0 for item in "${safe_to_clean[@]}"; do - local project_path - project_path=$(get_project_path "$item") + local project_path="${_cached_project_paths[$_sz_idx]}" local artifact_type artifact_type=$(get_artifact_display_name "$item") local size_raw diff --git a/lib/ui/app_selector.sh b/lib/ui/app_selector.sh index add9015..758326d 100755 --- a/lib/ui/app_selector.sh +++ b/lib/ui/app_selector.sh @@ -17,8 +17,8 @@ format_app_display() { fi # Format size - local size_str="N/A" - [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" ]] && size_str="$size" + local size_str="--" + [[ "$size" != "0" && "$size" != "" && "$size" != "Unknown" && "$size" != "N/A" && "$size" != "--" ]] && size_str="$size" # Calculate available width for app name based on terminal width # Accept pre-calculated max_name_width (5th param) to avoid recalculation in loops