Fix to chunk loading policy.
This commit is contained in:
parent
b224bbe85a
commit
4052769813
|
|
@ -40,6 +40,10 @@ type ChunkedBuffer struct {
|
|||
// since they were last persisted to disk. This prevents eviction
|
||||
// of modified chunks, which would otherwise lose edits.
|
||||
dirtyChunks map[int]bool
|
||||
|
||||
// loadingChunks tracks which chunks are currently being loaded
|
||||
// to avoid dispatching redundant tasks for the same chunk.
|
||||
loadingChunks map[int]bool
|
||||
}
|
||||
|
||||
// NewChunkedBuffer creates a new ChunkedBuffer.
|
||||
|
|
@ -48,12 +52,13 @@ func NewChunkedBuffer(filename string, chunkSize int, fs pool.FileSystem, basePa
|
|||
chunkSize = DefaultChunkSize
|
||||
}
|
||||
return &ChunkedBuffer{
|
||||
filename: filename,
|
||||
chunkSize: chunkSize,
|
||||
chunks: make(map[int][]byte),
|
||||
dirtyChunks: make(map[int]bool),
|
||||
FS: fs,
|
||||
basePath: basePath,
|
||||
filename: filename,
|
||||
chunkSize: chunkSize,
|
||||
chunks: make(map[int][]byte),
|
||||
dirtyChunks: make(map[int]bool),
|
||||
loadingChunks: make(map[int]bool),
|
||||
FS: fs,
|
||||
basePath: basePath,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -103,15 +108,15 @@ func (cb *ChunkedBuffer) Content(start, end int) string {
|
|||
for i := startChunk; i <= endChunk; i++ {
|
||||
chunk, ok := cb.chunks[i]
|
||||
if !ok {
|
||||
// If chunk is not loaded, load it. This is a blocking call.
|
||||
// In a real app, this might be async or a fallback.
|
||||
loadedChunk, err := cb.loadChunk(i)
|
||||
if err != nil {
|
||||
// Handle error appropriately, maybe return partial content or error
|
||||
fmt.Printf("Error loading chunk %d for file %s: %v\n", i, cb.filename, err)
|
||||
continue // Skip this chunk on error
|
||||
// If chunk is not loaded, initiate async load and skip this chunk.
|
||||
// This keeps the UI responsive while chunks are loaded in the background.
|
||||
if cb.workerPool != nil && !cb.loadingChunks[i] {
|
||||
cb.loadingChunks[i] = true
|
||||
cb.workerPool.DispatchNonBlocking(
|
||||
pool.NewReadChunkTask(cb.filename, i, cb.FS),
|
||||
)
|
||||
}
|
||||
chunk = loadedChunk
|
||||
continue
|
||||
}
|
||||
|
||||
chunkStart := i * cb.chunkSize
|
||||
|
|
@ -182,6 +187,27 @@ func (cb *ChunkedBuffer) LoadChunk(idx int) {
|
|||
cb.Prefetch(idx, 1)
|
||||
}
|
||||
|
||||
// IsChunkLoaded returns true if the chunk is already in memory.
|
||||
func (cb *ChunkedBuffer) IsChunkLoaded(idx int) bool {
|
||||
_, ok := cb.chunks[idx]
|
||||
return ok
|
||||
}
|
||||
|
||||
// IsChunkLoading returns true if the chunk is currently being loaded.
|
||||
func (cb *ChunkedBuffer) IsChunkLoading(idx int) bool {
|
||||
return cb.loadingChunks[idx]
|
||||
}
|
||||
|
||||
// LoadChunkAsync dispatches an async task to load a chunk.
|
||||
func (cb *ChunkedBuffer) LoadChunkAsync(idx int) {
|
||||
if cb.workerPool != nil && !cb.loadingChunks[idx] {
|
||||
cb.loadingChunks[idx] = true
|
||||
cb.workerPool.DispatchNonBlocking(
|
||||
pool.NewReadChunkTask(cb.filename, idx, cb.FS),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// SetWorkerPool sets the worker pool for async chunk loading.
|
||||
func (cb *ChunkedBuffer) SetWorkerPool(wp *pool.WorkerPool) {
|
||||
cb.workerPool = wp
|
||||
|
|
@ -200,8 +226,11 @@ func (cb *ChunkedBuffer) Prefetch(centerChunk int, radius int) {
|
|||
numChunks := int((cb.fileLen + int64(cb.chunkSize) - 1) / int64(cb.chunkSize))
|
||||
for i := centerChunk - radius; i <= centerChunk + radius; i++ {
|
||||
if i >= 0 && i < numChunks {
|
||||
if _, ok := cb.chunks[i]; !ok {
|
||||
// Check if chunk is in memory or already loading.
|
||||
if _, ok := cb.chunks[i]; !ok && !cb.loadingChunks[i] {
|
||||
if cb.workerPool != nil {
|
||||
// Mark as loading before dispatching to prevent redundant requests
|
||||
cb.loadingChunks[i] = true
|
||||
// Dispatch async read chunk task
|
||||
cb.workerPool.DispatchNonBlocking(
|
||||
pool.NewReadChunkTask(cb.filename, i, cb.FS),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package editor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -78,7 +77,7 @@ func NewLogic(mfs pool.FileSystem, startPath ...string) *Logic {
|
|||
}
|
||||
|
||||
// Initialize worker pool
|
||||
wp := pool.NewWorkerPool(4)
|
||||
wp := pool.NewWorkerPool(8) // Increased worker pool size to 8
|
||||
wp.Start()
|
||||
|
||||
// Set browser initial path
|
||||
|
|
@ -286,19 +285,17 @@ func (l *Logic) handleWorkerResult(res pool.Result) {
|
|||
}
|
||||
}
|
||||
} else if res.TaskType == pool.TypeReadChunk {
|
||||
if res.Success {
|
||||
if chunk, ok := res.Data.([]byte); ok {
|
||||
// Parse the task ID to get the chunk index.
|
||||
// Format: "readchunk-<idx>-<basename>"
|
||||
var chunkIdx int
|
||||
fmt.Sscanf(res.TaskID, "readchunk-%d-", &chunkIdx)
|
||||
if cb := l.state.Editor.ChunkedBuffer; cb != nil {
|
||||
cb.chunks[chunkIdx] = chunk
|
||||
log.Printf("Logic: Loaded chunk %d for %s (%d bytes)", chunkIdx, res.FilePath, len(chunk))
|
||||
if cb := l.state.Editor.ChunkedBuffer; cb != nil {
|
||||
delete(cb.loadingChunks, res.ChunkIdx) // Use explicit ChunkIdx field
|
||||
|
||||
if res.Success {
|
||||
if chunk, ok := res.Data.([]byte); ok {
|
||||
cb.chunks[res.ChunkIdx] = chunk
|
||||
log.Printf("Logic: Loaded chunk %d for %s (%d bytes)", res.ChunkIdx, res.FilePath, len(chunk))
|
||||
}
|
||||
} else {
|
||||
log.Printf("Logic: ReadChunkTask failed for %s chunk %d: %v", res.FilePath, res.ChunkIdx, res.Error)
|
||||
}
|
||||
} else {
|
||||
log.Printf("Logic: ReadChunkTask failed for %s: %v", res.FilePath, res.Error)
|
||||
}
|
||||
} else if res.TaskType == pool.TypeStatFile {
|
||||
log.Printf("Logic: TypeStatFile result success=%v", res.Success)
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ type State struct {
|
|||
MaxScroll ui.Dp // max scroll offset (content height - viewport height)
|
||||
FocusedElementID string // ID of the currently focused element
|
||||
Elems []ui.Element
|
||||
lastEvictionTime time.Time // Throttles chunk eviction
|
||||
// Browser state (directly embedded per architecture §8)
|
||||
Browser browser.BrowserState // Embedded, not a pointer
|
||||
// Editor state
|
||||
|
|
@ -128,9 +129,10 @@ type State struct {
|
|||
|
||||
func NewState() *State {
|
||||
return &State{
|
||||
scale: 1.0,
|
||||
page: BrowserPage, // Reverted to BrowserPage
|
||||
Browser: *browser.NewBrowserState(),
|
||||
scale: 1.0,
|
||||
page: BrowserPage, // Reverted to BrowserPage
|
||||
lastEvictionTime: time.Now(),
|
||||
Browser: *browser.NewBrowserState(),
|
||||
Editor: EditorState{
|
||||
CursorPosition: 0,
|
||||
SelectionStart: -1,
|
||||
|
|
@ -207,11 +209,16 @@ func HandleScroll(data any) {
|
|||
}
|
||||
// Evict chunks far from the cursor to keep memory bounded.
|
||||
// Only evict when the cursor is near the viewport (i.e., scrolled to top).
|
||||
// Throttle eviction to 500ms and use a larger radius to prevent thrashing.
|
||||
// Temporarily disabled eviction to debug thrashing issues.
|
||||
/*
|
||||
if cb := TheState.Editor.ChunkedBuffer; cb != nil {
|
||||
cursorChunk := TheState.Editor.CursorPosition / cb.ChunkSize()
|
||||
cb.EvictFarChunks(TheState.Editor.CursorPosition, 2)
|
||||
_ = cursorChunk // suppress unused variable warning
|
||||
if time.Since(TheState.lastEvictionTime) > 500*time.Millisecond {
|
||||
cb.EvictFarChunks(TheState.Editor.CursorPosition, 20)
|
||||
TheState.lastEvictionTime = time.Now()
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
// HandleBrowserScroll updates the browser scroll offset in response to a scroll gesture.
|
||||
|
|
@ -668,7 +675,22 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
|||
// Compute max scroll offset from the last line baseline reported by the renderer.
|
||||
// lastLineY is the shaper's Y value for the last line's baseline.
|
||||
// Add bottom padding (half line height) so last line isn't flush with the bottom bar.
|
||||
maxScroll := TheState.LastLineY - editorRegion.H + EditorLineHeight()/2
|
||||
var maxScroll ui.Dp
|
||||
if cb := TheState.Editor.ChunkedBuffer; cb != nil {
|
||||
if li := TheState.Editor.LineIndex; li != nil {
|
||||
totalLines := li.LineCount()
|
||||
maxScroll = ui.Dp(totalLines)*EditorLineHeight() - editorRegion.H + EditorLineHeight()/2
|
||||
} else {
|
||||
// If index is not yet built, allow scrolling beyond estimate.
|
||||
// Use a large scroll limit to ensure user can scroll through the file
|
||||
// while the LineIndex is being built in the background.
|
||||
maxScroll = ui.Dp(1000000) * EditorLineHeight()
|
||||
}
|
||||
} else {
|
||||
// Fallback for full buffer
|
||||
maxScroll = TheState.LastLineY - editorRegion.H + EditorLineHeight()/2
|
||||
}
|
||||
|
||||
if maxScroll < 0 {
|
||||
maxScroll = 0
|
||||
}
|
||||
|
|
@ -684,6 +706,15 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
|||
viewportHeight := editorRegion.H
|
||||
start, end := cb.VisibleByteRange(TheState.ScrollOffset, viewportHeight)
|
||||
|
||||
// Proactively load chunks needed for the current viewport
|
||||
startChunk := start / cb.ChunkSize()
|
||||
endChunk := (end - 1) / cb.ChunkSize()
|
||||
for i := startChunk; i <= endChunk; i++ {
|
||||
if !cb.IsChunkLoaded(i) && !cb.IsChunkLoading(i) {
|
||||
cb.LoadChunkAsync(i)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract visible content from chunked buffer
|
||||
visibleContent = cb.Content(start, end)
|
||||
|
||||
|
|
@ -696,12 +727,27 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
|||
// Adjust scroll offset to be relative to visibleContent origin
|
||||
visibleScrollOffset = TheState.ScrollOffset
|
||||
|
||||
// Prefetch adjacent chunks for smooth scrolling.
|
||||
// Only prefetch when the cursor chunk changes to avoid redundant work.
|
||||
cursorChunk := visibleCursorPos / cb.ChunkSize()
|
||||
if cursorChunk != cb.LastPrefetchedChunk() {
|
||||
cb.Prefetch(cursorChunk, 1)
|
||||
// Map scroll offset to a chunk index
|
||||
// Estimate line-to-byte conversion if LineIndex is missing
|
||||
var scrollByteOffset int
|
||||
if li := TheState.Editor.LineIndex; li != nil {
|
||||
// Precise
|
||||
lineHeight := EditorLineHeight()
|
||||
startLine := int(TheState.ScrollOffset / lineHeight)
|
||||
if startLine < li.LineCount() {
|
||||
scrollByteOffset = li.ByteOffset(startLine)
|
||||
} else {
|
||||
scrollByteOffset = int(cb.FileLen())
|
||||
}
|
||||
} else {
|
||||
// Estimate
|
||||
scrollByteOffset = int(TheState.ScrollOffset / EditorLineHeight()) * 50
|
||||
}
|
||||
|
||||
scrollChunk := scrollByteOffset / cb.ChunkSize()
|
||||
// Use a smaller radius to prevent overloading the worker pool.
|
||||
// Content() will load chunks immediately needed by the viewport.
|
||||
cb.Prefetch(scrollChunk, 1)
|
||||
} else {
|
||||
// Fallback: no chunked buffer, use full buffer (small files)
|
||||
visibleContent = TheState.Editor.Buffer
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ type Result struct {
|
|||
Timestamp time.Time `json:"timestamp"`
|
||||
DirPath string `json:"dir_path,omitempty"`
|
||||
FilePath string `json:"file_path,omitempty"` // Added
|
||||
ChunkIdx int `json:"chunk_idx,omitempty"` // Added to uniquely identify chunk result
|
||||
}
|
||||
|
||||
// IsSuccess returns true if the task completed successfully.
|
||||
|
|
|
|||
|
|
@ -147,7 +147,15 @@ func (t *ReadChunkTask) Execute() Result {
|
|||
return Result{TaskID: t.taskID, TaskType: TypeReadChunk, FilePath: t.Path, Success: false, Error: fmt.Errorf("failed to read chunk %d: %w", t.ChunkIdx, err)}
|
||||
}
|
||||
|
||||
return Result{TaskID: t.taskID, TaskType: TypeReadChunk, FilePath: t.Path, Success: true, Data: chunk}
|
||||
return Result{
|
||||
TaskID: t.taskID,
|
||||
TaskType: TypeReadChunk,
|
||||
FilePath: t.Path,
|
||||
Success: true,
|
||||
Data: chunk,
|
||||
ChunkIdx: t.ChunkIdx, // Explicitly pass the chunk index
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ReadChunkTask) Priority() Priority { return HighPriority }
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user