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
|
// since they were last persisted to disk. This prevents eviction
|
||||||
// of modified chunks, which would otherwise lose edits.
|
// of modified chunks, which would otherwise lose edits.
|
||||||
dirtyChunks map[int]bool
|
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.
|
// NewChunkedBuffer creates a new ChunkedBuffer.
|
||||||
|
|
@ -52,6 +56,7 @@ func NewChunkedBuffer(filename string, chunkSize int, fs pool.FileSystem, basePa
|
||||||
chunkSize: chunkSize,
|
chunkSize: chunkSize,
|
||||||
chunks: make(map[int][]byte),
|
chunks: make(map[int][]byte),
|
||||||
dirtyChunks: make(map[int]bool),
|
dirtyChunks: make(map[int]bool),
|
||||||
|
loadingChunks: make(map[int]bool),
|
||||||
FS: fs,
|
FS: fs,
|
||||||
basePath: basePath,
|
basePath: basePath,
|
||||||
}
|
}
|
||||||
|
|
@ -103,15 +108,15 @@ func (cb *ChunkedBuffer) Content(start, end int) string {
|
||||||
for i := startChunk; i <= endChunk; i++ {
|
for i := startChunk; i <= endChunk; i++ {
|
||||||
chunk, ok := cb.chunks[i]
|
chunk, ok := cb.chunks[i]
|
||||||
if !ok {
|
if !ok {
|
||||||
// If chunk is not loaded, load it. This is a blocking call.
|
// If chunk is not loaded, initiate async load and skip this chunk.
|
||||||
// In a real app, this might be async or a fallback.
|
// This keeps the UI responsive while chunks are loaded in the background.
|
||||||
loadedChunk, err := cb.loadChunk(i)
|
if cb.workerPool != nil && !cb.loadingChunks[i] {
|
||||||
if err != nil {
|
cb.loadingChunks[i] = true
|
||||||
// Handle error appropriately, maybe return partial content or error
|
cb.workerPool.DispatchNonBlocking(
|
||||||
fmt.Printf("Error loading chunk %d for file %s: %v\n", i, cb.filename, err)
|
pool.NewReadChunkTask(cb.filename, i, cb.FS),
|
||||||
continue // Skip this chunk on error
|
)
|
||||||
}
|
}
|
||||||
chunk = loadedChunk
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
chunkStart := i * cb.chunkSize
|
chunkStart := i * cb.chunkSize
|
||||||
|
|
@ -182,6 +187,27 @@ func (cb *ChunkedBuffer) LoadChunk(idx int) {
|
||||||
cb.Prefetch(idx, 1)
|
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.
|
// SetWorkerPool sets the worker pool for async chunk loading.
|
||||||
func (cb *ChunkedBuffer) SetWorkerPool(wp *pool.WorkerPool) {
|
func (cb *ChunkedBuffer) SetWorkerPool(wp *pool.WorkerPool) {
|
||||||
cb.workerPool = wp
|
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))
|
numChunks := int((cb.fileLen + int64(cb.chunkSize) - 1) / int64(cb.chunkSize))
|
||||||
for i := centerChunk - radius; i <= centerChunk + radius; i++ {
|
for i := centerChunk - radius; i <= centerChunk + radius; i++ {
|
||||||
if i >= 0 && i < numChunks {
|
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 {
|
if cb.workerPool != nil {
|
||||||
|
// Mark as loading before dispatching to prevent redundant requests
|
||||||
|
cb.loadingChunks[i] = true
|
||||||
// Dispatch async read chunk task
|
// Dispatch async read chunk task
|
||||||
cb.workerPool.DispatchNonBlocking(
|
cb.workerPool.DispatchNonBlocking(
|
||||||
pool.NewReadChunkTask(cb.filename, i, cb.FS),
|
pool.NewReadChunkTask(cb.filename, i, cb.FS),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package editor
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
@ -78,7 +77,7 @@ func NewLogic(mfs pool.FileSystem, startPath ...string) *Logic {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize worker pool
|
// Initialize worker pool
|
||||||
wp := pool.NewWorkerPool(4)
|
wp := pool.NewWorkerPool(8) // Increased worker pool size to 8
|
||||||
wp.Start()
|
wp.Start()
|
||||||
|
|
||||||
// Set browser initial path
|
// Set browser initial path
|
||||||
|
|
@ -286,19 +285,17 @@ func (l *Logic) handleWorkerResult(res pool.Result) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if res.TaskType == pool.TypeReadChunk {
|
} else if res.TaskType == pool.TypeReadChunk {
|
||||||
|
if cb := l.state.Editor.ChunkedBuffer; cb != nil {
|
||||||
|
delete(cb.loadingChunks, res.ChunkIdx) // Use explicit ChunkIdx field
|
||||||
|
|
||||||
if res.Success {
|
if res.Success {
|
||||||
if chunk, ok := res.Data.([]byte); ok {
|
if chunk, ok := res.Data.([]byte); ok {
|
||||||
// Parse the task ID to get the chunk index.
|
cb.chunks[res.ChunkIdx] = chunk
|
||||||
// Format: "readchunk-<idx>-<basename>"
|
log.Printf("Logic: Loaded chunk %d for %s (%d bytes)", res.ChunkIdx, res.FilePath, len(chunk))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Logic: ReadChunkTask failed for %s: %v", res.FilePath, res.Error)
|
log.Printf("Logic: ReadChunkTask failed for %s chunk %d: %v", res.FilePath, res.ChunkIdx, res.Error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if res.TaskType == pool.TypeStatFile {
|
} else if res.TaskType == pool.TypeStatFile {
|
||||||
log.Printf("Logic: TypeStatFile result success=%v", res.Success)
|
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)
|
MaxScroll ui.Dp // max scroll offset (content height - viewport height)
|
||||||
FocusedElementID string // ID of the currently focused element
|
FocusedElementID string // ID of the currently focused element
|
||||||
Elems []ui.Element
|
Elems []ui.Element
|
||||||
|
lastEvictionTime time.Time // Throttles chunk eviction
|
||||||
// Browser state (directly embedded per architecture §8)
|
// Browser state (directly embedded per architecture §8)
|
||||||
Browser browser.BrowserState // Embedded, not a pointer
|
Browser browser.BrowserState // Embedded, not a pointer
|
||||||
// Editor state
|
// Editor state
|
||||||
|
|
@ -130,6 +131,7 @@ func NewState() *State {
|
||||||
return &State{
|
return &State{
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
page: BrowserPage, // Reverted to BrowserPage
|
page: BrowserPage, // Reverted to BrowserPage
|
||||||
|
lastEvictionTime: time.Now(),
|
||||||
Browser: *browser.NewBrowserState(),
|
Browser: *browser.NewBrowserState(),
|
||||||
Editor: EditorState{
|
Editor: EditorState{
|
||||||
CursorPosition: 0,
|
CursorPosition: 0,
|
||||||
|
|
@ -207,11 +209,16 @@ func HandleScroll(data any) {
|
||||||
}
|
}
|
||||||
// Evict chunks far from the cursor to keep memory bounded.
|
// Evict chunks far from the cursor to keep memory bounded.
|
||||||
// Only evict when the cursor is near the viewport (i.e., scrolled to top).
|
// 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 {
|
if cb := TheState.Editor.ChunkedBuffer; cb != nil {
|
||||||
cursorChunk := TheState.Editor.CursorPosition / cb.ChunkSize()
|
if time.Since(TheState.lastEvictionTime) > 500*time.Millisecond {
|
||||||
cb.EvictFarChunks(TheState.Editor.CursorPosition, 2)
|
cb.EvictFarChunks(TheState.Editor.CursorPosition, 20)
|
||||||
_ = cursorChunk // suppress unused variable warning
|
TheState.lastEvictionTime = time.Now()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleBrowserScroll updates the browser scroll offset in response to a scroll gesture.
|
// 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.
|
// 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.
|
// 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.
|
// 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 {
|
if maxScroll < 0 {
|
||||||
maxScroll = 0
|
maxScroll = 0
|
||||||
}
|
}
|
||||||
|
|
@ -684,6 +706,15 @@ func EditorLayout(screenWidth, screenHeight ui.Dp, wordWrap bool) []ui.Element {
|
||||||
viewportHeight := editorRegion.H
|
viewportHeight := editorRegion.H
|
||||||
start, end := cb.VisibleByteRange(TheState.ScrollOffset, viewportHeight)
|
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
|
// Extract visible content from chunked buffer
|
||||||
visibleContent = cb.Content(start, end)
|
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
|
// Adjust scroll offset to be relative to visibleContent origin
|
||||||
visibleScrollOffset = TheState.ScrollOffset
|
visibleScrollOffset = TheState.ScrollOffset
|
||||||
|
|
||||||
// Prefetch adjacent chunks for smooth scrolling.
|
// Map scroll offset to a chunk index
|
||||||
// Only prefetch when the cursor chunk changes to avoid redundant work.
|
// Estimate line-to-byte conversion if LineIndex is missing
|
||||||
cursorChunk := visibleCursorPos / cb.ChunkSize()
|
var scrollByteOffset int
|
||||||
if cursorChunk != cb.LastPrefetchedChunk() {
|
if li := TheState.Editor.LineIndex; li != nil {
|
||||||
cb.Prefetch(cursorChunk, 1)
|
// 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 {
|
} else {
|
||||||
// Fallback: no chunked buffer, use full buffer (small files)
|
// Fallback: no chunked buffer, use full buffer (small files)
|
||||||
visibleContent = TheState.Editor.Buffer
|
visibleContent = TheState.Editor.Buffer
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ type Result struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
DirPath string `json:"dir_path,omitempty"`
|
DirPath string `json:"dir_path,omitempty"`
|
||||||
FilePath string `json:"file_path,omitempty"` // Added
|
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.
|
// 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: 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 }
|
func (t *ReadChunkTask) Priority() Priority { return HighPriority }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user