mirror of
https://github.com/tw93/Mole.git
synced 2026-02-16 17:00:17 +00:00
npm/venv scan speed improvement
This commit is contained in:
@@ -308,6 +308,12 @@ type overviewSizeMsg struct {
|
|||||||
|
|
||||||
type tickMsg time.Time
|
type tickMsg time.Time
|
||||||
|
|
||||||
|
type deleteProgressMsg struct {
|
||||||
|
done bool
|
||||||
|
err error
|
||||||
|
count int64
|
||||||
|
}
|
||||||
|
|
||||||
type model struct {
|
type model struct {
|
||||||
path string
|
path string
|
||||||
history []historyEntry
|
history []historyEntry
|
||||||
@@ -327,6 +333,8 @@ type model struct {
|
|||||||
isOverview bool
|
isOverview bool
|
||||||
deleteConfirm bool
|
deleteConfirm bool
|
||||||
deleteTarget *dirEntry
|
deleteTarget *dirEntry
|
||||||
|
deleting bool
|
||||||
|
deleteCount *int64
|
||||||
cache map[string]historyEntry
|
cache map[string]historyEntry
|
||||||
largeSelected int
|
largeSelected int
|
||||||
largeOffset int
|
largeOffset int
|
||||||
@@ -602,6 +610,28 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.KeyMsg:
|
case tea.KeyMsg:
|
||||||
return m.updateKey(msg)
|
return m.updateKey(msg)
|
||||||
|
case deleteProgressMsg:
|
||||||
|
if msg.done {
|
||||||
|
m.deleting = false
|
||||||
|
if msg.err != nil {
|
||||||
|
m.status = fmt.Sprintf("Failed to delete: %v", msg.err)
|
||||||
|
} else {
|
||||||
|
m.status = fmt.Sprintf("Deleted %d items", msg.count)
|
||||||
|
// Mark all caches as dirty
|
||||||
|
for i := range m.history {
|
||||||
|
m.history[i].dirty = true
|
||||||
|
}
|
||||||
|
for path := range m.cache {
|
||||||
|
entry := m.cache[path]
|
||||||
|
entry.dirty = true
|
||||||
|
m.cache[path] = entry
|
||||||
|
}
|
||||||
|
// Refresh the view
|
||||||
|
m.scanning = true
|
||||||
|
return m, tea.Batch(m.scanCmd(m.path), tickCmd())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
case scanResultMsg:
|
case scanResultMsg:
|
||||||
m.scanning = false
|
m.scanning = false
|
||||||
if msg.err != nil {
|
if msg.err != nil {
|
||||||
@@ -658,8 +688,15 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
case tickMsg:
|
case tickMsg:
|
||||||
if m.scanning || (m.isOverview && m.overviewScanning) {
|
if m.scanning || m.deleting || (m.isOverview && m.overviewScanning) {
|
||||||
m.spinner = (m.spinner + 1) % len(spinnerFrames)
|
m.spinner = (m.spinner + 1) % len(spinnerFrames)
|
||||||
|
// Update delete progress status
|
||||||
|
if m.deleting && m.deleteCount != nil {
|
||||||
|
count := atomic.LoadInt64(m.deleteCount)
|
||||||
|
if count > 0 {
|
||||||
|
m.status = fmt.Sprintf("Deleting... %s items removed", formatNumber(count))
|
||||||
|
}
|
||||||
|
}
|
||||||
return m, tickCmd()
|
return m, tickCmd()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -672,27 +709,17 @@ func (m model) updateKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
// Handle delete confirmation
|
// Handle delete confirmation
|
||||||
if m.deleteConfirm {
|
if m.deleteConfirm {
|
||||||
if msg.String() == "delete" || msg.String() == "backspace" {
|
if msg.String() == "delete" || msg.String() == "backspace" {
|
||||||
// Confirm delete
|
// Confirm delete - start async deletion
|
||||||
if m.deleteTarget != nil {
|
if m.deleteTarget != nil {
|
||||||
err := os.RemoveAll(m.deleteTarget.path)
|
m.deleteConfirm = false
|
||||||
if err != nil {
|
m.deleting = true
|
||||||
m.status = fmt.Sprintf("Failed to delete: %v", err)
|
var deleteCount int64
|
||||||
} else {
|
m.deleteCount = &deleteCount
|
||||||
m.status = fmt.Sprintf("Deleted %s", m.deleteTarget.name)
|
targetPath := m.deleteTarget.path
|
||||||
for i := range m.history {
|
targetName := m.deleteTarget.name
|
||||||
m.history[i].dirty = true
|
m.deleteTarget = nil
|
||||||
}
|
m.status = fmt.Sprintf("Deleting %s...", targetName)
|
||||||
for path := range m.cache {
|
return m, tea.Batch(deletePathCmd(targetPath, m.deleteCount), tickCmd())
|
||||||
entry := m.cache[path]
|
|
||||||
entry.dirty = true
|
|
||||||
m.cache[path] = entry
|
|
||||||
}
|
|
||||||
// Refresh the view
|
|
||||||
m.scanning = true
|
|
||||||
m.deleteConfirm = false
|
|
||||||
m.deleteTarget = nil
|
|
||||||
return m, tea.Batch(m.scanCmd(m.path), tickCmd())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m.deleteConfirm = false
|
m.deleteConfirm = false
|
||||||
m.deleteTarget = nil
|
m.deleteTarget = nil
|
||||||
@@ -967,6 +994,22 @@ func (m model) View() string {
|
|||||||
fmt.Fprintln(&b)
|
fmt.Fprintln(&b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.deleting {
|
||||||
|
// Show delete progress
|
||||||
|
count := int64(0)
|
||||||
|
if m.deleteCount != nil {
|
||||||
|
count = atomic.LoadInt64(m.deleteCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(&b, "\n%s%s%s%s Deleting: %s%s items%s removed, please wait...\n",
|
||||||
|
colorCyan, colorBold,
|
||||||
|
spinnerFrames[m.spinner],
|
||||||
|
colorReset,
|
||||||
|
colorYellow, formatNumber(count), colorReset)
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
if m.scanning {
|
if m.scanning {
|
||||||
filesScanned, dirsScanned, bytesScanned := m.getScanProgress()
|
filesScanned, dirsScanned, bytesScanned := m.getScanProgress()
|
||||||
|
|
||||||
@@ -1347,9 +1390,19 @@ func shouldFoldDirWithPath(name, path string) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special case: .npm directory - fold all single-letter subdirectories (npm cache structure)
|
// Special case: .npm directory - fold all subdirectories under cache folders
|
||||||
if strings.Contains(path, "/.npm/") && len(name) == 1 {
|
// This includes: .npm/_quick/*, .npm/_cacache/*, .npm/*/
|
||||||
return true
|
if strings.Contains(path, "/.npm/") {
|
||||||
|
// Get the parent directory name
|
||||||
|
parent := filepath.Base(filepath.Dir(path))
|
||||||
|
// If parent is a cache folder (_quick, _cacache, etc) or .npm itself, fold it
|
||||||
|
if parent == ".npm" || strings.HasPrefix(parent, "_") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Also fold single-letter subdirectories (npm cache structure)
|
||||||
|
if len(name) == 1 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -1361,9 +1414,9 @@ func calculateDirSizeWithDu(path string) int64 {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// Use -sb for exact byte count (matches info.Size() behavior)
|
// Use -sk for 1K-block output, then convert to bytes
|
||||||
// -s: summarize (don't show subdirs), -b: bytes (not blocks)
|
// macOS du doesn't support -b flag
|
||||||
cmd := exec.CommandContext(ctx, "du", "-sb", path)
|
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
@@ -1374,12 +1427,12 @@ func calculateDirSizeWithDu(path string) int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := strconv.ParseInt(fields[0], 10, 64)
|
kb, err := strconv.ParseInt(fields[0], 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return bytes
|
return kb * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldSkipFileForLargeTracking(path string) bool {
|
func shouldSkipFileForLargeTracking(path string) bool {
|
||||||
@@ -2073,12 +2126,60 @@ func scanOverviewPathCmd(path string, index int) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// deletePathCmd deletes a path recursively with progress tracking
|
||||||
|
func deletePathCmd(path string, counter *int64) tea.Cmd {
|
||||||
|
return func() tea.Msg {
|
||||||
|
count, err := deletePathWithProgress(path, counter)
|
||||||
|
return deleteProgressMsg{
|
||||||
|
done: true,
|
||||||
|
err: err,
|
||||||
|
count: count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// deletePathWithProgress recursively deletes a path and tracks progress
|
||||||
|
func deletePathWithProgress(root string, counter *int64) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
// Walk the directory tree and delete files
|
||||||
|
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
// If we can't read a path, skip it but continue
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't delete directories yet, just count and delete files
|
||||||
|
if !d.IsDir() {
|
||||||
|
if removeErr := os.Remove(path); removeErr == nil {
|
||||||
|
count++
|
||||||
|
if counter != nil {
|
||||||
|
atomic.StoreInt64(counter, count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now remove all empty directories using RemoveAll
|
||||||
|
// This is safe because we've already deleted all files
|
||||||
|
if err := os.RemoveAll(root); err != nil {
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
// measureOverviewSize calculates the size of a directory using multiple strategies:
|
// measureOverviewSize calculates the size of a directory using multiple strategies:
|
||||||
// 1. Check JSON cache (fast)
|
// 1. Check JSON cache (fast)
|
||||||
// 2. Try mdls metadata (fast, macOS only)
|
// 2. Try du command (fast and accurate)
|
||||||
// 3. Walk the directory to get logical size (matches detailed scans)
|
// 3. Walk the directory to get logical size (accurate but slower)
|
||||||
// 4. Try du command (moderate speed, physical size)
|
// 4. Check gob cache (fallback)
|
||||||
// 5. Check gob cache (fallback)
|
|
||||||
func measureOverviewSize(path string) (int64, error) {
|
func measureOverviewSize(path string) (int64, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return 0, fmt.Errorf("empty path")
|
return 0, fmt.Errorf("empty path")
|
||||||
@@ -2099,25 +2200,19 @@ func measureOverviewSize(path string) (int64, error) {
|
|||||||
return cached, nil
|
return cached, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 2: Try mdls (fastest, macOS only)
|
// Strategy 2: Try du command first (fast and accurate with -s flag)
|
||||||
if metadataSize, err := getDirectorySizeFromMetadata(path); err == nil && metadataSize > 0 {
|
|
||||||
_ = storeOverviewSize(path, metadataSize)
|
|
||||||
return metadataSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 3: Fall back to a quick logical size walk so the result matches detailed scans.
|
|
||||||
if logicalSize, err := getDirectoryLogicalSize(path); err == nil && logicalSize > 0 {
|
|
||||||
_ = storeOverviewSize(path, logicalSize)
|
|
||||||
return logicalSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 4: Try du command (fast and reliable for physical size)
|
|
||||||
if duSize, err := getDirectorySizeFromDu(path); err == nil && duSize > 0 {
|
if duSize, err := getDirectorySizeFromDu(path); err == nil && duSize > 0 {
|
||||||
_ = storeOverviewSize(path, duSize)
|
_ = storeOverviewSize(path, duSize)
|
||||||
return duSize, nil
|
return duSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strategy 5: Check gob cache as fallback
|
// Strategy 3: Fall back to logical size walk (accurate but slower)
|
||||||
|
if logicalSize, err := getDirectoryLogicalSize(path); err == nil && logicalSize > 0 {
|
||||||
|
_ = storeOverviewSize(path, logicalSize)
|
||||||
|
return logicalSize, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 4: Check gob cache as fallback
|
||||||
if cached, err := loadCacheFromDisk(path); err == nil {
|
if cached, err := loadCacheFromDisk(path); err == nil {
|
||||||
_ = storeOverviewSize(path, cached.TotalSize)
|
_ = storeOverviewSize(path, cached.TotalSize)
|
||||||
return cached.TotalSize, nil
|
return cached.TotalSize, nil
|
||||||
@@ -2171,12 +2266,14 @@ func getDirectorySizeFromMetadata(path string) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getDirectorySizeFromDu calculates directory size using the du command.
|
// getDirectorySizeFromDu calculates directory size using the du command.
|
||||||
// Uses -d 0 to avoid recursing into subdirectories, and includes a timeout protection.
|
// Uses -s to summarize total size including all subdirectories.
|
||||||
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)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, "du", "-sk", "-d", "0", path)
|
// Use -sk for 1K-block size output, -s for summary
|
||||||
|
// Note: -k and -s are separate flags (not -sk -s)
|
||||||
|
cmd := exec.CommandContext(ctx, "du", "-sk", path)
|
||||||
var stdout, stderr bytes.Buffer
|
var stdout, stderr bytes.Buffer
|
||||||
cmd.Stdout = &stdout
|
cmd.Stdout = &stdout
|
||||||
cmd.Stderr = &stderr
|
cmd.Stderr = &stderr
|
||||||
|
|||||||
Reference in New Issue
Block a user