Commit Graph

67 Commits

Author SHA1 Message Date
dailz
420b853cb9 fix(watcher): filter Remove events by path to prevent false removed reports (closes #15) 2026-06-09 13:18:23 +08:00
dailz
7852e92ecc fix(watcher): forward notify backend errors instead of silently discarding
Previously Err(_) => return in the notify callback silently dropped all
backend errors (inotify exhaustion, fs unmount, permission loss), leaving
the application unaware that file monitoring had stopped working.

Add FileEvent::WatcherError { message: String } variant to propagate
backend errors through the existing bounded channel. The TUI consumer
receives the event without disrupting the UI for transient errors.

Closes #14
2026-06-09 11:26:54 +08:00
dailz
d37ed6df68 fix(io): harden read_cache against zero-length false hits and overflow (closes #13)
- Add early return for len==0 (Ok(&[])) matching std::io semantics
- Add slot.len > 0 guard to cache hit predicate to prevent empty-slot
  false matches
- Replace unchecked arithmetic with checked_add/saturating_add for
  request_end, block_end, and post-read coverage check
- Fix misleading comment about get(file,0,0) behavior on miss path
- Strengthen clear() to fully reset block_offset and last_access
- Register read_cache module in io/mod.rs
- Add 4 regression tests: zero-len on fresh/populated cache,
  zero-len at u64::MAX, overflow error on nonzero read at u64::MAX
2026-06-09 10:48:34 +08:00
dailz
b58d66f2aa fix(io): use unicode-width for correct CJK/emoji/zero-width display width (closes #12) 2026-06-07 12:50:17 +08:00
dailz
d4679a7543 fix(io): update visual height of last line on append without trailing newline (closes #11)
When a file does not end with a newline, appending content extends the
last logical line's text and thus its visual height. The incremental
extend path in handle_file_appended only computed heights for newly
created logical lines, missing the old last line whose content changed.

Add VisualHeightIndex::replace_last_line_height() — an O(1) method that
rewrites the final prefix sum entry and total. Called before
extend_from_heights so the correct line is targeted.

Changes:
- progressive_reader.rs: add replace_last_line_height, pub with_params,
  7 VHI unit tests
- app.rs: save old_reader_line_count before update, recompute last old
  line height in extend path, 2 integration regression tests
2026-06-07 09:46:24 +08:00
dailz
6a2f8ecb66 fix(bench): resolve pre-existing clippy warnings in report.rs and mmap_reader.rs 2026-06-07 09:15:34 +08:00
dailz
f6081b9fe9 fix(bench): propagate I/O errors in append_lines instead of silently defaulting to 0 (closes #45) 2026-06-07 09:13:37 +08:00
dailz
97a2c6a925 fix(bench): regenerate growable file each iteration in truncate safety benchmarks (closes #44) 2026-06-07 09:02:19 +08:00
dailz
e6e0e2cc90 fix(bench): correct lines_read to actual successful reads in bench_scroll_rss
lines_read was incorrectly set to max_lines.min(total) (the loop upper bound)
instead of the actual number of successfully read lines. Now tracks lines_read
via get_line(i).is_some() counter and uses max_lines.min(total) as the correct
loop upper bound to handle empty file edge case.

Fixes #43
2026-06-07 08:50:20 +08:00
dailz
a8dc067cd4 fix(bench): [M23] prevent single_frame_tail/head overlap for small files
- Extract shared FRAME_LINES constant into suites/mod.rs
- Add select_frame_positions() helper with 3*FRAME_LINES threshold
  to guarantee non-overlapping head/middle/tail ranges
- Guard bench_reverse_scan against total <= FRAME_LINES
- Add 9 boundary tests for position selection (0..1M lines)

Closes #42
2026-06-07 08:31:00 +08:00
dailz
502479677b fix(bench): warn when clear_file_cache fails instead of silently skipping cold benchmarks (closes #41) 2026-06-05 17:28:57 +08:00
dailz
5656b26d7b refactor(bench): unify line counting in get_file_info to use count_existing_lines
Reuse the existing count_existing_lines() (reader.lines().count())
instead of a manual read_until loop, eliminating duplicate line-counting
logic in data_gen.rs.

Closes #40
2026-06-05 17:04:46 +08:00
dailz
a8b64e78bd fix(bench): stabilize report column/row ordering across input permutations (#39)
- variants: use direct tuple comparison instead of format! string, add sort() after dedup
- Memory section: sort rows by (test_name, backend, variant) before output
- Extra Metrics section: sort rows by (test_name, variant_label) before output
- add [lib] target to Cargo.toml to enable unit tests
- add regression test: same data in different input order produces identical report
2026-06-05 16:24:41 +08:00
dailz
e945a357f7 fix(bench): warn on all reset_vm_hwm errors, not just PermissionDenied
Issue #38: warn_reset_hwm() silently swallowed non-permission I/O errors
from /proc/self/clear_refs (e.g. missing /proc, read-only procfs, kernel
incompatibility). This left users unaware that VmHWM reset failed and
memory peak data could be contaminated across suites.

Changes:
- runner.rs: all errors now produce a warning with specific failure reason;
  PermissionDenied retains 'try running as root' hint; AtomicBool warn-once
  prevents duplicate output across 7 suite runs
- main.rs: preflight check now uses warn_reset_hwm() instead of the vague
  can_reset_vm_hwm(), sharing the same warn-once mechanism
- metrics.rs: remove dead can_reset_vm_hwm() (no callers remaining)
- tests: add hwm_warned_flag_prevents_reentry and warn_reset_hwm_does_not_panic
2026-06-05 15:52:01 +08:00
dailz
fb57584546 fix(bench): validate --suites names, reject unknown suites at CLI boundary
Introduce Suite enum (runner.rs) replacing stringly-typed suite matching.
BenchConfig.suites is now Option<Vec<Suite>>, making invalid states
unrepresentable. Unknown suite names produce a clear error listing all
valid values.

Fixes: #37
2026-06-05 15:20:24 +08:00
dailz
9baec5ab69 fix(bench): refresh PreadReader index periodically in scroll_during_append (closes #36)
The reader's line_index and file_size were frozen at open time.
After current_line exceeded the initial 150K lines, get_line_impl
returned None for all subsequent reads. With the background thread
appending ~10K lines/sec, ~40% of measured frame latencies were
actually the cost of a None return, not real I/O.

- Add PreadReaderCore::refresh_index(&mut self): seek to start,
  rebuild LineIndex, update file_size, invalidate read cache
- Add PreadReaderPlain::refresh_index forwarding method
- Add ReadCache::invalidate to force cache miss after reindex
- Rewrite bench_scroll_during_append: time-based refresh (250ms),
  only record latencies for successful reads, assert max_line > initial
- Add regression tests for refresh_index with appended lines
2026-06-05 14:40:32 +08:00
dailz
6dd87d2872 fix(bench): wrap file writes with BufWriter to reduce syscall overhead
Add BufWriter::with_capacity(64KB) to generate_test_file,
generate_growable_file, and append_lines in data_gen.rs.

Previously each writeln! triggered an individual write syscall,
making 5GB/74M-line benchmark data generation extremely slow.
BufWriter batches writes into 64KB chunks, reducing syscalls
by ~1000x.

Explicit flush()? + drop before subsequent reads ensures data
visibility and propagates flush errors (BufWriter::drop swallows
them).

Closes #35
2026-06-05 14:01:35 +08:00
dailz
83f633a562 fix(bench): make can_reset_vm_hwm side-effect-free with open probe (closes #34) 2026-06-05 13:34:01 +08:00
dailz
dad5f5a635 fix(bench): eliminate SIGBUS handler static mut UB with Once + raw atomics (closes #33)
Replace `static mut OLD_SIGBUS_HANDLER` with AtomicU8 + AtomicPtr to
remove data race UB when concurrent benchmarks call open() from multiple
threads.

Key changes:
- Use `Once::call_once` to guarantee single handler installation
- Publish old handler to atomics BEFORE installing new handler (closes
  the handler-active-but-state-unpublished race window)
- Read atomics with Acquire in signal handler (async-signal-safe)
- Align si_addr to page boundary before mmap(MAP_FIXED)
- Add concurrent test: 8 threads open all 5 variants simultaneously
2026-06-05 13:22:02 +08:00
dailz
534a089b58 fix(tui): defer file-change events during Loading state to prevent stale reader (closes #10)
Loading state silently dropped FileEvent::Appended/Truncated via _ => {}.
After Loading→Ready transition the FileReader was based on a stale snapshot.

- Add reload_after_loading flag to defer reload until Ready state
- Extract reload_ready_reader() from handle_file_truncated
- Explicit 3-branch match: Ready handles, Loading sets flag, rest ignores
- Clear flag on IndexerMessage::Error to prevent stale dirty bit
- 4 regression tests covering append/truncate/collapse/error paths
2026-06-04 17:32:58 +08:00
dailz
b7938e069d fix(tui): RAII TerminalGuard prevents terminal corruption on error exit (closes #8)
The previous code called std::process::exit(1) on file load failure,
bypassing all terminal restoration (disable_raw_mode, LeaveAlternateScreen,
show_cursor). This left the user's shell in a broken state with no echo
and no visible cursor.

Introduce TerminalGuard with Drop-based cleanup that fires on every exit
path: normal return, ? error propagation, and panic unwind. The guard
also handles partial initialization rollback (e.g. raw mode enabled but
alternate screen fails). Remove all process::exit calls from the guarded
scope.
2026-06-04 16:11:16 +08:00
dailz
d40d70c600 fix(watcher): use try_send to prevent blocking notify callback
Replace tx.send() with tx.try_send() in file watcher notify callback
to avoid blocking the notify thread when the bounded channel (cap 100)
is full. Events are silently dropped instead of blocking, preventing
shutdown delays and event loss during consumer stalls.

Closes #7
2026-06-04 14:33:40 +08:00
dailz
1350f659fa fix(io): eliminate SIGBUS risk in background indexer threads (closes #6)
Background threads (spawn_indexer, spawn_visual_height_rebuild) previously
held mmap during entire file scan, risking SIGBUS if file was truncated
externally. Now uses BufReader streaming scan with mmap created only
after scan completes, plus stat validation.

Changes:
- spawn_indexer: replace mmap scan with BufReader fill_buf/consume loop,
  create mmap post-scan with fd stat validation
- spawn_visual_height_rebuild: replace mmap/FileReader with sequential
  BufReader scan, discard results on line count mismatch
- FileReader::open/reload/update_for_append: add stat-after-mmap check
- LineIndex: make fields pub(crate) for direct construction from scan loop
- Add 3 regression tests for truncation scenarios
2026-06-04 14:10:51 +08:00
dailz
1bb6b2e9f3 fix: eliminate TOCTOU race in IndexCache::save (closes #5)
spawn_indexer builds LineIndex from mmap snapshot but IndexCache::save()
re-opened the file to compute the hash. If the file changed between those
two steps, the cached index would be stored under the wrong hash.

- Add IndexCache::save_with_hash() that computes hash from in-memory data
- Add compute_data_hash() public function (same algorithm as compute_file_hash)
- Update spawn_indexer, FileReader::save_cache, and test callers
2026-06-04 13:30:16 +08:00
dailz
bef0b44e91 fix(io): guard sampled_offsets index in get_line() to prevent panic on corrupt cache
Replace direct indexing with .get()? to return None instead of panicking
when sampled_offsets is shorter than expected (e.g. corrupt bincode cache).

Closes #3
2026-06-03 15:39:05 +08:00
dailz
24fe97a457 fix(io): eliminate TOCTOU race in FileReader open/reload
Replace LineIndex::from_reader(BufReader) with LineIndex::from_bytes(&mmap)
in both open() and reload(), ensuring the line index is always built from
the same mmap snapshot rather than a separate read through the file descriptor.

This closes the race window where an external file modification between
mmap() and from_reader() could cause line_index offsets to disagree with
the mmap data, leading to get_line() returning wrong content or panicking.

Closes #2
2026-06-03 15:08:22 +08:00
dailz
b6e655bff6 fix(io): handle file shrink in update_for_append to prevent SIGBUS
File truncation/rotation during mmap lifetime caused SIGBUS crash
because update_for_append() ignored new_size < old_size, leaving
a stale mapping that would fault on access.

Introduce AppendStatus enum (Unchanged/Appended/Reloaded) so the
caller can distinguish shrink events from normal appends. On shrink,
reload() rebuilds the mmap and line index. The TUI layer clamps
cursor and invalidates viewport cache on Reloaded, matching the
existing handle_file_truncated() behavior.

Fixes #1
2026-06-03 14:47:23 +08:00
dailz
b3256b2917 fix: audit fixes for 4 medium-severity bugs: index_cache tail hash, read_cache doc, mutex poison recovery, Remove event handling 2026-05-10 17:03:07 +08:00
dailz
fb23e4c7cb fix(tui): smooth visual-row scrolling during Loading state
During Loading state (before VHI is built), j/k used to jump by logical
line, visually skipping multiple wrapped rows. Now uses v_sub_offset to
track position within a wrapped line, enabling smooth 1-visual-row scroll.

- Add v_sub_offset field to App for sub-line visual position tracking
- scroll_down/up_line else branch: advance v_sub_offset, wrap to next line
- ensure_viewport_cache Loading path: pass v_sub_offset as offset_in_line
- ensure_cursor_visible: skip during Loading (scroll functions manage it)
- Reset v_sub_offset on Loading→Ready, scroll_to_top, scroll_to_bottom
- Add 3 tests for Loading-state sub-offset scrolling behavior
2026-04-24 19:04:30 +08:00
dailz
8c5a838db0 fix(tui): j/k scroll by visual row instead of logical line
When a JSON line wraps to many visual rows (e.g. 73 rows), pressing j
would skip the entire logical line, making wrapped content unreadable.
Now j/k scroll by 1 visual row when a VisualHeightIndex is available,
with cursor tracking the viewport center. Falls back to logical-line
scroll in Loading/no-index modes or when all content fits the viewport.

Adds 4 tests: visual scroll down, visual scroll up, small-file fallback,
j/k roundtrip.
2026-04-24 07:41:30 +08:00
dailz
81cc72bd84 test(tui): add Loading + JSON expansion unit tests 2026-04-14 17:38:35 +08:00
dailz
06b2a39816 fix(tui): post-check cursor visibility in viewport during Loading 2026-04-14 17:09:50 +08:00
dailz
0941092b07 feat(tui): enable JSON expansion during Loading state 2026-04-14 16:52:17 +08:00
dailz
4ec3eb7cee fix(core): cap incremental scan in get_line() to prevent O(N) blocking
Add SCAN_AHEAD_LIMIT (10000 lines) to get_line() in Sampling state. Without this, jumping to end-of-file (G) during progressive loading would scan the entire file byte-by-byte on the main thread, blocking the UI and consuming excessive memory. Lines beyond the scanned region + limit now return None, which the TUI renders as empty.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:58:20 +08:00
dailz
05491304bf fix(tui): allow free scrolling during progressive loading
total_lines() was returning sampled_line_count() (only ~300 lines from the initial 64KB scan) during the Loading state, capping the scroll range. Use estimated_lines instead so the user can scroll to any position while indexing runs in the background. get_line() already supports incremental forward scanning on demand.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:36:03 +08:00
dailz
a03af7e74e feat(core): implement FileWatcher for live file tailing
Complete FileWatcher implementation using notify 8.x crate with get_inode() for cross-platform file identity. Support append detection for incremental index updates and truncate detection for full reloads.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:07:27 +08:00
dailz
bab3d9078a feat(tui): replace O(N) scanning with progressive loading and VisualHeightIndex
Replace synchronous file loading with AppLoadingState state machine (Empty/Loading/Ready/Error) for instant interactivity. Add ViewportCache for on-demand viewport computation, replacing global wrap/level caches. Integrate background indexer polling and file watcher events into the TUI event loop. Add loading UI with progress percentage, estimated line numbers with ~ prefix, and error state display. Eliminate all O(N) linear scans using VisualHeightIndex binary search.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:07:18 +08:00
dailz
f9f451e0d6 feat(core): implement ProgressiveFileReader with background indexer
Add ProgressiveFileReader state machine (Sampling/Ready/Error) with scanned_newlines cache for O(1) line lookups. Implement spawn_indexer() for background mmap-based indexing with cancellation and progress reporting. Add VisualHeightIndex with prefix-sum + binary search for O(log N) visual height queries. Register all new io submodules and extend FileReader with reload(), save_cache(), and accessor methods.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:07:09 +08:00
dailz
210eecfa66 feat(core): extract wrap utilities and extend LineIndex for progressive loading
Move wrap_line_chars and format_json_line from app.rs to core/io/wrap.rs with MAX_WRAP_INPUT_LEN guard. Add serde derives, pub getters, and extend_from_bytes() to LineIndex for incremental index building.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:06:52 +08:00
dailz
cfbe4900a5 feat(core): add LineSampler for fast line count estimation
Implement head/tail 64KB sampling to estimate total line count without scanning the entire file. Also provide read_first_lines() for reading the first N lines for immediate display during progressive loading.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:06:42 +08:00
dailz
2260d60302 feat(core): add disk index cache infrastructure
Add xxhash-rust and bincode workspace dependencies for fast hashing and serialization. Implement cache_util for cache directory/path resolution with versioning, and IndexCache for saving/loading line indices to disk with file-hash validation.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-14 09:06:36 +08:00
dailz
62a176441e feat(core): refactor FileReader to use mmap with low-RSS index building
Replace Vec<u8> with memmap2::Mmap for file content. Build line index via BufReader streaming (from_reader) instead of scanning mmap'd data, keeping RSS at ~3MB for 5GB files instead of ~5GB.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 20:50:58 +08:00
dailz
25a17779ff feat(core): implement sparse LineIndex with BufReader streaming
Rewrite LineIndex to use sparse sampling (every 256 lines) instead of per-line offsets. Add from_reader() for low-RSS streaming index construction via BufReader fill_buf()/consume(), reducing 5GB file RSS from 5122MB to 3.4MB.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 20:50:14 +08:00
dailz
02d7323a8b feat(core): add Mmap error variant and memmap2 dependency
Add CoreError::Mmap(String) error variant for mmap operations and memmap2 = "0.9" as workspace dependency.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 20:49:27 +08:00
dailz
2ac3eb99c7 feat(tui): add Span-based rendering with level colors and settings panel
Replace Line::styled with Span-based rendering: gutter (DarkGray fg) + content (level-based fg color). Add centered settings popup (S key) with j/k navigation, arrow key color cycling, number key shortcuts, Enter to save, Esc to cancel.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:52:45 +08:00
dailz
c6e83cb8c2 feat(tui): add AppMode, level_cache and config integration
Add AppMode enum (Normal/Settings), level_cache field populated in recompute_wrap_cache via detect_level(), color_config loaded from TOML in main.rs. Refactor handle_key for mode dispatch with S key to enter settings.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:52:03 +08:00
dailz
ef4c2e7383 feat(tui): add color mapping module
Add color.rs with parse_color (string to ratatui::Color), level_fg (LogLevel to color via ColorConfig), and AVAILABLE_COLORS constant for the settings panel.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:51:26 +08:00
dailz
c914912389 feat(core): add detect_level() for plain text logs
Add level detection that tries JSON parsing first (trusts level field), then falls back to keyword scanning with word-boundary checks. Supports SEVERE, FATAL, ERROR, WARN, INFO, DEBUG, TRACE and their abbreviations.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:50:59 +08:00
dailz
105e428a43 feat(core): add ColorConfig struct with TOML support
Replace empty AppConfig stub with ColorConfig struct that stores level-to-color mappings as strings. Supports loading/saving from XDG config directory via TOML, with graceful degradation on missing/invalid files.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-12 10:50:39 +08:00
dailz
67a118a8c8 docs(core): add Chinese comments to types module
Add detailed Chinese comments explaining Rust derive macros, FromStr/Display traits, serde serialization, and core data types.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-11 16:30:20 +08:00