mirror of
https://github.com/tw93/Mole.git
synced 2026-02-15 21:00:05 +00:00
Continuously optimize go analysis
This commit is contained in:
BIN
bin/analyze-go
BIN
bin/analyze-go
Binary file not shown.
BIN
cmd/analyze/analyze
Executable file
BIN
cmd/analyze/analyze
Executable file
Binary file not shown.
@@ -16,6 +16,13 @@ const (
|
|||||||
maxConcurrentOverview = 3 // Scan up to 3 overview dirs concurrently
|
maxConcurrentOverview = 3 // Scan up to 3 overview dirs concurrently
|
||||||
pathUpdateInterval = 500 // Update current path every N files
|
pathUpdateInterval = 500 // Update current path every N files
|
||||||
batchUpdateSize = 100 // Batch atomic updates every N items
|
batchUpdateSize = 100 // Batch atomic updates every N items
|
||||||
|
|
||||||
|
// Worker pool configuration
|
||||||
|
minWorkers = 16 // Minimum workers for better I/O throughput
|
||||||
|
maxWorkers = 128 // Maximum workers to avoid excessive goroutines
|
||||||
|
cpuMultiplier = 4 // Worker multiplier per CPU core for I/O-bound operations
|
||||||
|
maxDirWorkers = 32 // Maximum concurrent subdirectory scans
|
||||||
|
openCommandTimeout = 10 * time.Second // Timeout for open/reveal commands
|
||||||
)
|
)
|
||||||
|
|
||||||
var foldDirs = map[string]bool{
|
var foldDirs = map[string]bool{
|
||||||
|
|||||||
@@ -22,9 +22,21 @@ func deletePathCmd(path string, counter *int64) tea.Cmd {
|
|||||||
|
|
||||||
func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
||||||
var count int64
|
var count int64
|
||||||
|
var firstErr error
|
||||||
|
|
||||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// Skip permission errors but continue walking
|
||||||
|
if os.IsPermission(err) {
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
// For other errors, record and continue
|
||||||
|
if firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +46,9 @@ func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
|||||||
if counter != nil {
|
if counter != nil {
|
||||||
atomic.StoreInt64(counter, count)
|
atomic.StoreInt64(counter, count)
|
||||||
}
|
}
|
||||||
|
} else if firstErr == nil {
|
||||||
|
// Record first deletion error
|
||||||
|
firstErr = removeErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,5 +63,6 @@ func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return count, nil
|
// Return the first error encountered during deletion if any
|
||||||
|
return count, firstErr
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
@@ -448,6 +449,9 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
delete(m.overviewScanningSet, msg.path)
|
delete(m.overviewScanningSet, msg.path)
|
||||||
|
|
||||||
if msg.err == nil {
|
if msg.err == nil {
|
||||||
|
if m.overviewSizeCache == nil {
|
||||||
|
m.overviewSizeCache = make(map[string]int64)
|
||||||
|
}
|
||||||
m.overviewSizeCache[msg.path] = msg.size
|
m.overviewSizeCache[msg.path] = msg.size
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,16 +634,20 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
if m.showLargeFiles {
|
if m.showLargeFiles {
|
||||||
if len(m.largeFiles) > 0 {
|
if len(m.largeFiles) > 0 {
|
||||||
selected := m.largeFiles[m.largeSelected]
|
selected := m.largeFiles[m.largeSelected]
|
||||||
go func() {
|
go func(path string) {
|
||||||
_ = exec.Command("open", selected.path).Run()
|
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout)
|
||||||
}()
|
defer cancel()
|
||||||
|
_ = exec.CommandContext(ctx, "open", path).Run()
|
||||||
|
}(selected.path)
|
||||||
m.status = fmt.Sprintf("Opening %s...", selected.name)
|
m.status = fmt.Sprintf("Opening %s...", selected.name)
|
||||||
}
|
}
|
||||||
} else if len(m.entries) > 0 {
|
} else if len(m.entries) > 0 {
|
||||||
selected := m.entries[m.selected]
|
selected := m.entries[m.selected]
|
||||||
go func() {
|
go func(path string) {
|
||||||
_ = exec.Command("open", selected.path).Run()
|
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout)
|
||||||
}()
|
defer cancel()
|
||||||
|
_ = exec.CommandContext(ctx, "open", path).Run()
|
||||||
|
}(selected.path)
|
||||||
m.status = fmt.Sprintf("Opening %s...", selected.name)
|
m.status = fmt.Sprintf("Opening %s...", selected.name)
|
||||||
}
|
}
|
||||||
case "f", "F":
|
case "f", "F":
|
||||||
@@ -648,14 +656,18 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
if len(m.largeFiles) > 0 {
|
if len(m.largeFiles) > 0 {
|
||||||
selected := m.largeFiles[m.largeSelected]
|
selected := m.largeFiles[m.largeSelected]
|
||||||
go func(path string) {
|
go func(path string) {
|
||||||
_ = exec.Command("open", "-R", path).Run()
|
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout)
|
||||||
|
defer cancel()
|
||||||
|
_ = exec.CommandContext(ctx, "open", "-R", path).Run()
|
||||||
}(selected.path)
|
}(selected.path)
|
||||||
m.status = fmt.Sprintf("Revealing %s in Finder...", selected.name)
|
m.status = fmt.Sprintf("Revealing %s in Finder...", selected.name)
|
||||||
}
|
}
|
||||||
} else if len(m.entries) > 0 {
|
} else if len(m.entries) > 0 {
|
||||||
selected := m.entries[m.selected]
|
selected := m.entries[m.selected]
|
||||||
go func(path string) {
|
go func(path string) {
|
||||||
_ = exec.Command("open", "-R", path).Run()
|
ctx, cancel := context.WithTimeout(context.Background(), openCommandTimeout)
|
||||||
|
defer cancel()
|
||||||
|
_ = exec.CommandContext(ctx, "open", "-R", path).Run()
|
||||||
}(selected.path)
|
}(selected.path)
|
||||||
m.status = fmt.Sprintf("Revealing %s in Finder...", selected.name)
|
m.status = fmt.Sprintf("Revealing %s in Finder...", selected.name)
|
||||||
}
|
}
|
||||||
@@ -934,12 +946,10 @@ func (m model) View() string {
|
|||||||
displayIndex := idx + 1
|
displayIndex := idx + 1
|
||||||
|
|
||||||
// Add unused time label if applicable
|
// Add unused time label if applicable
|
||||||
// For overview mode, get access time on-demand if not set and cache it
|
// For overview mode, get access time on-demand if not set
|
||||||
lastAccess := entry.lastAccess
|
lastAccess := entry.lastAccess
|
||||||
if lastAccess.IsZero() && entry.path != "" {
|
if lastAccess.IsZero() && entry.path != "" {
|
||||||
lastAccess = getLastAccessTime(entry.path)
|
lastAccess = getLastAccessTime(entry.path)
|
||||||
// Cache the result to avoid repeated syscalls
|
|
||||||
m.entries[idx].lastAccess = lastAccess
|
|
||||||
}
|
}
|
||||||
unusedLabel := formatUnusedTime(lastAccess)
|
unusedLabel := formatUnusedTime(lastAccess)
|
||||||
if unusedLabel == "" {
|
if unusedLabel == "" {
|
||||||
|
|||||||
@@ -34,21 +34,20 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
|
|||||||
|
|
||||||
// Use worker pool for concurrent directory scanning
|
// Use worker pool for concurrent directory scanning
|
||||||
// For I/O-bound operations, use more workers than CPU count
|
// For I/O-bound operations, use more workers than CPU count
|
||||||
maxWorkers := runtime.NumCPU() * 4
|
numWorkers := runtime.NumCPU() * cpuMultiplier
|
||||||
if maxWorkers < 16 {
|
if numWorkers < minWorkers {
|
||||||
maxWorkers = 16 // Minimum 16 workers for better I/O throughput
|
numWorkers = minWorkers
|
||||||
}
|
}
|
||||||
// Cap at 128 to avoid excessive goroutines
|
if numWorkers > maxWorkers {
|
||||||
if maxWorkers > 128 {
|
numWorkers = maxWorkers
|
||||||
maxWorkers = 128
|
|
||||||
}
|
}
|
||||||
if maxWorkers > len(children) {
|
if numWorkers > len(children) {
|
||||||
maxWorkers = len(children)
|
numWorkers = len(children)
|
||||||
}
|
}
|
||||||
if maxWorkers < 1 {
|
if numWorkers < 1 {
|
||||||
maxWorkers = 1
|
numWorkers = 1
|
||||||
}
|
}
|
||||||
sem := make(chan struct{}, maxWorkers)
|
sem := make(chan struct{}, numWorkers)
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
// Use channels to collect results without lock contention
|
// Use channels to collect results without lock contention
|
||||||
@@ -91,8 +90,8 @@ func scanPathConcurrent(root string, filesScanned, dirsScanned, bytesScanned *in
|
|||||||
defer func() { <-sem }()
|
defer func() { <-sem }()
|
||||||
|
|
||||||
// Try du command first for folded dirs (much faster)
|
// Try du command first for folded dirs (much faster)
|
||||||
size := calculateDirSizeWithDu(path)
|
size, err := getDirectorySizeFromDu(path)
|
||||||
if size <= 0 {
|
if err != nil || size <= 0 {
|
||||||
// Fallback to walk if du fails
|
// Fallback to walk if du fails
|
||||||
size = calculateDirSizeFast(path, filesScanned, dirsScanned, bytesScanned, currentPath)
|
size = calculateDirSizeFast(path, filesScanned, dirsScanned, bytesScanned, currentPath)
|
||||||
}
|
}
|
||||||
@@ -218,32 +217,6 @@ func shouldFoldDirWithPath(name, path string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculateDirSizeWithDu uses du command for fast directory size calculation
|
|
||||||
// Returns size in bytes, or 0 if command fails
|
|
||||||
func calculateDirSizeWithDu(path string) int64 {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
// Use -sk for 1K-block output, then convert to bytes
|
|
||||||
// macOS du doesn't support -b flag
|
|
||||||
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fields := strings.Fields(string(output))
|
|
||||||
if len(fields) < 1 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
kb, err := strconv.ParseInt(fields[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return kb * 1024
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldSkipFileForLargeTracking(path string) bool {
|
func shouldSkipFileForLargeTracking(path string) bool {
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
@@ -323,7 +296,10 @@ func findLargeFilesWithSpotlight(root string, minSize int64) []fileEntry {
|
|||||||
// mdfind query: files >= minSize in the specified directory
|
// mdfind query: files >= minSize in the specified directory
|
||||||
query := fmt.Sprintf("kMDItemFSSize >= %d", minSize)
|
query := fmt.Sprintf("kMDItemFSSize >= %d", minSize)
|
||||||
|
|
||||||
cmd := exec.Command("mdfind", "-onlyin", root, query)
|
ctx, cancel := context.WithTimeout(context.Background(), mdlsTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, "mdfind", "-onlyin", root, query)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Fallback: mdfind not available or failed
|
// Fallback: mdfind not available or failed
|
||||||
@@ -405,8 +381,8 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, fil
|
|||||||
|
|
||||||
// Limit concurrent subdirectory scans to avoid too many goroutines
|
// Limit concurrent subdirectory scans to avoid too many goroutines
|
||||||
maxConcurrent := runtime.NumCPU() * 2
|
maxConcurrent := runtime.NumCPU() * 2
|
||||||
if maxConcurrent > 32 {
|
if maxConcurrent > maxDirWorkers {
|
||||||
maxConcurrent = 32
|
maxConcurrent = maxDirWorkers
|
||||||
}
|
}
|
||||||
sem := make(chan struct{}, maxConcurrent)
|
sem := make(chan struct{}, maxConcurrent)
|
||||||
|
|
||||||
@@ -420,8 +396,8 @@ func calculateDirSizeConcurrent(root string, largeFileChan chan<- fileEntry, fil
|
|||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(path string) {
|
go func(path string) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
size := calculateDirSizeWithDu(path)
|
size, err := getDirectorySizeFromDu(path)
|
||||||
if size > 0 {
|
if err == nil && size > 0 {
|
||||||
atomic.AddInt64(&total, size)
|
atomic.AddInt64(&total, size)
|
||||||
atomic.AddInt64(bytesScanned, size)
|
atomic.AddInt64(bytesScanned, size)
|
||||||
atomic.AddInt64(dirsScanned, 1)
|
atomic.AddInt64(dirsScanned, 1)
|
||||||
@@ -507,45 +483,6 @@ func measureOverviewSize(path string) (int64, error) {
|
|||||||
return 0, fmt.Errorf("unable to measure directory size with fast methods")
|
return 0, fmt.Errorf("unable to measure directory size with fast methods")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDirectorySizeFromMetadata(path string) (int64, error) {
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("cannot stat path: %v", err)
|
|
||||||
}
|
|
||||||
if !info.IsDir() {
|
|
||||||
return 0, fmt.Errorf("not a directory")
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), mdlsTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "mdls", "-raw", "-name", "kMDItemFSSize", path)
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
if ctx.Err() == context.DeadlineExceeded {
|
|
||||||
return 0, fmt.Errorf("mdls timeout after %v", mdlsTimeout)
|
|
||||||
}
|
|
||||||
if stderr.Len() > 0 {
|
|
||||||
return 0, fmt.Errorf("mdls failed: %v (%s)", err, stderr.String())
|
|
||||||
}
|
|
||||||
return 0, fmt.Errorf("mdls failed: %v", err)
|
|
||||||
}
|
|
||||||
value := strings.TrimSpace(stdout.String())
|
|
||||||
if value == "" || value == "(null)" {
|
|
||||||
return 0, fmt.Errorf("metadata size unavailable")
|
|
||||||
}
|
|
||||||
size, err := strconv.ParseInt(value, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to parse mdls output: %v", err)
|
|
||||||
}
|
|
||||||
if size <= 0 {
|
|
||||||
return 0, fmt.Errorf("mdls size invalid: %d", size)
|
|
||||||
}
|
|
||||||
return size, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDirectorySizeFromDu(path string) (int64, error) {
|
func getDirectorySizeFromDu(path string) (int64, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), duTimeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user