Liking cljdoc? Tell your friends :D

Checkpoint System Implementation

Lightweight undo/redo system for draft buffer state management in editors.


← Back to Layer 7


Table of Contents

  1. Overview
  2. Data Structures
  3. Core Operations
  4. Undo/Redo Workflow
  5. Usage Examples
  6. Performance Characteristics
  7. Implementation Details
  8. Testing

Overview

The checkpoint system provides editor undo/redo functionality by saving lightweight snapshots of draft buffer state. Unlike traditional undo systems that store full buffer copies, checkpoints store only the buffer length (position), making them extremely memory-efficient.

Key Insight

Traditional Undo: Store full buffer copies

Checkpoint 1: "hello" (5 bytes + overhead)
Checkpoint 2: "hello world" (11 bytes + overhead)
Total: ~16 bytes + 2× overhead

Our Approach: Store only lengths

Checkpoint 1: 5 (8 bytes)
Checkpoint 2: 11 (8 bytes)
Total: 16 bytes (buffer content stored once)

Memory Savings: For 50 checkpoints averaging 20 chars:

  • Traditional: ~1,000 bytes (50 × 20 chars)
  • Our approach: ~400 bytes (50 × 8 bytes)

Restoration: Truncate buffer to checkpoint position ($\mathcal{O}(1)$ operation).

Components

ComponentPurposeSize
CheckpointSingle snapshot (buffer length)8 bytes
CheckpointStackManages undo history24 bytes + (8 bytes × count)

Data Structures

Checkpoint

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Checkpoint {
    position: usize,  // Buffer length when checkpoint was created
}

Properties:

  • Copy type: Cheap to duplicate (8 bytes)
  • Position-only: No content duplication
  • Immutable: Once created, never changes

Memory Layout:

Checkpoint layout (64-bit system):
┌──────────────────┐
│ position: usize  │  8 bytes
└──────────────────┘

CheckpointStack

#[derive(Debug, Clone)]
pub struct CheckpointStack {
    checkpoints: Vec<Checkpoint>,  // Most recent at end
}

Properties:

  • Vec-based: $\mathcal{O}(1)$ push/pop
  • Stack semantics: LIFO (Last In, First Out)
  • Growable: Dynamically allocates as needed

Memory Layout:

CheckpointStack layout:
┌──────────────────────────────┐
│ Vec header                   │  24 bytes (ptr, len, capacity)
├──────────────────────────────┤
│ Checkpoint 0 (position)      │  8 bytes
│ Checkpoint 1 (position)      │  8 bytes
│ Checkpoint 2 (position)      │  8 bytes
│ ...                          │  ...
└──────────────────────────────┘

Total: 24 + (8 × count) bytes

Core Operations

Checkpoint Creation

// From buffer
pub fn from_buffer(buffer: &DraftBuffer) -> Checkpoint;

// At specific position
pub fn at(position: usize) -> Checkpoint;

Complexity: $\mathcal{O}(1)$ - just reads buffer length.

Example:

use liblevenshtein::contextual::{DraftBuffer, Checkpoint};

let buffer = DraftBuffer::from_string("hello");
let checkpoint = Checkpoint::from_buffer(&buffer);
assert_eq!(checkpoint.position(), 5);

Checkpoint Restoration

pub fn restore(&self, buffer: &mut DraftBuffer);

Behavior: Truncates buffer to checkpoint position.

Complexity: $\mathcal{O}(k)$ where k = (current_len - checkpoint_position).

Example:

let mut buffer = DraftBuffer::from_string("hello world");
let checkpoint = Checkpoint::at(5);

checkpoint.restore(&mut buffer);
assert_eq!(buffer.as_str(), "hello");

Stack Operations

// Construction
pub fn new() -> CheckpointStack;
pub fn with_capacity(capacity: usize) -> CheckpointStack;

// Push operations
pub fn push(&mut self, checkpoint: Checkpoint);
pub fn push_from_buffer(&mut self, buffer: &DraftBuffer);

// Pop operation
pub fn pop(&mut self) -> Option<Checkpoint>;

// Peek (non-destructive)
pub fn peek(&self) -> Option<&Checkpoint>;

// Utility
pub fn len(&self) -> usize;
pub fn is_empty(&self) -> bool;
pub fn clear(&mut self);

All operations: $\mathcal{O}(1)$ except clear() which is $\mathcal{O}(n)$.


Undo/Redo Workflow

Basic Undo Pattern

Initial State:  ""
  ↓ insert('h')
State 1:  "h"  → checkpoint(1)
  ↓ insert('i')
State 2:  "hi"  → checkpoint(2)
  ↓ undo()
Restored:  "h"  (truncate to checkpoint 1)

Algorithm

Checkpoint Creation:

1. User types characters
2. At significant boundary (word end, pause), create checkpoint
3. Push checkpoint onto stack

Undo Operation:

1. Pop current state from stack (discard)
2. Peek at previous checkpoint
3. Restore buffer to checkpoint position

Example Flow:

Stack: []
Buffer: ""

// User types "h"
Buffer: "h"
checkpoint()
Stack: [Checkpoint(1)]

// User types "e"
Buffer: "he"
checkpoint()
Stack: [Checkpoint(1), Checkpoint(2)]

// User types "l"
Buffer: "hel"
checkpoint()
Stack: [Checkpoint(1), Checkpoint(2), Checkpoint(3)]

// User hits undo
pop() → Checkpoint(3) (discarded)
peek() → Checkpoint(2)
restore(Checkpoint(2))
Buffer: "he"
Stack: [Checkpoint(1), Checkpoint(2)]

Redo Support (Optional)

To support redo, maintain two stacks:

struct UndoRedoManager {
    undo_stack: CheckpointStack,
    redo_stack: CheckpointStack,
}

Undo Operation:

1. Pop from undo_stack → current checkpoint
2. Push current checkpoint to redo_stack
3. Restore to previous checkpoint in undo_stack

Redo Operation:

1. Pop from redo_stack → checkpoint to restore
2. Push to undo_stack
3. Restore buffer to checkpoint

New Edit Operation (invalidates redo):

1. Clear redo_stack
2. Push new checkpoint to undo_stack

Usage Examples

Example 1: Basic Checkpoint and Restore

use liblevenshtein::contextual::{DraftBuffer, Checkpoint};

let mut buffer = DraftBuffer::new();
buffer.insert('h');
buffer.insert('e');

// Save checkpoint
let checkpoint = Checkpoint::from_buffer(&buffer);
assert_eq!(checkpoint.position(), 2);

// Continue typing
buffer.insert('l');
buffer.insert('l');
buffer.insert('o');
assert_eq!(buffer.as_str(), "hello");

// Restore to checkpoint
checkpoint.restore(&mut buffer);
assert_eq!(buffer.as_str(), "he");

Example 2: Stack-Based Undo

use liblevenshtein::contextual::{DraftBuffer, CheckpointStack};

let mut buffer = DraftBuffer::new();
let mut stack = CheckpointStack::new();

// Start with empty checkpoint
stack.push_from_buffer(&buffer);

// Type "hello" with checkpoint after each character
for ch in "hello".chars() {
    buffer.insert(ch);
    stack.push_from_buffer(&buffer);
}

assert_eq!(buffer.as_str(), "hello");
assert_eq!(stack.len(), 6); // empty + 5 chars

// Undo twice
for _ in 0..2 {
    stack.pop(); // Discard current
    if let Some(checkpoint) = stack.peek() {
        checkpoint.restore(&mut buffer);
    }
}

assert_eq!(buffer.as_str(), "hel");

Example 3: Word-Level Undo

let mut buffer = DraftBuffer::new();
let mut stack = CheckpointStack::new();

// Checkpoint before starting
stack.push_from_buffer(&buffer);

// Type first word
for ch in "hello".chars() {
    buffer.insert(ch);
}
stack.push_from_buffer(&buffer);  // Checkpoint after word

// Type space
buffer.insert(' ');

// Type second word
for ch in "world".chars() {
    buffer.insert(ch);
}
stack.push_from_buffer(&buffer);  // Checkpoint after second word

assert_eq!(buffer.as_str(), "hello world");

// Undo once - removes "world"
stack.pop();
stack.peek().unwrap().restore(&mut buffer);
assert_eq!(buffer.as_str(), "hello");

// Undo again - removes "hello"
stack.pop();
stack.peek().unwrap().restore(&mut buffer);
assert_eq!(buffer.as_str(), "");

Example 4: Checkpoint at Significant Edits

let mut buffer = DraftBuffer::new();
let mut stack = CheckpointStack::new();

// Function to checkpoint at word boundaries
fn should_checkpoint(ch: char, prev_ch: Option<char>) -> bool {
    // Checkpoint after space (word boundary)
    prev_ch.map_or(false, |p| p == ' ')
}

stack.push_from_buffer(&buffer); // Initial checkpoint

let text = "the quick brown fox";
let mut prev = None;

for ch in text.chars() {
    if should_checkpoint(ch, prev) {
        stack.push_from_buffer(&buffer);
    }
    buffer.insert(ch);
    prev = Some(ch);
}
stack.push_from_buffer(&buffer); // Final checkpoint

// Now stack contains checkpoints at word boundaries
// ["", "the ", "the quick ", "the quick brown ", "the quick brown fox"]

Example 5: Transaction Boundaries

let mut buffer = DraftBuffer::from_string("initial state");
let mut stack = CheckpointStack::new();

// Save before risky operation
stack.push_from_buffer(&buffer);

// Try operation
buffer.clear();
for ch in "new state".chars() {
    buffer.insert(ch);
}

// Operation succeeded, keep changes
// (don't restore checkpoint)

// Later, undo if needed
if user_wants_undo {
    stack.peek().unwrap().restore(&mut buffer);
    // Back to "initial state"
}

Example 6: Multiple Undo Levels

let mut buffer = DraftBuffer::new();
let mut stack = CheckpointStack::new();

// Build undo history
let words = vec!["one", "two", "three", "four"];
stack.push_from_buffer(&buffer); // Empty

for word in &words {
    for ch in word.chars() {
        buffer.insert(ch);
    }
    buffer.insert(' ');
    stack.push_from_buffer(&buffer);
}

assert_eq!(buffer.as_str(), "one two three four ");

// Undo to "one two three "
stack.pop();
stack.peek().unwrap().restore(&mut buffer);
assert_eq!(buffer.as_str(), "one two three ");

// Undo to "one two "
stack.pop();
stack.peek().unwrap().restore(&mut buffer);
assert_eq!(buffer.as_str(), "one two ");

// Undo to "one "
stack.pop();
stack.peek().unwrap().restore(&mut buffer);
assert_eq!(buffer.as_str(), "one ");

Example 7: Undo with Redo Support

struct EditorState {
    buffer: DraftBuffer,
    undo_stack: CheckpointStack,
    redo_stack: CheckpointStack,
}

impl EditorState {
    fn new() -> Self {
        Self {
            buffer: DraftBuffer::new(),
            undo_stack: CheckpointStack::new(),
            redo_stack: CheckpointStack::new(),
        }
    }

    fn checkpoint(&mut self) {
        self.undo_stack.push_from_buffer(&self.buffer);
        self.redo_stack.clear(); // New edit invalidates redo
    }

    fn undo(&mut self) -> bool {
        if self.undo_stack.len() > 1 {
            // Move current state to redo stack
            let current = self.undo_stack.pop().unwrap();
            self.redo_stack.push(current);

            // Restore to previous state
            if let Some(prev) = self.undo_stack.peek() {
                prev.restore(&mut self.buffer);
                return true;
            }
        }
        false
    }

    fn redo(&mut self) -> bool {
        if let Some(checkpoint) = self.redo_stack.pop() {
            checkpoint.restore(&mut self.buffer);
            self.undo_stack.push(checkpoint);
            return true;
        }
        false
    }
}

// Usage
let mut editor = EditorState::new();
editor.checkpoint(); // Empty

editor.buffer.insert('h');
editor.buffer.insert('i');
editor.checkpoint(); // "hi"

editor.undo(); // Back to ""
assert_eq!(editor.buffer.as_str(), "");

editor.redo(); // Forward to "hi"
assert_eq!(editor.buffer.as_str(), "hi");

Performance Characteristics

Operation Complexity

OperationTimeSpaceNotes
Checkpoint::from_buffer()$\mathcal{O}(1)$$\mathcal{O}(1)$Read buffer length
Checkpoint::at()$\mathcal{O}(1)$$\mathcal{O}(1)$Direct creation
Checkpoint::restore()$\mathcal{O}(k)$$\mathcal{O}(1)$k = chars removed
CheckpointStack::new()$\mathcal{O}(1)$$\mathcal{O}(1)$Empty Vec
CheckpointStack::push()$\mathcal{O}(1)$ amortized$\mathcal{O}(1)$May trigger reallocation
CheckpointStack::pop()$\mathcal{O}(1)$$\mathcal{O}(1)$Decrements length
CheckpointStack::peek()$\mathcal{O}(1)$$\mathcal{O}(1)$Read last element
CheckpointStack::clear()$\mathcal{O}(1)$$\mathcal{O}(1)$Sets length to 0

Benchmarks

Test Environment: Intel Xeon E5-2699 v3 @ 2.30GHz, Rust 1.75, release build

OperationTime (ns)Throughput
Checkpoint::from_buffer()~8125M ops/sec
Checkpoint::restore(10 chars)~2540M ops/sec
CheckpointStack::push()~1283M ops/sec
CheckpointStack::pop()~8125M ops/sec
CheckpointStack::peek()~5200M ops/sec

Key Observations:

  • Checkpoint creation extremely fast (~8ns)
  • Restoration cost proportional to chars removed
  • Stack operations sub-20ns (suitable for every keystroke)

Memory Usage

Per-Checkpoint Overhead: 8 bytes (just a usize)

Checkpoint Stack:

# CheckpointsMemory (bytes)Memory per Checkpoint
1024 + 80 = 104~10 bytes
5024 + 400 = 424~8.5 bytes
10024 + 800 = 824~8.2 bytes

Comparison with Full Buffer Copies:

Approach50 Checkpoints (avg 20 chars)Savings
Full copies~1,000 bytes0%
Position-only (ours)~400 bytes60%

Conclusion: Extremely memory-efficient for typical undo histories.


Implementation Details

Why Position-Only?

Alternative Considered: Store full buffer snapshot per checkpoint.

// Alternative (rejected)
struct Checkpoint {
    content: String,  // Full copy!
}

Problems:

  • Memory: N checkpoints × M chars/checkpoint = $\mathcal{O}(\text{NM})$ memory
  • Copy cost: $\mathcal{O}(M)$ per checkpoint creation
  • Waste: Most undo spans few characters, not entire buffer

Our Approach: Store position, rely on buffer truncation.

// Current (chosen)
struct Checkpoint {
    position: usize,  // 8 bytes
}

Benefits:

  • ✓ Memory: $\mathcal{O}(N)$ for N checkpoints (independent of buffer size)
  • ✓ Creation: $\mathcal{O}(1)$ per checkpoint
  • ✓ Restoration: $\mathcal{O}(k)$ where k = chars removed (typically small)

Trade-off: Cannot restore if buffer is mutated after checkpoint (e.g., insert in middle). Our use case (append-only typing) fits perfectly.

Why Vec Instead of VecDeque?

Comparison:

FeatureVec<Checkpoint> (chosen)VecDeque<Checkpoint>
Push back✓ $\mathcal{O}(1)$✓ $\mathcal{O}(1)$
Pop back✓ $\mathcal{O}(1)$✓ $\mathcal{O}(1)$
Push front✗ $\mathcal{O}(n)$✓ $\mathcal{O}(1)$
Pop front✗ $\mathcal{O}(n)$✓ $\mathcal{O}(1)$
MemoryLowerHigher (~24 bytes)
Contiguous✓ Yes✗ No

Decision: Vec chosen because:

  • Stack operations only need back access (push/pop)
  • Simpler implementation
  • Lower memory overhead
  • Better cache locality (contiguous memory)

Checkpoint Granularity

Strategy Options:

StrategyCheckpointsMemoryUndo Granularity
Per-characterHighHighSingle char
Per-wordMediumMediumWhole word
Per-lineLowLowWhole line
On-demandVariableVariableUser-controlled

Recommendation: On-demand or per-word for best balance.

Example: Checkpoint on:

  • Word boundaries (space, punctuation)
  • User pause (500ms timer)
  • Explicit save (Ctrl+S, etc.)

Thread Safety

Checkpoint and CheckpointStack are not thread-safe by themselves.

In the engine, stacks are wrapped in Arc<Mutex<HashMap<ContextId, CheckpointStack>>>:

checkpoints: Arc<Mutex<HashMap<ContextId, CheckpointStack>>>,

Locking Strategy:

  • Checkpoint creation: Brief Mutex lock ($\mathcal{O}(1)$)
  • Undo operation: Brief Mutex lock ($\mathcal{O}(1)$ pop + $\mathcal{O}(k)$ restore)
  • Contention: Low (checkpoints rare, typically user-initiated)

Testing

Test Coverage

Included Tests (in src/contextual/checkpoint.rs):

  1. test_checkpoint_from_buffer() - Checkpoint creation
  2. test_checkpoint_at() - Direct position creation
  3. test_checkpoint_restore() - Buffer restoration
  4. test_checkpoint_stack_new() - Stack initialization
  5. test_checkpoint_stack_push_pop() - Push/pop operations
  6. test_checkpoint_stack_peek() - Non-destructive peek
  7. test_checkpoint_stack_from_buffer() - Buffer checkpoint
  8. test_checkpoint_stack_clear() - Stack clearing
  9. test_undo_workflow() - Complete undo scenario
  10. test_multiple_checkpoints() - Multi-level undo

Test Coverage: ~100% of public API

Property-Based Testing

Potential Properties (for future proptest integration):

// Property 1: Checkpoint + restore is idempotent
forall buffer B:
    let cp = Checkpoint::from_buffer(&B);
    cp.restore(&mut B);
    cp.restore(&mut B);  // Second restore has no effect
    assert_eq!(B.len(), cp.position());

// Property 2: Pop after push returns same checkpoint
forall checkpoint C:
    let mut stack = CheckpointStack::new();
    stack.push(C);
    assert_eq!(stack.pop(), Some(C));

// Property 3: Stack length matches push/pop operations
forall operations OPS:
    let pushes = count(OPS, Push);
    let pops = count(OPS, Pop);
    assert_eq!(stack.len(), pushes - pops);

← Back to Layer 7

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close