1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

perf: optimize scanner timer usage and app protection matching

- Replace time.After() with reusable timer to reduce GC pressure
- Use pre-compiled regex for app bundle matching (O(1) vs O(N))
- Fix Bash 3.2 compatibility (remove local -n usage)
This commit is contained in:
tw93
2026-01-30 15:37:13 +08:00
parent 89dcb0c3b5
commit e81be16031
2 changed files with 120 additions and 25 deletions

View File

@@ -119,6 +119,16 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
size := getActualFileSize(fullPath, info) size := getActualFileSize(fullPath, info)
atomic.AddInt64(&total, size) atomic.AddInt64(&total, size)
// Reuse timer to reduce GC pressure
timer := time.NewTimer(0)
// Ensure timer is drained immediately since we start with 0
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
select { select {
case entryChan <- dirEntry{ case entryChan <- dirEntry{
Name: child.Name() + " →", Name: child.Name() + " →",
@@ -127,10 +137,26 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
IsDir: isDir, IsDir: isDir,
LastAccess: getLastAccessTimeFromInfo(info), LastAccess: getLastAccessTimeFromInfo(info),
}: }:
case <-time.After(100 * time.Millisecond): default:
// If channel is full, use timer to wait with timeout
timer.Reset(100 * time.Millisecond)
select {
case entryChan <- dirEntry{
Name: child.Name() + " →",
Path: fullPath,
Size: size,
IsDir: isDir,
LastAccess: getLastAccessTimeFromInfo(info),
}:
if !timer.Stop() {
<-timer.C
}
case <-timer.C:
// Skip if channel is blocked // Skip if channel is blocked
} }
}
continue continue
} }
if child.IsDir() { if child.IsDir() {
@@ -162,6 +188,7 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
atomic.AddInt64(&total, size) atomic.AddInt64(&total, size)
atomic.AddInt64(dirsScanned, 1) atomic.AddInt64(dirsScanned, 1)
timer := time.NewTimer(100 * time.Millisecond)
select { select {
case entryChan <- dirEntry{ case entryChan <- dirEntry{
Name: name, Name: name,
@@ -170,7 +197,10 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
IsDir: true, IsDir: true,
LastAccess: time.Time{}, LastAccess: time.Time{},
}: }:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
}(child.Name(), fullPath) }(child.Name(), fullPath)
continue continue
@@ -195,6 +225,7 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
atomic.AddInt64(&total, size) atomic.AddInt64(&total, size)
atomic.AddInt64(dirsScanned, 1) atomic.AddInt64(dirsScanned, 1)
timer := time.NewTimer(100 * time.Millisecond)
select { select {
case entryChan <- dirEntry{ case entryChan <- dirEntry{
Name: name, Name: name,
@@ -203,7 +234,10 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
IsDir: true, IsDir: true,
LastAccess: time.Time{}, LastAccess: time.Time{},
}: }:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
}(child.Name(), fullPath) }(child.Name(), fullPath)
continue continue
@@ -219,6 +253,7 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
atomic.AddInt64(&total, size) atomic.AddInt64(&total, size)
atomic.AddInt64(dirsScanned, 1) atomic.AddInt64(dirsScanned, 1)
timer := time.NewTimer(100 * time.Millisecond)
select { select {
case entryChan <- dirEntry{ case entryChan <- dirEntry{
Name: name, Name: name,
@@ -227,7 +262,10 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
IsDir: true, IsDir: true,
LastAccess: time.Time{}, LastAccess: time.Time{},
}: }:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
}(child.Name(), fullPath) }(child.Name(), fullPath)
continue continue
@@ -243,6 +281,9 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
atomic.AddInt64(filesScanned, 1) atomic.AddInt64(filesScanned, 1)
atomic.AddInt64(bytesScanned, size) atomic.AddInt64(bytesScanned, size)
// Single-use timer for main loop (less pressure than tight loop above)
// But let's be consistent and optimized
timer := time.NewTimer(100 * time.Millisecond)
select { select {
case entryChan <- dirEntry{ case entryChan <- dirEntry{
Name: child.Name(), Name: child.Name(),
@@ -251,16 +292,23 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
IsDir: false, IsDir: false,
LastAccess: getLastAccessTimeFromInfo(info), LastAccess: getLastAccessTimeFromInfo(info),
}: }:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
// Track large files only. // Track large files only.
if !shouldSkipFileForLargeTracking(fullPath) { if !shouldSkipFileForLargeTracking(fullPath) {
minSize := atomic.LoadInt64(&largeFileMinSize) minSize := atomic.LoadInt64(&largeFileMinSize)
if size >= minSize { if size >= minSize {
timer.Reset(100 * time.Millisecond)
select { select {
case largeFileChan <- fileEntry{Name: child.Name(), Path: fullPath, Size: size}: case largeFileChan <- fileEntry{Name: child.Name(), Path: fullPath, Size: size}:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
} }
} }
@@ -471,6 +519,15 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, lar
maxConcurrent := min(runtime.NumCPU()*2, maxDirWorkers) maxConcurrent := min(runtime.NumCPU()*2, maxDirWorkers)
sem := make(chan struct{}, maxConcurrent) sem := make(chan struct{}, maxConcurrent)
// Reuse timer for large file sends
timer := time.NewTimer(0)
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
for _, child := range children { for _, child := range children {
fullPath := filepath.Join(root, child.Name()) fullPath := filepath.Join(root, child.Name())
@@ -536,9 +593,13 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, lar
if !shouldSkipFileForLargeTracking(fullPath) && largeFileMinSize != nil { if !shouldSkipFileForLargeTracking(fullPath) && largeFileMinSize != nil {
minSize := atomic.LoadInt64(largeFileMinSize) minSize := atomic.LoadInt64(largeFileMinSize)
if size >= minSize { if size >= minSize {
timer.Reset(100 * time.Millisecond)
select { select {
case largeFileChan <- fileEntry{Name: child.Name(), Path: fullPath, Size: size}: case largeFileChan <- fileEntry{Name: child.Name(), Path: fullPath, Size: size}:
case <-time.After(100 * time.Millisecond): if !timer.Stop() {
<-timer.C
}
case <-timer.C:
} }
} }
} }

View File

@@ -545,24 +545,53 @@ bundle_matches_pattern() {
return 1 return 1
} }
# Helper to build regex from array (Bash 3.2 compatible - no namerefs)
# $1: Variable name to store result
# $2...: Array elements (passed as expanded list)
build_regex_var() {
local var_name="$1"
shift
local regex=""
for pattern in "$@"; do
# Escape dots . -> \.
local p="${pattern//./\\.}"
# Convert * to .*
p="${p//\*/.*}"
# Start and end anchors
p="^${p}$"
if [[ -z "$regex" ]]; then
regex="$p"
else
regex="$regex|$p"
fi
done
eval "$var_name=\"\$regex\""
}
# Generate Regex strings once
APPLE_UNINSTALLABLE_REGEX=""
build_regex_var APPLE_UNINSTALLABLE_REGEX "${APPLE_UNINSTALLABLE_APPS[@]}"
SYSTEM_CRITICAL_REGEX=""
build_regex_var SYSTEM_CRITICAL_REGEX "${SYSTEM_CRITICAL_BUNDLES[@]}"
DATA_PROTECTED_REGEX=""
build_regex_var DATA_PROTECTED_REGEX "${DATA_PROTECTED_BUNDLES[@]}"
# Check if application is a protected system component # Check if application is a protected system component
should_protect_from_uninstall() { should_protect_from_uninstall() {
local bundle_id="$1" local bundle_id="$1"
# First check if it's an uninstallable Apple app # First check if it's an uninstallable Apple app
# These apps have com.apple.* bundle IDs but are NOT system-critical if [[ "$bundle_id" =~ $APPLE_UNINSTALLABLE_REGEX ]]; then
for pattern in "${APPLE_UNINSTALLABLE_APPS[@]}"; do
if bundle_matches_pattern "$bundle_id" "$pattern"; then
return 1 # Can be uninstalled return 1 # Can be uninstalled
fi fi
done
# Then check system-critical components # Then check system-critical components
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}"; do if [[ "$bundle_id" =~ $SYSTEM_CRITICAL_REGEX ]]; then
if bundle_matches_pattern "$bundle_id" "$pattern"; then
return 0 # Protected return 0 # Protected
fi fi
done
return 1 return 1
} }
@@ -570,12 +599,17 @@ should_protect_from_uninstall() {
# Check if application data should be protected during cleanup # Check if application data should be protected during cleanup
should_protect_data() { should_protect_data() {
local bundle_id="$1" local bundle_id="$1"
# Protect both system critical and data protected bundles during cleanup
for pattern in "${SYSTEM_CRITICAL_BUNDLES[@]}" "${DATA_PROTECTED_BUNDLES[@]}"; do # Check system critical
if bundle_matches_pattern "$bundle_id" "$pattern"; then if [[ "$bundle_id" =~ $SYSTEM_CRITICAL_REGEX ]]; then
return 0 return 0
fi fi
done
# Check data protected
if [[ "$bundle_id" =~ $DATA_PROTECTED_REGEX ]]; then
return 0
fi
return 1 return 1
} }