ci/fingerprint/README.md
Centralized fingerprinting and caching system for efficient change detection across the FastLED build system.
The fingerprint cache system has been refactored and centralized in ci/fingerprint/ to provide:
check_needs_update(), mark_success(), invalidate())ci/fingerprint/
├── __init__.py # Package exports
├── README.md # This file
├── core.py # Core cache implementations
├── config.py # Cache configuration and presets
└── rules.py # Invalidation rules and policies
from ci.fingerprint import TwoLayerFingerprintCache
from ci.fingerprint.config import get_cache_config
# Get configuration for C++ linting cache
config = get_cache_config("cpp_lint")
# Create cache instance
cache = TwoLayerFingerprintCache(config.cache_dir, config.name)
# Check if files need processing
file_paths = [Path("src/main.cpp"), Path("src/utils.h")]
if cache.check_needs_update(file_paths):
# Files changed - run linting
run_linting(file_paths)
# Mark success after linting completes
cache.mark_success()
else:
print("No changes detected - skipping linting")
For caches that depend on build mode (quick/debug/release):
from ci.fingerprint.config import get_cache_config
# Get configuration with build mode
config = get_cache_config("cpp_test", build_mode="debug")
# Cache file will be: .cache/fingerprint/cpp_test_debug.json
print(config.get_cache_filename()) # "cpp_test_debug.json"
Efficient two-layer detection (mtime + MD5) for batch file operations.
Use when:
Features:
Example:
from ci.fingerprint import TwoLayerFingerprintCache
cache = TwoLayerFingerprintCache(Path(".cache"), "cpp_lint")
needs_update = cache.check_needs_update(file_paths)
Single SHA256 hash for all files, optimized for concurrent access.
Use when:
Features:
Example:
from ci.fingerprint import HashFingerprintCache
cache = HashFingerprintCache(Path(".cache"), "examples")
needs_update = cache.check_needs_update(file_paths)
Original per-file tracking system. Kept for backward compatibility.
Use when:
Features:
has_changed(path, previous_mtime) APINote: For new code, prefer TwoLayerFingerprintCache.
The config.py module defines standard caches:
| Cache Name | Type | Build Modes | Description |
|---|---|---|---|
cpp_lint | TwoLayer | No | C++ linting (clang-format + custom) |
python_lint | TwoLayer | No | Python type checking (pyright) |
js_lint | TwoLayer | No | JavaScript/TypeScript linting |
cpp_test | Hash | Yes | C++ unit tests |
python_test | Hash | No | Python unit tests |
examples | Hash | Yes | Example compilation |
all | Hash | No | Entire src/ directory |
from ci.fingerprint.config import CacheConfig, CacheType, BuildMode
from pathlib import Path
config = CacheConfig(
name="my_custom_cache",
cache_type=CacheType.TWO_LAYER,
cache_dir=Path(".cache"),
build_mode=BuildMode.DEBUG,
description="My custom cache for XYZ"
)
# Cache file: .cache/fingerprint/my_custom_cache_debug.json
cache_path = config.get_cache_path()
The rules.py module centralizes cache invalidation logic.
Operations are skipped when:
"success"--no-fingerprint flag)Operations run when:
"failure" (retry failed tests)--no-fingerprint)Each cache defines what files it monitors:
from ci.fingerprint.rules import CacheInvalidationRules
# Get monitored files for C++ linting
patterns = CacheInvalidationRules.get_monitored_files_for_cache("cpp_lint")
# Returns: ["src/**/*.cpp", "src/**/*.h", "examples/**/*.ino", ...]
# Get exclude patterns
excludes = CacheInvalidationRules.excludes_for_cache("cpp_lint")
# Returns: [".cache/", ".build/", ".venv/", ...]
All caches follow the safe pattern to avoid race conditions:
# 1. Compute and store fingerprint BEFORE processing
if cache.check_needs_update(file_paths):
# Fingerprint is stored internally
# 2. Run the operation (files may change during this)
run_operation()
# 3. Save the pre-computed fingerprint (immune to changes during step 2)
cache.mark_success()
Why this works:
.pending filemark_success() saves the pre-computed valueBefore:
from ci.util.two_layer_fingerprint_cache import TwoLayerFingerprintCache
from ci.util.hash_fingerprint_cache import HashFingerprintCache
from ci.ci.fingerprint_cache import FingerprintCache
After:
from ci.fingerprint import (
TwoLayerFingerprintCache,
HashFingerprintCache,
FingerprintCache,
)
Before:
cache_dir = Path(".cache")
cache = TwoLayerFingerprintCache(cache_dir, "cpp_lint")
After:
from ci.fingerprint.config import get_cache_config
config = get_cache_config("cpp_lint")
cache = TwoLayerFingerprintCache(config.cache_dir, config.name)
| Operation | Time | Notes |
|---|---|---|
| Cache hit (mtime match) | < 1ms | Fast path - no hashing |
| Fingerprint calculation (src/) | ~2-5ms | TwoLayer or Hash |
| Fingerprint calculation (examples/) | ~10-20ms | More files to process |
| Full Python fingerprint | ~50-100ms | Includes dependency scanning |
Both TwoLayerFingerprintCache and HashFingerprintCache support concurrent access:
LockDatabase for inter-process locking.pending files prevent corruptionSafe for:
Possible causes:
Solutions:
.cache/fingerprint/<name>.json--no-fingerprint to force rebuild and clear statePossible causes:
.cache/ directory not writablemark_success() calledSolutions:
.cache/ exists and is writablemark_success() is called in finally block or error handlerTo add a new cache type:
Add configuration in config.py:
"my_cache": CacheConfig(
name="my_cache",
cache_type=CacheType.TWO_LAYER,
cache_dir=cache_dir,
description="My cache description",
),
Add monitored files in rules.py:
"my_cache": [
"my_dir/**/*.ext",
"config.yaml",
],
Use in code:
from ci.fingerprint.config import get_cache_config
from ci.fingerprint import TwoLayerFingerprintCache
config = get_cache_config("my_cache")
cache = TwoLayerFingerprintCache(config.cache_dir, config.name)
check_needs_update, mark_success, invalidate)