Add performance measurements, trash test and documentation
This commit is contained in:
+24
-5
@@ -124,6 +124,14 @@ releases the lock between batches). This is the only lock in the data path.
|
|||||||
3. Nodes are never deleted; a `NodeID` once observed stays valid.
|
3. Nodes are never deleted; a `NodeID` once observed stays valid.
|
||||||
4. Workers share nothing mutable except the bounded channels and atomic
|
4. Workers share nothing mutable except the bounded channels and atomic
|
||||||
progress counters.
|
progress counters.
|
||||||
|
5. No goroutine ever takes `RLock` while already holding it. Go's
|
||||||
|
`RWMutex` blocks new readers once a writer waits, so a recursive read
|
||||||
|
lock can deadlock against the scan controller. The TUI takes the lock
|
||||||
|
once per entry point (`View`, `handleKey`, mouse handlers); helpers they
|
||||||
|
call assume it is held. Background commands (filter, duplicates,
|
||||||
|
snapshot save) take their own lock on their own goroutine.
|
||||||
|
6. After a scan finishes, the UI goroutine may become the writer (move to
|
||||||
|
trash updates aggregates under `Lock`). Trash is refused while scanning.
|
||||||
|
|
||||||
## Hard links and sizes
|
## Hard links and sizes
|
||||||
|
|
||||||
@@ -170,15 +178,25 @@ all except for a single `scanDoneMsg`. The UI polls at a fixed rate.
|
|||||||
`View()` is called by Bubble Tea after *every* `Update`. The model therefore
|
`View()` is called by Bubble Tea after *every* `Update`. The model therefore
|
||||||
caches the rendered frame and only repaints when something visible changed.
|
caches the rendered frame and only repaints when something visible changed.
|
||||||
|
|
||||||
|
Expensive per-directory work (sorting children by size) is cached per data
|
||||||
|
version. During a scan the version advances at most every 400 ms, so the
|
||||||
|
layout does not jitter at frame rate even though labels and counters update
|
||||||
|
at the 10 Hz refresh.
|
||||||
|
|
||||||
## Rendering: the canvas
|
## Rendering: the canvas
|
||||||
|
|
||||||
All drawing goes to an in-memory `Canvas` of cells (grapheme, fg, bg, attrs).
|
All drawing goes to an in-memory `Canvas` of cells (grapheme, fg, bg, attrs).
|
||||||
Views and modals paint into the canvas; the canvas is serialised to a string
|
Views and modals paint into the canvas; the canvas is serialised to a string
|
||||||
with minimal SGR changes. Bubble Tea's renderer diffs frames and downsamples
|
with minimal SGR changes. Bubble Tea's renderer diffs frames and downsamples
|
||||||
true-colour to the detected profile (256 / 16 / none), so the drawing code
|
true-colour to the detected profile (256 / 16 / none), so the drawing code
|
||||||
uses RGB throughout. Selection never relies on colour alone: the selected
|
uses RGB throughout. For 256-colour terminals the canvas quantizes colours
|
||||||
rectangle uses a double-line border, bold reversed title and a status-line
|
itself (redmean distance over the 6×6×6 cube and grey ramp, never the
|
||||||
description.
|
palette-dependent first 16 entries): the stock conversion maps dark tints to
|
||||||
|
saturated olive/navy and destroys the shading. `--color` overrides
|
||||||
|
detection (useful over SSH, where `COLORTERM` is often not forwarded).
|
||||||
|
`NO_COLOR` selects the `mono` theme, which relies on attributes and glyphs
|
||||||
|
only. Selection never relies on colour alone: the selected rectangle uses a
|
||||||
|
double-line border, bold reversed title and a status-line description.
|
||||||
|
|
||||||
## Treemap algorithm
|
## Treemap algorithm
|
||||||
|
|
||||||
@@ -230,8 +248,9 @@ when content does not fit.
|
|||||||
|
|
||||||
## Selection persistence
|
## Selection persistence
|
||||||
|
|
||||||
Selection is a `NodeID` (plus, for the synthetic LOD group, the group's parent
|
Selection is a `NodeID` (or, for the synthetic LOD group, `-1 - parentID`).
|
||||||
ID and a group marker). After any relayout the renderer looks the ID up in the
|
Until the user moves the selection it follows the largest block, which
|
||||||
|
changes while a scan runs; after that it is sticky. After any relayout the renderer looks the ID up in the
|
||||||
new rectangle list. If it is absent (filtered out, collapsed into a group,
|
new rectangle list. If it is absent (filtered out, collapsed into a group,
|
||||||
zoom changed) selection falls back to: the group containing it, then its
|
zoom changed) selection falls back to: the group containing it, then its
|
||||||
nearest visible ancestor, then the largest block.
|
nearest visible ancestor, then the largest block.
|
||||||
|
|||||||
@@ -6,3 +6,13 @@
|
|||||||
- Non-interactive reports: `--top`, `--largest-files`, `--json`, `--csv`,
|
- Non-interactive reports: `--top`, `--largest-files`, `--json`, `--csv`,
|
||||||
`--duplicates`, `--no-ui`.
|
`--duplicates`, `--no-ui`.
|
||||||
- Versioned, checksummed snapshot format; `--compare`.
|
- Versioned, checksummed snapshot format; `--compare`.
|
||||||
|
- Interactive TUI (Bubble Tea v2): live squarified treemap with nested
|
||||||
|
previews and level-of-detail grouping, spatial arrow navigation, zoom,
|
||||||
|
breadcrumbs, responsive layouts, info/help/confirm/save modals, mouse.
|
||||||
|
- Views: directory list, file types, top lists, scan info & errors,
|
||||||
|
duplicates, snapshot changes.
|
||||||
|
- Search/filter language with live treemap filtering.
|
||||||
|
- Themes (default, dark, high-contrast, mono), `--color` override,
|
||||||
|
perceptual 256-colour quantizer, NO_COLOR support.
|
||||||
|
- Move to trash (freedesktop / macOS / Windows), `--read-only`.
|
||||||
|
- `treegen` synthetic tree generator; benchmarks in PERFORMANCE.md.
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Performance
|
||||||
|
|
||||||
|
All numbers below were measured, not estimated. Re-run them on your own
|
||||||
|
hardware before relying on them; filesystem caches dominate scan timings.
|
||||||
|
|
||||||
|
## Test machine (2026-10-02)
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| CPU | Intel Core i7-7700 @ 3.60 GHz, 4 cores / 8 threads |
|
||||||
|
| RAM | 32 GiB |
|
||||||
|
| OS | Linux 7.0, Go 1.27.1 |
|
||||||
|
| Filesystem | ext4 on Linux software RAID (`/dev/md2`) over two 7200 rpm SATA HDDs |
|
||||||
|
|
||||||
|
## Scanner
|
||||||
|
|
||||||
|
Dataset: `~/go/pkg/mod` — 388,307 files + 65,518 directories (458,510
|
||||||
|
inventory nodes), 10.5 GiB allocated. **Warm cache only**: dropping the page
|
||||||
|
cache needs root, which was not available, so these measure CPU and syscall
|
||||||
|
cost, not disk seeks. Cold-cache behaviour on HDDs will be dominated by seek
|
||||||
|
time; expect fewer workers to be optimal there.
|
||||||
|
|
||||||
|
`mapsize --no-ui --workers N ~/go/pkg/mod`, wall time, 3 runs each
|
||||||
|
(`/usr/bin/time`):
|
||||||
|
|
||||||
|
| workers | run 1 | run 2 | run 3 | entries/s (best) | peak RSS |
|
||||||
|
|---:|---:|---:|---:|---:|---:|
|
||||||
|
| 1 | 1.77 s | 1.67 s | 1.65 s | ~278,000 | 117 MB |
|
||||||
|
| 2 | 1.06 s | 0.99 s | 1.00 s | ~463,000 | 120 MB |
|
||||||
|
| 4 | 0.59 s | 0.59 s | 0.62 s | ~777,000 | 117 MB |
|
||||||
|
| 8 (balanced default) | 0.48 s | 0.50 s | 0.51 s | ~955,000 | 121 MB |
|
||||||
|
| 16 | 0.45 s | 0.44 s | 0.43 s | ~1,066,000 | 121 MB |
|
||||||
|
| 32 | 0.43 s | 0.44 s | 0.42 s | ~1,092,000 | 122 MB |
|
||||||
|
|
||||||
|
For comparison, `du -s` on the same tree (warm): 1.59 s, 34 MB RSS.
|
||||||
|
|
||||||
|
CPU profile at 8 workers: ~60 % of samples in `statx`/`getdents` syscalls;
|
||||||
|
path reconstruction for job dispatch ~7 %; aggregation is negligible.
|
||||||
|
|
||||||
|
### Memory
|
||||||
|
|
||||||
|
`go test ./internal/scan -run TestMemoryPerNode` with
|
||||||
|
`MAPSIZE_MEM_ROOT=~/go/pkg/mod`: 458,510 nodes, 69.1 MiB live heap,
|
||||||
|
**~158 bytes per node** (node record ~112 bytes plus name storage and
|
||||||
|
directory side tables). Extrapolated, 10 M entries need ~1.6 GiB.
|
||||||
|
|
||||||
|
## UI
|
||||||
|
|
||||||
|
`go test ./internal/tui -bench Big` on a synthetic in-memory tree of
|
||||||
|
1,008,201 nodes (200 directories × 40 subdirectories × 100 files, plus one
|
||||||
|
flat directory with 200,000 files), 200×60 terminal:
|
||||||
|
|
||||||
|
| operation | time per op |
|
||||||
|
|---|---:|
|
||||||
|
| cold frame at root (sort children, LOD, layout incl. nested previews, paint) | 33.8 ms |
|
||||||
|
| resize to a new size (layout + paint, child lists cached) | 2.0 ms |
|
||||||
|
| arrow-key move + re-render | 1.5 ms |
|
||||||
|
| zoomed into the 200,000-entry directory, cold | 29.5 ms |
|
||||||
|
| render of a small tree (2.4 K nodes) at 200×60 | 0.66 ms |
|
||||||
|
| resize relayout of the small tree | 0.45 ms |
|
||||||
|
|
||||||
|
During a scan the size caches are invalidated at most every 400 ms, so a
|
||||||
|
cold frame (worst case above ~34 ms) costs under 10 % of one core; other
|
||||||
|
frames reuse caches. Resize events only record the new size; layout happens
|
||||||
|
once per rendered frame, so a resize storm cannot queue up work.
|
||||||
|
|
||||||
|
### Geometry
|
||||||
|
|
||||||
|
`go test ./internal/treemap -bench .`:
|
||||||
|
|
||||||
|
| operation | time |
|
||||||
|
|---|---:|
|
||||||
|
| squarified layout, 300 items | 89 µs |
|
||||||
|
| spatial neighbour lookup among 300 blocks | 2.1 µs |
|
||||||
|
|
||||||
|
### Filtering (1,008,201 nodes)
|
||||||
|
|
||||||
|
| query | time |
|
||||||
|
|---|---:|
|
||||||
|
| `size > 1MB` | 31 ms |
|
||||||
|
| `*.dat AND size > 500k` | 119 ms |
|
||||||
|
| `path contains sub3` (reconstructs every path) | 244 ms |
|
||||||
|
|
||||||
|
Filters run on a background goroutine with cancellation and a 120 ms
|
||||||
|
keystroke debounce; the UI never waits for them.
|
||||||
|
|
||||||
|
## Known costs / future work
|
||||||
|
|
||||||
|
* Path predicates rebuild each node's path; an incremental evaluation along
|
||||||
|
the tree would make them ~10× cheaper.
|
||||||
|
* Node records could shrink (owner/group/mode/link count are only needed for
|
||||||
|
display and some filters) if memory on 10 M+ entry trees matters.
|
||||||
|
* Cold-cache HDD benchmarks still need to be run with root access.
|
||||||
@@ -8,7 +8,63 @@ runs over SSH.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
Under active development — see [ROADMAP.md](ROADMAP.md) for what works today.
|
Milestones 0.1–0.8 are implemented; 0.9 (hardening) is in progress. See
|
||||||
|
[ROADMAP.md](ROADMAP.md) for detail and [PERFORMANCE.md](PERFORMANCE.md) for
|
||||||
|
measured numbers.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
* **Live treemap.** A squarified, space-filling treemap fills the terminal
|
||||||
|
and grows while the scan runs. Directories show nested previews of their
|
||||||
|
contents; colours are file categories (video, archives, disk images, code,
|
||||||
|
…). Items too small to draw collapse into a "N smaller items" block until
|
||||||
|
you zoom in.
|
||||||
|
* **Spatial navigation.** Arrow keys move to the block that is actually to
|
||||||
|
the left/right/above/below. Enter inspects, Space zooms in, Backspace zooms
|
||||||
|
out, Home returns to the root. The selection survives rescans, resizes and
|
||||||
|
filtering.
|
||||||
|
* **Resizes properly.** Every resize recomputes the layout for the new
|
||||||
|
geometry (it never stretches the old one); large terminals get a side
|
||||||
|
panel, small ones a compact layout, tiny ones a polite message.
|
||||||
|
* **Search and filter** with a small query language — the treemap shows only
|
||||||
|
matching data:
|
||||||
|
`ubuntu` · `*.iso` · `size > 5GB` · `ext IN (iso,qcow2,vmdk)` ·
|
||||||
|
`age > 365d` · `path contains cache AND NOT type = dir` · `owner = andy`
|
||||||
|
* **Investigation views** (Tab): directory list, file types, top lists
|
||||||
|
(largest files/dirs, oldest/newest large files, most entries, sparse and
|
||||||
|
hard-linked files), scan information with every error, duplicates.
|
||||||
|
* **Honest numbers.** Allocated vs logical size, hard links counted once,
|
||||||
|
sparse files flagged, permission errors and skipped areas always shown;
|
||||||
|
incomplete totals are labelled as such.
|
||||||
|
* **Snapshots**: save a scan, reopen it instantly, compare two — the treemap
|
||||||
|
colours growth and shrinkage.
|
||||||
|
* **Safe with hostile filenames**: control characters and escape sequences
|
||||||
|
in names are displayed escaped and never reach the terminal.
|
||||||
|
|
||||||
|
## Keys
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|---|---|
|
||||||
|
| ↑ ↓ ← → / hjkl | move spatially through the map |
|
||||||
|
| Enter | item information |
|
||||||
|
| Space / → | zoom into directory |
|
||||||
|
| Backspace / ← | zoom out |
|
||||||
|
| Home | scan root |
|
||||||
|
| / | search / filter (Esc clears) |
|
||||||
|
| Tab, Shift+Tab, 1–7 | switch views |
|
||||||
|
| x, g, e | file types, top lists, scan info & errors |
|
||||||
|
| D | find duplicates |
|
||||||
|
| a | toggle allocated / logical size |
|
||||||
|
| c, o | copy path, reveal in file manager |
|
||||||
|
| d | move to trash (asks; disabled with `--read-only`) |
|
||||||
|
| s, r | save snapshot, rescan |
|
||||||
|
| T | cycle theme |
|
||||||
|
| ? | help |
|
||||||
|
| Ctrl+C | cancel scan, then quit |
|
||||||
|
| q | quit |
|
||||||
|
|
||||||
|
Mouse: click selects, double-click zooms, right-click inspects, wheel zooms
|
||||||
|
the map or scrolls lists. Everything works without a mouse (`--no-mouse`).
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -72,13 +128,30 @@ Optional JSON file at `$XDG_CONFIG_HOME/mapsize/config.json`
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Colour
|
||||||
|
|
||||||
|
Colour support is auto-detected; `--color truecolor|256|16|none` overrides
|
||||||
|
it (handy over SSH, where `COLORTERM` is often not forwarded). Themes:
|
||||||
|
`default`, `dark`, `high-contrast`, `mono` (`--theme`, or `T` to cycle).
|
||||||
|
`NO_COLOR` is respected. On the Linux console, or with `MAPSIZE_ASCII=1`,
|
||||||
|
box drawing falls back to ASCII.
|
||||||
|
|
||||||
### Read-only mode
|
### Read-only mode
|
||||||
|
|
||||||
`--read-only` disables every modifying action and shows `READ ONLY` in the
|
`--read-only` disables every modifying action and shows `READ ONLY` in the
|
||||||
status bar. It does not make mapsize a forensic acquisition tool: scanning
|
header. (Saving a snapshot file is still allowed; it writes only where you
|
||||||
|
tell it to.) It does not make mapsize a forensic acquisition tool: scanning
|
||||||
uses ordinary filesystem APIs, and duplicate detection reads file content
|
uses ordinary filesystem APIs, and duplicate detection reads file content
|
||||||
(which may update access times).
|
(which may update access times).
|
||||||
|
|
||||||
|
### Deletion
|
||||||
|
|
||||||
|
`d` moves the selected item to the system trash (freedesktop.org trash on
|
||||||
|
Linux, `~/.Trash` on macOS, the Recycle Bin on Windows) after confirmation,
|
||||||
|
using native file operations — never a shell. Permanent deletion is not
|
||||||
|
offered. Trash is unavailable while scanning, for snapshots, and in
|
||||||
|
read-only mode.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
* [ARCHITECTURE.md](ARCHITECTURE.md) — design and invariants
|
* [ARCHITECTURE.md](ARCHITECTURE.md) — design and invariants
|
||||||
|
|||||||
+23
-20
@@ -3,45 +3,48 @@
|
|||||||
Status markers: `[x]` done, `[~]` partial, `[ ]` not started.
|
Status markers: `[x]` done, `[~]` partial, `[ ]` not started.
|
||||||
|
|
||||||
## 0.1 — inventory engine
|
## 0.1 — inventory engine
|
||||||
- [ ] Go project skeleton, Makefile, CI
|
- [x] Go project skeleton, Makefile, CI
|
||||||
- [ ] portable filesystem scanner, bounded worker pool, no-deadlock scheduler
|
- [x] portable filesystem scanner, bounded worker pool, no-deadlock scheduler
|
||||||
- [ ] inventory node model, incremental aggregation
|
- [x] inventory node model, incremental aggregation
|
||||||
- [ ] cancellation, progress counters, error accounting
|
- [x] cancellation, progress counters, error accounting
|
||||||
- [ ] CLI: `--top`, `--json`, `--csv`, `--no-ui`
|
- [x] CLI: `--top`, `--json`, `--csv`, `--no-ui`
|
||||||
|
|
||||||
## 0.2 — TUI shell
|
## 0.2 — TUI shell
|
||||||
- [ ] full-screen Bubble Tea v2 app, canvas renderer, frame cache
|
- [x] full-screen Bubble Tea v2 app, canvas renderer, frame cache
|
||||||
- [ ] responsive layout (large / medium / small / too-small)
|
- [x] responsive layout (large / medium / small / too-small)
|
||||||
- [ ] resize handling, status bar, progressive display, directory list
|
- [x] resize handling, status bar, progressive display, directory list
|
||||||
|
|
||||||
## 0.3 — treemap
|
## 0.3 — treemap
|
||||||
- [ ] squarified layout, edge-snapped cell rectangles
|
- [x] squarified layout, edge-snapped cell rectangles
|
||||||
- [ ] renderer with borders, nested preview, adaptive labels
|
- [x] renderer with borders, nested preview, adaptive labels
|
||||||
- [ ] level-of-detail grouping, category colouring
|
- [x] level-of-detail grouping, category colouring
|
||||||
|
|
||||||
## 0.4 — interaction
|
## 0.4 — interaction
|
||||||
- [ ] spatial arrow navigation, stable NodeID selection
|
- [x] spatial arrow navigation, stable NodeID selection
|
||||||
- [ ] info modal (scrollable, resize-aware), zoom, breadcrumbs, mouse
|
- [x] info modal (scrollable, resize-aware), zoom, breadcrumbs, mouse
|
||||||
|
|
||||||
## 0.5 — investigation
|
## 0.5 — investigation
|
||||||
- [ ] search & filter language (AND/OR/NOT, parentheses, units, ages)
|
- [x] search & filter language (AND/OR/NOT, parentheses, units, ages)
|
||||||
- [ ] extension statistics, largest files / dirs, error view
|
- [x] extension statistics, largest files / dirs, error view
|
||||||
|
|
||||||
## 0.6 — snapshots
|
## 0.6 — snapshots
|
||||||
- [ ] versioned binary snapshot format, save/load, comparison, growth treemap
|
- [x] versioned binary snapshot format, save/load, comparison, growth treemap
|
||||||
|
|
||||||
## 0.7 — duplicates
|
## 0.7 — duplicates
|
||||||
- [ ] staged duplicate detection (size → sampled hash → SHA-256), view
|
- [x] staged duplicate detection (size → sampled hash → SHA-256), view
|
||||||
|
|
||||||
## 0.8 — operations
|
## 0.8 — operations
|
||||||
- [ ] read-only mode, JSON/CSV export
|
- [x] read-only mode, JSON/CSV export
|
||||||
- [ ] move-to-trash (freedesktop, macOS ~/.Trash, Windows recycle bin)
|
- [x] move-to-trash: freedesktop trash (tested on Linux); macOS ~/.Trash and Windows Recycle Bin implemented but not yet tested on those systems
|
||||||
|
|
||||||
## 0.9 — hardening (ongoing)
|
## 0.9 — hardening (ongoing)
|
||||||
- [ ] profiling & benchmarks (see PERFORMANCE.md)
|
- [~] profiling & benchmarks (warm-cache only so far) (see PERFORMANCE.md)
|
||||||
- [ ] memory reduction (name arena) if profiles justify it
|
- [ ] memory reduction (name arena) if profiles justify it
|
||||||
- [ ] Windows allocated-size via GetCompressedFileSizeW (opt-in)
|
- [ ] Windows allocated-size via GetCompressedFileSizeW (opt-in)
|
||||||
- [ ] configurable keybindings
|
- [ ] configurable keybindings
|
||||||
|
- [ ] cold-cache HDD scan benchmarks (need root to drop caches)
|
||||||
|
- [ ] incremental path-predicate evaluation for filters
|
||||||
|
- [ ] permanent deletion (deliberately not offered yet; trash only)
|
||||||
|
|
||||||
## 1.0
|
## 1.0
|
||||||
- [ ] stable snapshot format v1 guarantee, packaged releases
|
- [ ] stable snapshot format v1 guarantee, packaged releases
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package platform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTrashFreedesktop(t *testing.T) {
|
||||||
|
data := t.TempDir()
|
||||||
|
t.Setenv("XDG_DATA_HOME", data)
|
||||||
|
dir := filepath.Join(data, "work")
|
||||||
|
os.Mkdir(dir, 0o755)
|
||||||
|
victim := filepath.Join(dir, "weird name ;rm -rf ~;")
|
||||||
|
if err := os.WriteFile(victim, []byte("x"), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := Trash(victim); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(victim); !os.IsNotExist(err) {
|
||||||
|
t.Fatal("file still present")
|
||||||
|
}
|
||||||
|
trashed := filepath.Join(data, "Trash", "files", "weird name ;rm -rf ~;")
|
||||||
|
if _, err := os.Lstat(trashed); err != nil {
|
||||||
|
t.Fatalf("not in trash: %v", err)
|
||||||
|
}
|
||||||
|
info, err := os.ReadFile(filepath.Join(data, "Trash", "info", "weird name ;rm -rf ~;.trashinfo"))
|
||||||
|
if err != nil || !strings.Contains(string(info), "Path="+strings.ReplaceAll(dir, " ", "%20")) {
|
||||||
|
t.Fatalf("bad trashinfo: %q %v", info, err)
|
||||||
|
}
|
||||||
|
// A second item with the same name gets a distinct trash name.
|
||||||
|
os.WriteFile(victim, []byte("y"), 0o644)
|
||||||
|
if err := Trash(victim); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := os.Lstat(trashed + ".2"); err != nil {
|
||||||
|
t.Fatal("collision not handled")
|
||||||
|
}
|
||||||
|
if err := Trash("relative/path"); err == nil {
|
||||||
|
t.Fatal("relative paths must be refused")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package scan
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestMemoryPerNode reports heap use per inventory node for a real tree.
|
||||||
|
// Set MAPSIZE_MEM_ROOT to a directory to run it.
|
||||||
|
func TestMemoryPerNode(t *testing.T) {
|
||||||
|
root := os.Getenv("MAPSIZE_MEM_ROOT")
|
||||||
|
if root == "" {
|
||||||
|
t.Skip("set MAPSIZE_MEM_ROOT")
|
||||||
|
}
|
||||||
|
var before, after runtime.MemStats
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&before)
|
||||||
|
start := time.Now()
|
||||||
|
s, err := Start(context.Background(), root, Options{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
<-s.Done()
|
||||||
|
el := time.Since(start)
|
||||||
|
runtime.GC()
|
||||||
|
runtime.ReadMemStats(&after)
|
||||||
|
n := s.Tree.Len()
|
||||||
|
heap := after.HeapAlloc - before.HeapAlloc
|
||||||
|
t.Logf("nodes=%d scan=%s heap=%.1f MiB (%.0f bytes/node)", n, el.Round(time.Millisecond),
|
||||||
|
float64(heap)/(1<<20), float64(heap)/float64(n))
|
||||||
|
runtime.KeepAlive(s)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package tui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/colorprofile"
|
||||||
|
|
||||||
|
"github.com/andydixon/mapsize/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bigTree builds ~1M nodes in memory: 200 top-level dirs with nested
|
||||||
|
// subdirectories, plus one flat directory with 200k files.
|
||||||
|
func bigTree() *inventory.Tree {
|
||||||
|
r := rand.New(rand.NewSource(1))
|
||||||
|
t := inventory.New("/big", inventory.KindDir)
|
||||||
|
file := func(p inventory.NodeID, i int) {
|
||||||
|
id := t.Add(p, fmt.Sprintf("f%d.dat", i), inventory.KindFile)
|
||||||
|
n := t.Node(id)
|
||||||
|
n.Size = int64(r.ExpFloat64() * 1e6)
|
||||||
|
n.Alloc = n.Size
|
||||||
|
n.Cat = inventory.Category(r.Intn(int(inventory.NumCategories)))
|
||||||
|
}
|
||||||
|
flat := t.Add(0, "flat", inventory.KindDir)
|
||||||
|
for i := range 200_000 {
|
||||||
|
file(flat, i)
|
||||||
|
}
|
||||||
|
for d := range 200 {
|
||||||
|
top := t.Add(0, fmt.Sprintf("dir%d", d), inventory.KindDir)
|
||||||
|
for s := range 40 {
|
||||||
|
sub := t.Add(top, fmt.Sprintf("sub%d", s), inventory.KindDir)
|
||||||
|
for f := range 100 {
|
||||||
|
file(sub, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Recompute()
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
var bigTreeCache *inventory.Tree
|
||||||
|
|
||||||
|
func benchModel(b *testing.B) *Model {
|
||||||
|
if bigTreeCache == nil {
|
||||||
|
bigTreeCache = bigTree()
|
||||||
|
}
|
||||||
|
m := newModel(Options{}, nil, bigTreeCache, func() {})
|
||||||
|
m.profile = colorprofile.TrueColor
|
||||||
|
send(m, size(200, 60))
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cold frame: sorting children, LOD selection, layout and paint.
|
||||||
|
func BenchmarkBigColdFrame(b *testing.B) {
|
||||||
|
m := benchModel(b)
|
||||||
|
b.ReportMetric(float64(m.tree.Len()), "nodes")
|
||||||
|
for b.Loop() {
|
||||||
|
m.invalidate()
|
||||||
|
m.View()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize: child lists cached; layout and paint rerun for the new size.
|
||||||
|
func BenchmarkBigResize(b *testing.B) {
|
||||||
|
m := benchModel(b)
|
||||||
|
m.View()
|
||||||
|
i := 0
|
||||||
|
for b.Loop() {
|
||||||
|
send(m, size(150+i%100, 40+i%30))
|
||||||
|
m.View()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zoomed into a 200k-entry directory (cold child sort each time).
|
||||||
|
func BenchmarkBigFlatDirCold(b *testing.B) {
|
||||||
|
m := benchModel(b)
|
||||||
|
m.tree.Children(0, func(id inventory.NodeID, n *inventory.Node) {
|
||||||
|
if n.Name == "flat" {
|
||||||
|
m.zoom = id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for b.Loop() {
|
||||||
|
m.invalidate()
|
||||||
|
m.View()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arrow-key navigation including the re-render it triggers.
|
||||||
|
func BenchmarkBigNavigate(b *testing.B) {
|
||||||
|
m := benchModel(b)
|
||||||
|
m.View()
|
||||||
|
keys := []string{"right", "down", "left", "up"}
|
||||||
|
i := 0
|
||||||
|
for b.Loop() {
|
||||||
|
send(m, key(keys[i%4]))
|
||||||
|
m.View()
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkBigFilter(b *testing.B) {
|
||||||
|
m := benchModel(b)
|
||||||
|
for _, q := range []string{"size > 1MB", "*.dat AND size > 500k", "path contains sub3"} {
|
||||||
|
b.Run(q, func(b *testing.B) {
|
||||||
|
m.search.ed.set(q)
|
||||||
|
m.queryChanged()
|
||||||
|
for b.Loop() {
|
||||||
|
m.applyFilter()()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user