Agent Skills
› lichtblick-suite/lichtblick
› caching-internals
caching-internals
GitHub解析Lichtblick预加载子系统的缓存内部机制,涵盖CachingIterableSource的LRU淘汰策略、内存预算管理及BlockLoader的优先级加载逻辑。
Trigger Scenarios
询问Lichtblick缓存实现细节
分析数据预加载与内存管理策略
排查回放过程中的缓存命中或淘汰问题
Install
npx skills add lichtblick-suite/lichtblick --skill caching-internals -g -y
SKILL.md
Frontmatter
{
"name": "caching-internals",
"description": "Deep implementation details of caching strategies, memory budgets, block eviction, and buffered reading in the Lichtblick preloading subsystem."
}
Caching Internals Skill
CachingIterableSource Implementation
Cache Structure
class CachingIterableSource {
#blocks = new Map<string, CacheBlock>(); // key: serialized range+topics
#totalSize = 0; // current memory usage
#cacheSizeBytes = 600 * 1024 * 1024; // 600MB budget
#maxBlockSize = 50 * 1024 * 1024; // 50MB per block
#accessOrder: string[] = []; // LRU tracking
}
Cache Key Design
- Key combines:
startTime + endTime + sorted topic names - This means the same time range with different topic sets creates separate cache entries
- Allows partial cache hits when subscription set changes
Eviction Algorithm
- When
#totalSize + newBlockSize > #cacheSizeBytes: - Find blocks behind current read position (already consumed)
- Evict LRU blocks until sufficient space is freed
- If no blocks behind read position exist, evict oldest block regardless
Block Lifecycle
EMPTY → LOADING → CACHED → EVICTED
│
└──► ACCESSED (moves to front of LRU)
BufferedIterableSource Implementation
Read-Ahead Configuration
// packages/suite-base/src/players/IterablePlayer/BufferedIterableSource.ts
const DEFAULT_READ_AHEAD_DURATION = { sec: 10, nsec: 0 };
// Overridable per-instance via opt.readAheadDuration; defaults to 10 s.
⚠️ There is no
MCAP_READ_AHEAD_DURATION_SEC = 120(or any 120-second MCAP-specific read-ahead). The only default is{ sec: 10, nsec: 0 }, applied uniformly regardless of source format.
Producer-Consumer Coordination
Producer Thread (may be Worker):
while (not at end && buffer not full) {
message = await source.next();
buffer.push(message);
signal consumer;
}
wait for consumer to drain;
Consumer (IterablePlayer tick loop):
messages = buffer.drain(upToTime);
signal producer to refill;
Backpressure Mechanism
- Producer fills buffer up to
readAheadDurationworth of messages - When buffer is full, producer yields (awaits a drain signal)
- Consumer pulls messages up to its tick budget time
- After consuming, signals producer to resume filling
VecQueue Details
- Backed by a plain array with start/end pointers
push(): appends to end, O(1)drain(predicate): returns all items matching predicate from front, O(n) but bulk operation- Periodic compaction: when start pointer exceeds threshold, shifts array
BlockLoader Implementation
Block Division Strategy
- Total time range divided into N blocks of equal duration
- Block count determined by:
Math.ceil(totalDuration / targetBlockDuration) - Target block duration balances granularity vs overhead
Loading Priority
- Block containing current playback time (immediate need)
- Blocks ahead of current time (upcoming data)
- Blocks behind current time (for seek-back scenarios)
- Never-accessed blocks (lowest priority)
Progress Reporting
interface Progress {
fullyLoadedFractionRanges: Array<{ start: number; end: number }>;
// Fraction 0..1 representing which portions of the time range are cached
}
Memory Optimization Patterns
- Shared ArrayBuffers: Message data stored as
Uint8Arrayviews into shared buffers where possible - Lazy deserialization: Raw bytes cached, deserialized only when consumed
- Topic-scoped loading: Each block only contains data for requested topics
- Size estimation: Block size estimated before full load to prevent over-allocation
Version History
- cab9317 Current 2026-07-24 12:16


