1package vt
2
3import (
4 "errors"
5 "fmt"
6 "math"
7 "slices"
8
9 renderer "github.com/bnema/vev-vt/core"
10)
11
12// ErrHistoryRowTooLarge is returned when one row cannot fit within the
13// configured logical-byte budget or compact page bounds. History is unchanged.
14var ErrHistoryRowTooLarge = errors.New("history row exceeds logical byte capacity")
15
16var errInvalidHistoryRowID = errors.New("invalid history row ID")
17
18const maxTailPreallocCells = 32 * 1024
19
20// History stores terminal rows in immutable chunks. It is intended to be
21// mutated by the owner of a Screen; views are safe to retain after later
22// appends.
23type History struct {
24 maxRows int
25 rowCeiling int
26 maxBytes uint64
27 byteCeiling uint64
28 chunkRows int
29 chunks []*HistoryChunk
30 tail [][]renderer.Cell
31 cachedTail []*HistoryChunk
32 tailCells []renderer.Cell
33 tailBounds []LineBound
34 tailIDs []RowID
35 tailBytes uint64
36 tailPageWidth int
37 tailPageRows int
38 tailPageStyles map[renderer.Style]struct{}
39 styleScratch map[renderer.Style]struct{}
40 payloadScratch map[renderer.CellPayload]struct{}
41 tailPagePayloads map[renderer.CellPayload]struct{}
42 rows int
43 cells int
44 logicalBytes uint64
45 nextRowID RowID
46 compressionCursor int
47}
48
49// HistoryChunk is an immutable compact slab of equal-width history rows. Its
50// identity is stable and can be used by consumers to reuse unchanged chunks.
51type HistoryChunk struct {
52 page *sealedPage
53 start int
54 count int
55 width int
56 bounds []LineBound
57 rowIDs []RowID
58 styleDrops []uint64
59 styleCount uint64
60 payloadDrops []uint64
61 payloadBytes uint64
62}
63
64func newHistoryChunks(rows [][]renderer.Cell, bounds []LineBound, rowIDs []RowID) []*HistoryChunk {
65 bounds = growBounds(bounds, len(rows))
66 rowIDs = growRowIDs(rowIDs, len(rows))
67 var chunks []*HistoryChunk
68 for start := 0; start < len(rows); {
69 width := len(rows[start])
70 limit := start + min(len(rows)-start, maxHistorySlabRows(width))
71 end := start + 1
72 for end < limit && len(rows[end]) == width {
73 end++
74 }
75 frame := renderer.NewFrame(width, end-start)
76 lastStyleRow := make(map[renderer.Style]int)
77 for i := start; i < end; i++ {
78 frame.WriteRow(i-start, 0, rows[i])
79 for _, cell := range rows[i] {
80 style := cell.Style.Canonical()
81 if style != renderer.DefaultStyle() {
82 lastStyleRow[style] = i - start
83 }
84 }
85 }
86 styleDrops := make([]uint64, end-start)
87 for _, row := range lastStyleRow {
88 styleDrops[row]++
89 }
90 chunks = append(chunks, &HistoryChunk{
91 page: newSealedPage(frame),
92 count: end - start,
93 width: width,
94 bounds: append([]LineBound(nil), bounds[start:end]...),
95 rowIDs: append([]RowID(nil), rowIDs[start:end]...),
96 styleDrops: styleDrops,
97 styleCount: uint64(len(lastStyleRow)) + 1,
98 })
99 chunks[len(chunks)-1].recordPayloads()
100 start = end
101 }
102 return chunks
103}
104
105func maxHistorySlabRows(width int) int {
106 if width <= 0 {
107 return math.MaxInt
108 }
109 rows := min(uint64(math.MaxUint32)/uint64(width), uint64(math.MaxInt))
110 return max(int(rows), 1)
111}
112
113func (c *HistoryChunk) len() int {
114 if c == nil {
115 return 0
116 }
117 return c.count
118}
119
120func (c *HistoryChunk) row(i int) []renderer.Cell {
121 if c == nil || i < 0 || i >= c.count {
122 return nil
123 }
124 return c.frameView().Row(c.start + i)
125}
126
127func (c *HistoryChunk) cell(row, column int) renderer.Cell {
128 return c.frameView().Cell(column, c.start+row)
129}
130
131func (c *HistoryChunk) cells() int { return c.count * c.width }
132
133// CheckInvariants validates one compact immutable history slab.
134func (c *HistoryChunk) CheckInvariants() error {
135 if c == nil || c.page == nil {
136 return fmt.Errorf("nil history chunk backing")
137 }
138 frame, err := c.page.readFrame(true)
139 if err != nil {
140 return err
141 }
142 if c.start < 0 || c.count <= 0 || c.width < 0 || c.start > frame.Height-c.count {
143 return fmt.Errorf("invalid history chunk range start=%d count=%d", c.start, c.count)
144 }
145 if frame.Width != c.width || len(c.bounds) != c.count || len(c.rowIDs) != c.count || len(c.styleDrops) != frame.Height {
146 return fmt.Errorf("history chunk metadata is not parallel")
147 }
148 styles := uint64(1)
149 for _, dropped := range c.styleDrops[c.start:] {
150 styles += dropped
151 }
152 if c.styleCount != styles {
153 return fmt.Errorf("history chunk style count = %d want %d", c.styleCount, styles)
154 }
155 var payloadBytes uint64
156 if len(c.payloadDrops) != 0 {
157 if len(c.payloadDrops) != frame.Height {
158 return fmt.Errorf("history payload metadata is not parallel")
159 }
160 for _, dropped := range c.payloadDrops[c.start:] {
161 payloadBytes += dropped
162 }
163 }
164 if payloadBytes != c.payloadBytes {
165 return fmt.Errorf("history payload accounting mismatch")
166 }
167 if err := frame.CheckInvariants(); err != nil {
168 return fmt.Errorf("history chunk frame: %w", err)
169 }
170 for i, id := range c.rowIDs {
171 if id == 0 || id >= ^RowID(0)-1 {
172 return fmt.Errorf("history row %d has invalid ID %d", i, id)
173 }
174 if !validHistoryBound(c.bounds[i], c.width) {
175 return fmt.Errorf("history row %d has invalid bound", i)
176 }
177 }
178 return nil
179}
180
181// HistoryView is an immutable snapshot of history. Row returns a copy so the
182// storage behind a sealed chunk remains owned by VT.
183type HistoryView struct {
184 chunks []*HistoryChunk
185 rows int
186 cells int
187 logicalBytes uint64
188 nextRowID RowID
189}
190
191// HistorySnapshotView captures sealed history chunks and the mutable tail
192// independently. Sealed chunks are shared by identity; Tail is owned by the
193// view and can be serialized without rotating the live tail into a chunk.
194type HistorySnapshotView struct {
195 chunks []*HistoryChunk
196 tail HistoryView
197 rows int
198 cells int
199 logicalBytes uint64
200 nextRowID RowID
201}
202
203func historyRowBaseLogicalBytes(width int) uint64 {
204 return uint64(width)*renderer.StoredCellLogicalBytes + renderer.RowDescriptorLogicalBytes
205}
206
207func addLogicalBytes(total, value uint64) uint64 {
208 if value > math.MaxUint64-total {
209 return math.MaxUint64
210 }
211 return total + value
212}
213
214func historyChunkLogicalBytes(chunk *HistoryChunk) uint64 {
215 if chunk == nil {
216 return 0
217 }
218 return uint64(chunk.len())*historyRowBaseLogicalBytes(chunk.width) + chunk.styleCount*renderer.StyleRecordLogicalBytes + chunk.payloadBytes
219}
220
221func historyChunksLogicalBytes(chunks []*HistoryChunk) uint64 {
222 var total uint64
223 for _, chunk := range chunks {
224 total = addLogicalBytes(total, historyChunkLogicalBytes(chunk))
225 }
226 return total
227}
228
229func (h *History) prepareRowStyles(row []renderer.Cell) uint64 {
230 clear(h.payloadScratch)
231 if h.styleScratch == nil {
232 h.styleScratch = make(map[renderer.Style]struct{})
233 } else {
234 clear(h.styleScratch)
235 }
236 if len(row) > 0 {
237 firstRaw := row[0].Style
238 same := true
239 for _, cell := range row {
240 h.recordPayloadScratch(cell.Payload)
241 if cell.Style != firstRaw {
242 same = false
243 }
244 }
245 if same {
246 first := firstRaw.Canonical()
247 if first != renderer.DefaultStyle() {
248 h.styleScratch[first] = struct{}{}
249 }
250 } else {
251 for _, cell := range row {
252 style := cell.Style.Canonical()
253 if style != renderer.DefaultStyle() {
254 h.styleScratch[style] = struct{}{}
255 }
256 }
257 }
258 }
259 return historyRowBaseLogicalBytes(len(row)) + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
260}
261
262func (h *History) prepareChunkRowStyles(chunk *HistoryChunk, row int) uint64 {
263 clear(h.payloadScratch)
264 if h.styleScratch == nil {
265 h.styleScratch = make(map[renderer.Style]struct{})
266 } else {
267 clear(h.styleScratch)
268 }
269 for column := range chunk.width {
270 cell := chunk.cell(row, column)
271 h.recordPayloadScratch(cell.Payload)
272 style := cell.Style.Canonical()
273 if style != renderer.DefaultStyle() {
274 h.styleScratch[style] = struct{}{}
275 }
276 }
277 return historyRowBaseLogicalBytes(chunk.width) + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
278}
279
280func (h *History) tailAppendDelta(width int) uint64 {
281 newPage := h.tailPageRows == 0 || h.tailPageWidth != width || h.tailPageRows >= maxHistorySlabRows(width)
282 delta := historyRowBaseLogicalBytes(width)
283 if newPage {
284 return delta + renderer.StyleRecordLogicalBytes + uint64(len(h.styleScratch))*renderer.StyleRecordLogicalBytes + h.rowPayloadBytes()
285 }
286 for style := range h.styleScratch {
287 if _, ok := h.tailPageStyles[style]; !ok {
288 delta += renderer.StyleRecordLogicalBytes
289 }
290 }
291 for p := range h.payloadScratch {
292 if _, ok := h.tailPagePayloads[p]; !ok {
293 delta += p.LogicalBytes()
294 }
295 }
296 return delta
297}
298
299func (h *History) recordTailRowStyles(width int) {
300 if h.tailPageRows == 0 || h.tailPageWidth != width || h.tailPageRows >= maxHistorySlabRows(width) {
301 h.tailPageWidth = width
302 h.tailPageRows = 0
303 h.tailPageStyles = map[renderer.Style]struct{}{renderer.DefaultStyle(): {}}
304 h.tailPagePayloads = nil
305 }
306 for p := range h.payloadScratch {
307 if h.tailPagePayloads == nil {
308 h.tailPagePayloads = make(map[renderer.CellPayload]struct{})
309 }
310 h.tailPagePayloads[p] = struct{}{}
311 }
312 for style := range h.styleScratch {
313 h.tailPageStyles[style] = struct{}{}
314 }
315 h.tailPageRows++
316}
317
318func (h *History) rebuildTailAccounting() {
319 old := h.tailBytes
320 h.tailBytes = 0
321 h.tailPageWidth = 0
322 h.tailPageRows = 0
323 h.tailPageStyles = nil
324 h.tailPagePayloads = nil
325 for _, row := range h.tail {
326 h.prepareRowStyles(row)
327 delta := h.tailAppendDelta(len(row))
328 h.tailBytes += delta
329 h.recordTailRowStyles(len(row))
330 }
331 h.logicalBytes = h.logicalBytes - old + h.tailBytes
332}
333
334// Append records a copy of row along with its logical extent and an
335// automatically allocated nonzero ID. Once a chunk is full it is sealed
336// forever. Rows larger than the logical-byte capacity are rejected without
337// mutation.
338func (h *History) Append(row []renderer.Cell, bound LineBound) error {
339 if h == nil || h.maxRows == 0 {
340 return nil
341 }
342 defer func() { clear(h.payloadScratch) }()
343 if uint64(len(row)) > uint64(math.MaxUint32) || historyRowBaseLogicalBytes(len(row))+renderer.StyleRecordLogicalBytes > h.maxBytes || h.prepareRowStyles(row) > h.maxBytes {
344 return ErrHistoryRowTooLarge
345 }
346 id, err := h.allocateRowID()
347 if err != nil {
348 return err
349 }
350 return h.appendRow(row, bound, id)
351}
352
353// AppendWithID records a copy of row with an explicit persisted identity.
354// Unlike Append, malformed identities are rejected rather than synthesized.
355func (h *History) AppendWithID(row []renderer.Cell, bound LineBound, id RowID) error {
356 if id == 0 || id >= ^RowID(0)-1 {
357 return errInvalidHistoryRowID
358 }
359 if h == nil || h.maxRows == 0 {
360 return nil
361 }
362 defer func() { clear(h.payloadScratch) }()
363 if uint64(len(row)) > uint64(math.MaxUint32) || historyRowBaseLogicalBytes(len(row))+renderer.StyleRecordLogicalBytes > h.maxBytes || h.prepareRowStyles(row) > h.maxBytes {
364 return ErrHistoryRowTooLarge
365 }
366 if h.hasRowID(id) {
367 return errInvalidHistoryRowID
368 }
369 return h.appendRow(row, bound, id)
370}
371
372func (h *History) appendRow(row []renderer.Cell, bound LineBound, id RowID) error {
373 if h.nextRowID == ^RowID(0) {
374 return errInvalidHistoryRowID
375 }
376 if id >= ^RowID(0)-1 {
377 return errInvalidHistoryRowID
378 }
379 if id >= h.nextRowID {
380 h.nextRowID = id + 1
381 }
382 // Make room before adding so accounting cannot overflow even when the
383 // configured capacities use their maximum values.
384 h.evictFor(row)
385 delta := h.tailAppendDelta(len(row))
386 h.appendTailRow(row)
387 h.recordTailRowStyles(len(row))
388 h.tailBytes += delta
389 h.logicalBytes += delta
390 h.tailBounds = append(h.tailBounds, clampBound(bound, len(row)))
391 h.tailIDs = append(h.tailIDs, id)
392 h.rows++
393 h.cells += len(row)
394 if len(h.tail) == h.chunkRows {
395 h.sealTail()
396 }
397 return nil
398}
399
400// allocateRowID relies on appendRow advancing nextRowID above every accepted
401// explicit ID; automatic IDs therefore never need to scan retained rows.
402func (h *History) allocateRowID() (RowID, error) {
403 if h.nextRowID == 0 {
404 h.nextRowID = 1
405 }
406 if h.nextRowID >= ^RowID(0)-1 {
407 return 0, errInvalidHistoryRowID
408 }
409 id := h.nextRowID
410 h.nextRowID++
411 return id, nil
412}
413
414func (h *History) hasRowID(id RowID) bool {
415 for _, chunk := range h.chunks {
416 if slices.Contains(chunk.rowIDs, id) {
417 return true
418 }
419 }
420 return slices.Contains(h.tailIDs, id)
421}
422
423// clampBound keeps End inside the row it describes so a persisted bound can be
424// validated without consulting its row again.
425func clampBound(b LineBound, cells int) LineBound {
426 b.End = min(max(b.End, 0), cells)
427 return b
428}
429
430// growBounds returns bounds sized to rows, padding with zero values. It keeps
431// the parallel-slice invariant when a chunk arrives with fewer bounds than rows.
432func growBounds(bounds []LineBound, rows int) []LineBound {
433 if len(bounds) == rows {
434 return bounds
435 }
436 out := make([]LineBound, rows)
437 copy(out, bounds)
438 return out
439}
440
441func growRowIDs(ids []RowID, rows int) []RowID {
442 if len(ids) == rows {
443 return ids
444 }
445 out := make([]RowID, rows)
446 copy(out, ids)
447 return out
448}
449
450func (h *History) reserveTailRow(width int) (start, end int) {
451 h.cachedTail = nil
452 start = len(h.tailCells)
453 end = start + width
454 if end > cap(h.tailCells) {
455 capacity := end
456 if cap(h.tailCells) <= (math.MaxInt-1)/2 {
457 capacity = max(capacity, cap(h.tailCells)*2)
458 }
459 if len(h.tailCells) == 0 {
460 prealloc := uint64(maxTailPreallocCells)
461 prealloc = min(prealloc, h.maxBytes/renderer.StoredCellLogicalBytes, uint64(width)*uint64(h.chunkRows))
462 capacity = max(capacity, max(width, int(prealloc)))
463 }
464 cells := make([]renderer.Cell, len(h.tailCells), capacity)
465 copy(cells, h.tailCells)
466 h.tailCells = cells
467 offset := 0
468 for i := range h.tail {
469 rowWidth := len(h.tail[i])
470 h.tail[i] = h.tailCells[offset : offset+rowWidth : offset+rowWidth]
471 offset += rowWidth
472 }
473 }
474 h.tailCells = h.tailCells[:end]
475 return start, end
476}
477
478func (h *History) appendTailRow(row []renderer.Cell) {
479 start, end := h.reserveTailRow(len(row))
480 copy(h.tailCells[start:end], row)
481 h.tail = append(h.tail, h.tailCells[start:end:end])
482}
483
484func (h *History) appendTailChunkRow(chunk *HistoryChunk, row int) {
485 start, end := h.reserveTailRow(chunk.width)
486 for x := range chunk.width {
487 h.tailCells[start+x] = chunk.cell(row, x)
488 }
489 h.tail = append(h.tail, h.tailCells[start:end:end])
490}
491
492func (h *History) sealTail() {
493 if len(h.tail) == 0 {
494 return
495 }
496 chunks := h.tailViewChunks()
497 h.chunks = append(h.chunks, chunks...)
498 h.logicalBytes = h.logicalBytes - h.tailBytes + historyChunksLogicalBytes(chunks)
499 h.tail = nil
500 h.cachedTail = nil
501 h.tailCells = nil
502 h.tailBounds = nil
503 h.tailIDs = nil
504 h.tailBytes = 0
505 h.tailPageWidth = 0
506 h.tailPageRows = 0
507 h.tailPageStyles = nil
508 h.tailPagePayloads = nil
509}
510
511// evictFor discards oldest rows until row can fit both retention budgets.
512func (h *History) evictFor(row []renderer.Cell) {
513 for {
514 delta := h.tailAppendDelta(len(row))
515 if h.rows <= h.maxRows-1 && h.logicalBytes <= h.maxBytes-delta {
516 return
517 }
518 h.evictOldest()
519 h.prepareRowStyles(row)
520 }
521}
522
523func (h *History) evictOldest() {
524 if h.rows == 0 {
525 return
526 }
527 if len(h.chunks) > 0 {
528 chunk := h.chunks[0]
529 oldBytes := historyChunkLogicalBytes(chunk)
530 h.rows--
531 h.cells -= chunk.width
532 if chunk.count == 1 {
533 h.logicalBytes -= oldBytes
534 copy(h.chunks, h.chunks[1:])
535 h.chunks[len(h.chunks)-1] = nil
536 h.chunks = h.chunks[:len(h.chunks)-1]
537 } else {
538 // Preserve cell storage while replacing only the chunk wrapper: a
539 // retained view may still refer to the original immutable chunk. The
540 // wrapper tracks only styles still used by its retained suffix.
541 h.chunks[0] = chunk.withoutFirstRow()
542 h.logicalBytes -= oldBytes - historyChunkLogicalBytes(h.chunks[0])
543 }
544 return
545 }
546 if len(h.tail) == 0 {
547 return
548 }
549 h.cachedTail = nil
550 row := h.tail[0]
551 h.rows--
552 h.cells -= len(row)
553 h.tail[0] = nil
554 h.tail = h.tail[1:]
555 h.tailCells = h.tailCells[len(row):]
556 h.tailBounds = growBounds(h.tailBounds, len(h.tail)+1)[1:]
557 h.tailIDs = growRowIDs(h.tailIDs, len(h.tail)+1)[1:]
558 h.rebuildTailAccounting()
559}
560
561// SealAndView rotates the mutable tail into an immutable chunk, then captures
562// the sealed chunks by identity. Callers must synchronize access to History.
563func (h *History) SealAndView() HistoryView {
564 if h != nil {
565 h.sealTail()
566 }
567 return h.View()
568}
569
570func (h *History) tailViewChunks() []*HistoryChunk {
571 if h.cachedTail == nil && len(h.tail) > 0 {
572 h.cachedTail = newHistoryChunks(h.tail, h.tailBounds, h.tailIDs)
573 }
574 return h.cachedTail
575}
576
577// View captures the current history. Sealed chunks are shared by identity;
578// the immutable tail copy is reused until the mutable tail changes.
579func (h *History) View() HistoryView {
580 if h == nil {
581 return HistoryView{}
582 }
583 if h.rows == 0 {
584 return HistoryView{nextRowID: h.nextRowID}
585 }
586 chunks := make([]*HistoryChunk, len(h.chunks), len(h.chunks)+1)
587 copy(chunks, h.chunks)
588 if len(h.tail) > 0 {
589 chunks = append(chunks, h.tailViewChunks()...)
590 }
591 return HistoryView{chunks: chunks, rows: h.rows, cells: h.cells, logicalBytes: h.logicalBytes, nextRowID: h.nextRowID}
592}
593
594// SnapshotView captures history for persistence without sealing the mutable
595// tail. Sealed chunks are shared by identity and the tail is deeply copied.
596func (h *History) SnapshotView() HistorySnapshotView {
597 if h == nil {
598 return HistorySnapshotView{}
599 }
600 if h.rows == 0 {
601 return HistorySnapshotView{nextRowID: h.nextRowID}
602 }
603 tailChunks := newHistoryChunks(h.tail, h.tailBounds, h.tailIDs)
604 return HistorySnapshotView{
605 chunks: append([]*HistoryChunk(nil), h.chunks...),
606 tail: HistoryView{
607 chunks: tailChunks,
608 rows: len(h.tail),
609 cells: historyRowsCells(h.tail),
610 logicalBytes: historyChunksLogicalBytes(tailChunks),
611 nextRowID: h.nextRowID,
612 },
613 rows: h.rows,
614 cells: h.cells,
615 logicalBytes: h.logicalBytes,
616 nextRowID: h.nextRowID,
617 }
618}
619
620// Len returns the currently retained history row count.
621func (h *History) Len() int {
622 if h == nil {
623 return 0
624 }
625 return h.rows
626}
627
628// Cells returns the currently retained history cell count.
629func (h *History) Cells() int {
630 if h == nil {
631 return 0
632 }
633 return h.cells
634}
635
636// LogicalBytes returns deterministic uncompressed bytes retained by history.
637// It excludes the live primary and alternate screens and has no page allowance.
638func (h *History) LogicalBytes() uint64 {
639 if h == nil {
640 return 0
641 }
642 return h.logicalBytes
643}
644
645// NextRowID returns the next identity allocated by this history.
646func (h *History) NextRowID() RowID {
647 if h == nil || h.nextRowID == 0 {
648 return 1
649 }
650 return h.nextRowID
651}
652
653// Cap returns the configured row ceiling; zero means no independent row
654// ceiling, or disabled history when ByteCap is also zero.
655func (h *History) Cap() int {
656 if h == nil {
657 return 0
658 }
659 return h.rowCeiling
660}
661
662// ByteCap returns the configured uncompressed byte ceiling. Zero means no
663// independent byte ceiling, or disabled history when Cap is also zero.
664func (h *History) ByteCap() uint64 {
665 if h == nil {
666 return 0
667 }
668 return h.byteCeiling
669}
670
671func (v HistoryView) Len() int { return v.rows }
672func (v HistoryView) Cells() int { return v.cells }
673func (v HistoryView) LogicalBytes() uint64 {
674 if v.logicalBytes != 0 || len(v.chunks) == 0 {
675 return v.logicalBytes
676 }
677 return historyChunksLogicalBytes(v.chunks)
678}
679func (v HistoryView) ChunkCount() int { return len(v.chunks) }
680
681// NextRowID returns the next identity recorded by this view.
682func (v HistoryView) NextRowID() RowID {
683 if v.nextRowID == 0 {
684 next, ok := historyViewNextRowID(v)
685 if !ok {
686 return 1
687 }
688 return next
689 }
690 return v.nextRowID
691}
692
693// Chunk returns the immutable chunk at i, or nil when i is out of range.
694func (v HistoryView) Chunk(i int) *HistoryChunk {
695 if i < 0 || i >= len(v.chunks) {
696 return nil
697 }
698 return v.chunks[i]
699}
700
701// Range calls yield with a decoded owned row in oldest-first order. Returning
702// false stops iteration successfully; a backing restore failure returns an error.
703func (v HistoryView) Range(yield func([]renderer.Cell) bool) error {
704 for _, chunk := range v.chunks {
705 // Streaming search restores at most one transient page at a time and
706 // does not populate the random-access cache of every cold page.
707 frame, err := chunk.page.readFrame(false)
708 if err != nil {
709 return err
710 }
711 for i := range chunk.len() {
712 if !yield(frame.Row(chunk.start + i)) {
713 return nil
714 }
715 }
716 }
717 return nil
718}
719
720// Row returns a decoded owned copy of the row at i, or nil when out of range.
721func (v HistoryView) Row(i int) []renderer.Cell {
722 chunk, row := v.locateRow(i)
723 if chunk == nil {
724 return nil
725 }
726 return chunk.row(row)
727}
728
729// RowID returns the identity of the row at i, or zero when i is out of range.
730func (v HistoryView) RowID(i int) RowID {
731 if i < 0 {
732 return 0
733 }
734 for _, chunk := range v.chunks {
735 if i < chunk.len() {
736 if i < len(chunk.rowIDs) {
737 return chunk.rowIDs[i]
738 }
739 return 0
740 }
741 i -= chunk.len()
742 }
743 return 0
744}
745
746// FindRowID returns the oldest-first index of id, or -1 when id is absent.
747func (v HistoryView) FindRowID(id RowID) int {
748 if id == 0 {
749 return -1
750 }
751 index := 0
752 for _, chunk := range v.chunks {
753 for row := range chunk.len() {
754 if row < len(chunk.rowIDs) && chunk.rowIDs[row] == id {
755 return index
756 }
757 index++
758 }
759 }
760 return -1
761}
762
763// RowID returns the identity of the row within an immutable chunk.
764func (c *HistoryChunk) RowID(i int) RowID {
765 if c == nil || i < 0 || i >= len(c.rowIDs) {
766 return 0
767 }
768 return c.rowIDs[i]
769}
770
771// Bound returns the logical extent of the row at i, or the zero value when i is
772// out of range. A zero value describes a hard row whose End is not meaningful.
773func (v HistoryView) Bound(i int) LineBound {
774 if i < 0 {
775 return LineBound{}
776 }
777 for _, chunk := range v.chunks {
778 if i < chunk.len() {
779 if i < len(chunk.bounds) {
780 return chunk.bounds[i]
781 }
782 return LineBound{}
783 }
784 i -= chunk.len()
785 }
786 return LineBound{}
787}
788
789func (v HistorySnapshotView) Len() int { return v.rows }
790func (v HistorySnapshotView) Cells() int { return v.cells }
791func (v HistorySnapshotView) LogicalBytes() uint64 {
792 if v.logicalBytes != 0 || v.rows == 0 {
793 return v.logicalBytes
794 }
795 return addLogicalBytes(historyChunksLogicalBytes(v.chunks), v.tail.LogicalBytes())
796}
797func (v HistorySnapshotView) ChunkCount() int { return len(v.chunks) }
798
799// NextRowID returns the next identity recorded by this snapshot.
800func (v HistorySnapshotView) NextRowID() RowID {
801 if v.nextRowID == 0 {
802 return v.Tail().NextRowID()
803 }
804 return v.nextRowID
805}
806
807// Chunk returns a sealed immutable chunk at i, or nil when i is out of range.
808func (v HistorySnapshotView) Chunk(i int) *HistoryChunk {
809 if i < 0 || i >= len(v.chunks) {
810 return nil
811 }
812 return v.chunks[i]
813}
814
815// Tail returns an immutable view of the copied mutable tail.
816func (v HistorySnapshotView) Tail() HistoryView {
817 tail := v.tail
818 tail.nextRowID = v.nextRowID
819 return tail
820}
821
822func historyRowsCells(rows [][]renderer.Cell) int {
823 cells := 0
824 for _, row := range rows {
825 cells += len(row)
826 }
827 return cells
828}