From 83046d2ae0c5d134e4a7318006295b82fa183e8c Mon Sep 17 00:00:00 2001 From: Percy Date: Sun, 3 Aug 2025 00:50:20 -0400 Subject: [PATCH 1/2] Clean up CMakeLists (#23) --- CMakeLists.txt | 128 +++++++------------------------------------------ 1 file changed, 18 insertions(+), 110 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 05bdbc2..1fd03e5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,16 +21,6 @@ set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -# ============================================================================ -set(USER_PREFIX "$ENV{HOME}/local") - -list(APPEND CMAKE_PREFIX_PATH "${USER_PREFIX}") -list(APPEND CMAKE_LIBRARY_PATH "${USER_PREFIX}/lib") -list(APPEND CMAKE_INCLUDE_PATH "${USER_PREFIX}/include") - -include_directories("${USER_PREFIX}/include") -link_directories("${USER_PREFIX}/lib") - # ============================================================================= # Compiler Flags Configuration # ============================================================================= @@ -165,18 +155,6 @@ configure_logging() # Dependency Management # ============================================================================= -# Add user-installed dependencies to search paths -if(DEFINED ENV{CMAKE_PREFIX_PATH}) - list(PREPEND CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH}) -endif() - -# Add common user installation paths -set(USER_PREFIX_PATHS - "$ENV{HOME}/local" - "$ENV{HOME}/.local" - "/usr/local" -) - # Find required packages find_package(Python3 REQUIRED COMPONENTS Interpreter Development.Module) find_package(pybind11 CONFIG REQUIRED) @@ -192,85 +170,23 @@ include_directories(${GLib_INCLUDE_DIRS}) link_directories(${GLib_LIBRARY_DIRS}) list(APPEND required_libs ${GLib_LIBRARIES}) -# ZSTD dependency - try multiple find methods -find_package(ZSTD QUIET) -if(NOT ZSTD_FOUND) - # Try pkg-config - pkg_check_modules(ZSTD_PC QUIET libzstd) - if(ZSTD_PC_FOUND) - set(ZSTD_FOUND TRUE) - set(ZSTD_INCLUDE_DIR ${ZSTD_PC_INCLUDE_DIRS}) - set(ZSTD_LIBRARIES ${ZSTD_PC_LIBRARIES}) - set(ZSTD_LIBRARY_DIRS ${ZSTD_PC_LIBRARY_DIRS}) - else() - # Try manual find - find_path(ZSTD_INCLUDE_DIR zstd.h - PATHS ${CMAKE_INCLUDE_PATH} - PATH_SUFFIXES zstd - ) - find_library(ZSTD_LIBRARIES zstd - PATHS ${CMAKE_LIBRARY_PATH} - ) - if(ZSTD_INCLUDE_DIR AND ZSTD_LIBRARIES) - set(ZSTD_FOUND TRUE) - endif() - endif() -endif() - -if(NOT ZSTD_FOUND) - message(FATAL_ERROR "ZSTD not found. Please install zstd or set CMAKE_PREFIX_PATH to point to user installation.") -endif() - +# ZSTD dependency +find_package(ZSTD REQUIRED) message(STATUS "ZSTD_INCLUDE_DIR: ${ZSTD_INCLUDE_DIR}, ZSTD_LIBRARIES: ${ZSTD_LIBRARIES}") -include_directories(${ZSTD_INCLUDE_DIR}) -if(ZSTD_LIBRARY_DIRS) - link_directories(${ZSTD_LIBRARY_DIRS}) +if("${ZSTD_LIBRARIES}" STREQUAL "") + message(FATAL_ERROR "zstd not found") endif() +include_directories(${ZSTD_INCLUDE_DIR}) +link_directories(${ZSTD_LIBRARY_DIRS}) list(APPEND required_libs ${ZSTD_LIBRARIES}) -# TCMalloc dependency (optional) -find_library(TCMALLOC_LIBRARY tcmalloc - PATHS ${CMAKE_LIBRARY_PATH} -) -if(TCMALLOC_LIBRARY) - list(APPEND optional_libs ${TCMALLOC_LIBRARY}) - message(STATUS "TCMalloc found: ${TCMALLOC_LIBRARY}") - add_compile_definitions(USE_TCMALLOC=1) -else() - message(STATUS "TCMalloc not found, using system malloc") -endif() - # Optional dependencies based on features if(ENABLE_GLCACHE) - # Try to find XGBoost - find_package(xgboost QUIET) - if(NOT xgboost_FOUND) - # Try manual find for user installation - find_path(XGBOOST_INCLUDE_DIR xgboost - PATHS ${CMAKE_INCLUDE_PATH} - ) - find_library(XGBOOST_LIBRARIES xgboost - PATHS ${CMAKE_LIBRARY_PATH} - ) - if(XGBOOST_INCLUDE_DIR AND XGBOOST_LIBRARIES) - set(xgboost_FOUND TRUE) - add_library(xgboost::xgboost UNKNOWN IMPORTED) - set_target_properties(xgboost::xgboost PROPERTIES - IMPORTED_LOCATION ${XGBOOST_LIBRARIES} - INTERFACE_INCLUDE_DIRECTORIES ${XGBOOST_INCLUDE_DIR} - ) - endif() - endif() - - if(xgboost_FOUND) - include_directories(${XGBOOST_INCLUDE_DIR}) - list(APPEND optional_libs xgboost::xgboost) - add_compile_definitions(ENABLE_GLCACHE=1) - message(STATUS "XGBOOST_INCLUDE_DIR: ${XGBOOST_INCLUDE_DIR}") - else() - message(WARNING "XGBoost not found, disabling GLCACHE feature") - set(ENABLE_GLCACHE OFF) - endif() + find_package(xgboost REQUIRED) + include_directories(${XGBOOST_INCLUDE_DIR}) + list(APPEND optional_libs xgboost::xgboost) + add_compile_definitions(ENABLE_GLCACHE=1) + message(STATUS "XGBOOST_INCLUDE_DIR: ${XGBOOST_INCLUDE_DIR}") endif() # LightGBM for LRB and 3L_CACHE @@ -285,30 +201,22 @@ foreach(FEATURE ${LIGHTGBM_FEATURES}) endforeach() if(LIGHTGBM_NEEDED) - # Try to find LightGBM if(NOT DEFINED LIGHTGBM_PATH) - find_path(LIGHTGBM_PATH LightGBM - PATHS ${CMAKE_INCLUDE_PATH} - ) - endif() - - if(NOT DEFINED LIGHTGBM_LIB) - find_library(LIGHTGBM_LIB _lightgbm - PATHS ${CMAKE_LIBRARY_PATH} - ) + find_path(LIGHTGBM_PATH LightGBM) endif() - if(NOT LIGHTGBM_PATH) - message(FATAL_ERROR "LIGHTGBM_PATH not found. Please install LightGBM or set CMAKE_PREFIX_PATH.") + message(FATAL_ERROR "LIGHTGBM_PATH not found") endif() + if(NOT DEFINED LIGHTGBM_LIB) + find_library(LIGHTGBM_LIB _lightgbm) + endif() if(NOT LIGHTGBM_LIB) - message(FATAL_ERROR "LIGHTGBM_LIB not found. Please install LightGBM or set CMAKE_PREFIX_PATH.") + message(FATAL_ERROR "LIGHTGBM_LIB not found") endif() include_directories(${LIGHTGBM_PATH}) list(APPEND optional_libs ${LIGHTGBM_LIB}) - message(STATUS "LightGBM found: ${LIGHTGBM_PATH}, ${LIGHTGBM_LIB}") endif() # ============================================================================= @@ -411,4 +319,4 @@ configure_platform_specific_linking(libcachesim_python) # Installation # ============================================================================= -install(TARGETS libcachesim_python LIBRARY DESTINATION libcachesim) +install(TARGETS libcachesim_python LIBRARY DESTINATION libcachesim) \ No newline at end of file From 544a3d4fcda88110c60013398a7cf964cebb7a35 Mon Sep 17 00:00:00 2001 From: Percy Date: Mon, 4 Aug 2025 02:00:30 -0400 Subject: [PATCH 2/2] [Build] Use CMAKE_ARGS to control optional features (#24) * Use cmake args and update doc * Format with ruff * Remove optional deps in workflow * Run optinal only in cibuild * Fix tests in cibuild --- .github/workflows/build.yml | 4 +- CMakeLists.txt | 6 +- docs/mkdocs.yml | 23 +- docs/src/assets/logos/logo.jpg | Bin 0 -> 137757 bytes docs/src/en/api.md | 394 +------------- docs/src/en/developer.md | 3 + docs/src/en/examples.md | 501 ------------------ docs/src/en/examples/analysis.md | 3 + .../src/en/{plugin.md => examples/plugins.md} | 0 docs/src/en/examples/simulation.md | 3 + docs/src/en/faq.md | 5 + docs/src/en/getting_started/installation.md | 3 + docs/src/en/getting_started/quickstart.md | 205 +++++++ docs/src/en/index.md | 85 +-- docs/src/en/quickstart.md | 183 ------- examples/basic_usage.py | 18 +- examples/plugin_cache/s3fifo.py | 54 +- examples/trace_analysis.py | 32 ++ libcachesim/cache.py | 25 +- libcachesim/synthetic_reader.py | 2 +- libcachesim/trace_reader.py | 2 +- pyproject.toml | 49 +- scripts/detect_deps.py | 65 ++- scripts/smart_build.py | 44 +- tests/reference.csv | 20 - tests/test_cache.py | 313 +++++++---- 26 files changed, 667 insertions(+), 1375 deletions(-) create mode 100644 docs/src/assets/logos/logo.jpg create mode 100644 docs/src/en/developer.md delete mode 100644 docs/src/en/examples.md create mode 100644 docs/src/en/examples/analysis.md rename docs/src/en/{plugin.md => examples/plugins.md} (100%) create mode 100644 docs/src/en/examples/simulation.md create mode 100644 docs/src/en/faq.md create mode 100644 docs/src/en/getting_started/installation.md create mode 100644 docs/src/en/getting_started/quickstart.md delete mode 100644 docs/src/en/quickstart.md create mode 100644 examples/trace_analysis.py delete mode 100644 tests/reference.csv diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bc97a64..af120ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,13 +43,13 @@ jobs: - name: Build and test with uv run: | uv venv --python ${{ matrix.python-version }} - CMAKE_ARGS="-DENABLE_GLCACHE=OFF -DENABLE_LRB=OFF -DENABLE_3L_CACHE=OFF" uv pip install -e .[dev] -vvv + uv pip install -e .[dev] -vvv uv run python -c "import libcachesim; print('✓ Import successful for Python ${{ matrix.python-version }} on ${{ matrix.os }}')" - name: Run tests run: | if [ -d "tests" ]; then - uv run python -m pytest tests/ -v + uv run python -m pytest tests/ -v -m "not optional" else echo "No tests directory found, skipping tests" fi diff --git a/CMakeLists.txt b/CMakeLists.txt index 1fd03e5..3e63c5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,9 +12,9 @@ endif() message(STATUS "CMAKE_BUILD_TYPE: ${CMAKE_BUILD_TYPE}") # Options -option(ENABLE_GLCACHE "Enable group-learned cache" ON) -option(ENABLE_LRB "Enable LRB" ON) -option(ENABLE_3L_CACHE "Enable 3LCache" ON) +option(ENABLE_GLCACHE "Enable group-learned cache" OFF) +option(ENABLE_LRB "Enable LRB" OFF) +option(ENABLE_3L_CACHE "Enable 3LCache" OFF) # C++ standard set(CMAKE_CXX_STANDARD 17) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index f2686f3..481de58 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -9,17 +9,24 @@ docs_dir: src nav: - Home: - libCacheSim Python: index.md - - Getting Started: - - Quick Start: quickstart.md + - Getting Started: + - getting_started/quickstart.md + - getting_started/installation.md + - Examples: + - examples/simulation.md + - examples/analysis.md + - examples/plugins.md + - User Guide: + - FAQ: faq.md + - Developer Guide: + - General: developer.md - API Reference: - API Documentation: api.md - - Examples: - - Usage Examples: examples.md theme: name: material - logo: assets/logos/logo-only-light.ico - favicon: assets/logos/logo-only-light.ico + # logo: assets/logos/logo-only-light.ico + # favicon: assets/logos/logo-only-light.ico language: en palette: # Palette toggle for automatic mode @@ -77,7 +84,9 @@ plugins: build: true nav_translations: Home: 首页 - Quick Start: 快速开始 + Getting Started: 快速开始 + User Guide: 用户指南 + Developer Guide: 开发者指南 API Reference: API参考 Examples: 使用示例 diff --git a/docs/src/assets/logos/logo.jpg b/docs/src/assets/logos/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..779fe2a30642f4a1371be8c5feedd7c7c9f3a151 GIT binary patch literal 137757 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≪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 z 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 literal 0 HcmV?d00001 diff --git a/docs/src/en/api.md b/docs/src/en/api.md index b3c4a68..8c3fc1b 100644 --- a/docs/src/en/api.md +++ b/docs/src/en/api.md @@ -1,395 +1,3 @@ # API Reference -This page provides detailed API documentation for the libCacheSim Python bindings. - -## Core Classes - -### Cache Classes - -All cache classes inherit from the base cache interface and provide the following methods: - -```python -class Cache: - """Base cache interface.""" - - def get(self, obj_id: int, obj_size: int = 1) -> bool: - """Request an object from the cache. - - Args: - obj_id: Object identifier - obj_size: Object size in bytes - - Returns: - True if cache hit, False if cache miss - """ - - def get_hit_ratio(self) -> float: - """Get the current cache hit ratio.""" - - def get_miss_ratio(self) -> float: - """Get the current cache miss ratio.""" - - def get_num_hits(self) -> int: - """Get the total number of cache hits.""" - - def get_num_misses(self) -> int: - """Get the total number of cache misses.""" -``` - -### Available Cache Algorithms - -```python -# Basic algorithms -def LRU(cache_size: int) -> Cache: ... -def LFU(cache_size: int) -> Cache: ... -def FIFO(cache_size: int) -> Cache: ... -def Clock(cache_size: int) -> Cache: ... -def Random(cache_size: int) -> Cache: ... - -# Advanced algorithms -def ARC(cache_size: int) -> Cache: ... -def S3FIFO(cache_size: int) -> Cache: ... -def Sieve(cache_size: int) -> Cache: ... -def TinyLFU(cache_size: int) -> Cache: ... -def TwoQ(cache_size: int) -> Cache: ... -```ence - -This page provides detailed API documentation for libCacheSim Python bindings. - -## Core Classes - -### Cache Classes - -All cache classes inherit from the base cache interface and provide the following methods: - -::: libcachesim.cache - -### TraceReader - -```python -class TraceReader: - """Read trace files in various formats.""" - - def __init__(self, trace_path: str, trace_type: TraceType, - reader_params: ReaderInitParam = None): - """Initialize trace reader. - - Args: - trace_path: Path to trace file - trace_type: Type of trace format - reader_params: Optional reader configuration - """ - - def __iter__(self): - """Iterate over requests in the trace.""" - - def reset(self): - """Reset reader to beginning of trace.""" - - def skip(self, n: int): - """Skip n requests.""" - - def clone(self): - """Create a copy of the reader.""" -``` - -### SyntheticReader - -```python -class SyntheticReader: - """Generate synthetic workloads.""" - - def __init__(self, num_objects: int, num_requests: int, - distribution: str = "zipf", alpha: float = 1.0, - obj_size: int = 1, seed: int = None): - """Initialize synthetic reader. - - Args: - num_objects: Number of unique objects - num_requests: Total requests to generate - distribution: Distribution type ("zipf", "uniform") - alpha: Zipf skewness parameter - obj_size: Object size in bytes - seed: Random seed for reproducibility - """ -``` - -### TraceAnalyzer - -```python -class TraceAnalyzer: - """Analyze trace characteristics.""" - - def __init__(self, trace_path: str, trace_type: TraceType, - reader_params: ReaderInitParam = None): - """Initialize trace analyzer.""" - - def get_num_requests(self) -> int: - """Get total number of requests.""" - - def get_num_objects(self) -> int: - """Get number of unique objects.""" - - def get_working_set_size(self) -> int: - """Get working set size.""" -``` - -## Enumerations and Constants - -### TraceType - -```python -class TraceType: - """Supported trace file formats.""" - CSV_TRACE = "csv" - BINARY_TRACE = "binary" - ORACLE_GENERAL_TRACE = "oracle" - PLAIN_TXT_TRACE = "txt" -``` - -### SamplerType - -```python -class SamplerType: - """Sampling strategies.""" - SPATIAL_SAMPLER = "spatial" - TEMPORAL_SAMPLER = "temporal" -``` - -### ReqOp - -```python -class ReqOp: - """Request operation types.""" - READ = "read" - WRITE = "write" - DELETE = "delete" -``` - -## Data Structures - -### Request - -```python -class Request: - """Represents a cache request.""" - - def __init__(self): - self.obj_id: int = 0 - self.obj_size: int = 1 - self.timestamp: int = 0 - self.op: str = "read" -``` - -### ReaderInitParam - -```python -class ReaderInitParam: - """Configuration parameters for trace readers.""" - - def __init__(self): - self.has_header: bool = False - self.delimiter: str = "," - self.obj_id_is_num: bool = True - self.ignore_obj_size: bool = False - self.ignore_size_zero_req: bool = True - self.cap_at_n_req: int = -1 - self.block_size: int = 4096 - self.trace_start_offset: int = 0 - - # Field mappings (1-indexed) - self.time_field: int = 1 - self.obj_id_field: int = 2 - self.obj_size_field: int = 3 - self.op_field: int = 4 - - self.sampler: Sampler = None -``` - -### Sampler - -```python -class Sampler: - """Configuration for request sampling.""" - - def __init__(self, sample_ratio: float = 1.0, - type: str = "spatial"): - """Initialize sampler. - - Args: - sample_ratio: Fraction of requests to sample (0.0-1.0) - type: Sampling type ("spatial" or "temporal") - """ - self.sample_ratio = sample_ratio - self.type = type -``` - -## Utility Functions - -### Synthetic Trace Generation - -```python -def create_zipf_requests(num_objects, num_requests, alpha, obj_size, seed=None): - """ - Create Zipf-distributed synthetic requests. - - Args: - num_objects (int): Number of unique objects - num_requests (int): Total number of requests to generate - alpha (float): Zipf skewness parameter (higher = more skewed) - obj_size (int): Size of each object in bytes - seed (int, optional): Random seed for reproducibility - - Returns: - List[Request]: List of generated requests - """ - -def create_uniform_requests(num_objects, num_requests, obj_size, seed=None): - """ - Create uniformly-distributed synthetic requests. - - Args: - num_objects (int): Number of unique objects - num_requests (int): Total number of requests to generate - obj_size (int): Size of each object in bytes - seed (int, optional): Random seed for reproducibility - - Returns: - List[Request]: List of generated requests - """ -``` - -### Cache Algorithms - -Available cache algorithms with their factory functions: - -```python -# Basic algorithms -LRU(cache_size: int) -> Cache -LFU(cache_size: int) -> Cache -FIFO(cache_size: int) -> Cache -Clock(cache_size: int) -> Cache -Random(cache_size: int) -> Cache - -# Advanced algorithms -ARC(cache_size: int) -> Cache -S3FIFO(cache_size: int) -> Cache -Sieve(cache_size: int) -> Cache -TinyLFU(cache_size: int) -> Cache -TwoQ(cache_size: int) -> Cache -LRB(cache_size: int) -> Cache - -# Experimental algorithms -cache_3L(cache_size: int) -> Cache -``` - -### Performance Metrics - -```python -class CacheStats: - """Cache performance statistics.""" - - def __init__(self): - self.hits = 0 - self.misses = 0 - self.evictions = 0 - self.bytes_written = 0 - self.bytes_read = 0 - - @property - def hit_ratio(self) -> float: - """Calculate hit ratio.""" - total = self.hits + self.misses - return self.hits / total if total > 0 else 0.0 - - @property - def miss_ratio(self) -> float: - """Calculate miss ratio.""" - return 1.0 - self.hit_ratio -``` - -## Error Handling - -The library uses standard Python exceptions: - -- `ValueError`: Invalid parameters or configuration -- `FileNotFoundError`: Trace file not found -- `RuntimeError`: Runtime errors from underlying C++ library -- `MemoryError`: Out of memory conditions - -Example error handling: - -```python -try: - reader = lcs.TraceReader("nonexistent.csv", lcs.TraceType.CSV_TRACE) -except FileNotFoundError: - print("Trace file not found") -except ValueError as e: - print(f"Invalid configuration: {e}") -``` - -## Configuration Options - -### Reader Configuration - -```python -reader_params = lcs.ReaderInitParam( - has_header=True, # CSV has header row - delimiter=",", # Field delimiter - obj_id_is_num=True, # Object IDs are numeric - ignore_obj_size=False, # Don't ignore object sizes - ignore_size_zero_req=True, # Ignore zero-size requests - cap_at_n_req=1000000, # Limit number of requests - block_size=4096, # Block size for block-based traces - trace_start_offset=0, # Skip initial requests -) - -# Field mappings (1-indexed) -reader_params.time_field = 1 -reader_params.obj_id_field = 2 -reader_params.obj_size_field = 3 -reader_params.op_field = 4 -``` - -### Sampling Configuration - -```python -sampler = lcs.Sampler( - sample_ratio=0.1, # Sample 10% of requests - type=lcs.SamplerType.SPATIAL_SAMPLER # Spatial sampling -) -reader_params.sampler = sampler -``` - -## Thread Safety - -The library provides thread-safe operations for most use cases: - -- Cache operations are thread-safe within a single cache instance -- Multiple readers can be used concurrently -- Analysis operations can utilize multiple threads - -For high-concurrency scenarios, consider using separate cache instances per thread. - -## Memory Management - -The library automatically manages memory for most operations: - -- Cache objects handle their own memory allocation -- Trace readers manage buffering automatically -- Request objects are lightweight and reusable - -For large-scale simulations, monitor memory usage and consider: - -- Using sampling to reduce trace size -- Processing traces in chunks -- Limiting cache sizes appropriately - -## Best Practices - -1. **Use appropriate cache sizes**: Size caches based on your simulation goals -2. **Set random seeds**: For reproducible results in synthetic traces -3. **Handle errors**: Always wrap file operations in try-catch blocks -4. **Monitor memory**: For large traces, consider sampling or chunking -5. **Use threading**: Leverage multi-threading for analysis tasks -6. **Validate traces**: Check trace format and content before simulation +[TBD] \ No newline at end of file diff --git a/docs/src/en/developer.md b/docs/src/en/developer.md new file mode 100644 index 0000000..8fcc019 --- /dev/null +++ b/docs/src/en/developer.md @@ -0,0 +1,3 @@ +# Developer Guide + +[TBD] \ No newline at end of file diff --git a/docs/src/en/examples.md b/docs/src/en/examples.md deleted file mode 100644 index 0d56aa9..0000000 --- a/docs/src/en/examples.md +++ /dev/null @@ -1,501 +0,0 @@ -# Examples - -This page provides practical examples of using libCacheSim Python bindings for various cache simulation scenarios. - -## Basic Cache Simulation - -### Simple LRU Cache Example - -```python -import libcachesim as lcs - -# Create an LRU cache with 1MB capacity -cache = lcs.LRU(cache_size=1024*1024) - -# Generate synthetic Zipf trace -reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0, - num_objects=1000, - seed=42 -) - -# Simulate cache behavior -hits = 0 -total = 0 - -for req in reader: - if cache.get(req): - hits += 1 - total += 1 - -print(f"Hit ratio: {hits/total:.4f}") -print(f"Total requests: {total}") -``` - -### Comparing Multiple Cache Algorithms - -```python -import libcachesim as lcs - -def compare_algorithms(trace_file, cache_size): - """Compare hit ratios of different cache algorithms.""" - - algorithms = { - "LRU": lcs.LRU, - "LFU": lcs.LFU, - "FIFO": lcs.FIFO, - "Clock": lcs.Clock, - "ARC": lcs.ARC, - "S3FIFO": lcs.S3FIFO - } - - results = {} - - for name, cache_class in algorithms.items(): - # Create fresh reader for each algorithm - reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0, - seed=42 # Same seed for fair comparison - ) - - cache = cache_class(cache_size=cache_size) - hits = 0 - - for req in reader: - if cache.get(req): - hits += 1 - - hit_ratio = hits / reader.get_num_of_req() - results[name] = hit_ratio - print(f"{name:8}: {hit_ratio:.4f}") - - return results - -# Compare with 64KB cache -results = compare_algorithms("trace.csv", 64*1024) -``` - -## Working with Real Traces - -### Reading CSV Traces - -```python -import libcachesim as lcs - -def simulate_csv_trace(csv_file): - """Simulate cache behavior on CSV trace.""" - - # Configure CSV reader - reader_params = lcs.ReaderInitParam( - has_header=True, - delimiter=",", - obj_id_is_num=True - ) - - # Set field mappings (1-indexed) - reader_params.time_field = 1 - reader_params.obj_id_field = 2 - reader_params.obj_size_field = 3 - reader_params.op_field = 4 - - reader = lcs.TraceReader( - trace=csv_file, - trace_type=lcs.TraceType.CSV_TRACE, - reader_init_params=reader_params - ) - - print(f"Loaded trace with {reader.get_num_of_req()} requests") - - # Test different cache sizes - cache_sizes = [1024*1024*i for i in [1, 2, 4, 8, 16]] # 1MB to 16MB - - for size in cache_sizes: - cache = lcs.LRU(cache_size=size) - reader.reset() # Reset to beginning - - hits = 0 - for req in reader: - if cache.get(req): - hits += 1 - - hit_ratio = hits / reader.get_num_of_req() - print(f"Cache size: {size//1024//1024}MB, Hit ratio: {hit_ratio:.4f}") - -# Usage -simulate_csv_trace("workload.csv") -``` - -### Handling Large Traces with Sampling - -```python -import libcachesim as lcs - -def analyze_large_trace(trace_file, sample_ratio=0.1): - """Analyze large trace using sampling.""" - - # Create sampler - sampler = lcs.Sampler( - sample_ratio=sample_ratio, - type=lcs.SamplerType.SPATIAL_SAMPLER - ) - - reader_params = lcs.ReaderInitParam( - has_header=True, - delimiter=",", - obj_id_is_num=True - ) - reader_params.sampler = sampler - - reader = lcs.TraceReader( - trace=trace_file, - trace_type=lcs.TraceType.CSV_TRACE, - reader_init_params=reader_params - ) - - print(f"Sampling {sample_ratio*100}% of trace") - print(f"Sampled requests: {reader.get_num_of_req()}") - - # Run simulation on sampled trace - cache = lcs.LRU(cache_size=10*1024*1024) # 10MB - hits = 0 - - for req in reader: - if cache.get(req): - hits += 1 - - hit_ratio = hits / reader.get_num_of_req() - print(f"Hit ratio on sampled trace: {hit_ratio:.4f}") - -# Sample 5% of a large trace -analyze_large_trace("large_trace.csv", sample_ratio=0.05) -``` - -## Advanced Analysis - -### Comprehensive Trace Analysis - -```python -import libcachesim as lcs -import os - -def comprehensive_analysis(trace_file, output_dir="analysis_results"): - """Run comprehensive trace analysis.""" - - # Create output directory - os.makedirs(output_dir, exist_ok=True) - - # Load trace - reader = lcs.TraceReader(trace_file, lcs.TraceType.CSV_TRACE) - - # Run trace analysis - analyzer = lcs.TraceAnalyzer(reader, f"{output_dir}/trace_analysis") - print("Running trace analysis...") - analyzer.run() - - print(f"Analysis complete. Results saved to {output_dir}/") - print("Generated files:") - for file in os.listdir(output_dir): - print(f" - {file}") - -# Run analysis -comprehensive_analysis("workload.csv") -``` - -### Hit Ratio Curves - -```python -import libcachesim as lcs -import matplotlib.pyplot as plt - -def plot_hit_ratio_curve(trace_file, algorithms=None): - """Plot hit ratio curves for different algorithms.""" - - if algorithms is None: - algorithms = ["LRU", "LFU", "FIFO", "ARC"] - - # Cache sizes from 1MB to 100MB - cache_sizes = [1024*1024*i for i in range(1, 101, 5)] - - plt.figure(figsize=(10, 6)) - - for algo_name in algorithms: - hit_ratios = [] - - for cache_size in cache_sizes: - reader = lcs.SyntheticReader( - num_of_req=5000, - obj_size=1024, - dist="zipf", - alpha=1.0, - seed=42 - ) - - cache = getattr(lcs, algo_name)(cache_size=cache_size) - hits = 0 - - for req in reader: - if cache.get(req): - hits += 1 - - hit_ratio = hits / reader.get_num_of_req() - hit_ratios.append(hit_ratio) - - # Convert to MB for plotting - sizes_mb = [size // 1024 // 1024 for size in cache_sizes] - plt.plot(sizes_mb, hit_ratios, label=algo_name, marker='o') - - plt.xlabel('Cache Size (MB)') - plt.ylabel('Hit Ratio') - plt.title('Hit Ratio vs Cache Size') - plt.legend() - plt.grid(True, alpha=0.3) - plt.show() - -# Generate hit ratio curves -plot_hit_ratio_curve("trace.csv") -``` - -## Custom Cache Policies - -### Implementing a Custom LRU with Python Hooks - -```python -import libcachesim as lcs -from collections import OrderedDict - -def create_python_lru(cache_size): - """Create a custom LRU cache using Python hooks.""" - - def init_hook(size): - """Initialize cache data structure.""" - return { - 'data': OrderedDict(), - 'size': 0, - 'capacity': size - } - - def hit_hook(cache_dict, obj_id, obj_size): - """Handle cache hit.""" - # Move to end (most recently used) - cache_dict['data'].move_to_end(obj_id) - - def miss_hook(cache_dict, obj_id, obj_size): - """Handle cache miss.""" - # Add new item - cache_dict['data'][obj_id] = obj_size - cache_dict['size'] += obj_size - - def eviction_hook(cache_dict, obj_id, obj_size): - """Handle eviction when cache is full.""" - # Remove least recently used items - while cache_dict['size'] + obj_size > cache_dict['capacity']: - if not cache_dict['data']: - break - lru_id, lru_size = cache_dict['data'].popitem(last=False) - cache_dict['size'] -= lru_size - - return lcs.PythonHookCache( - cache_size=cache_size, - init_hook=init_hook, - hit_hook=hit_hook, - miss_hook=miss_hook, - eviction_hook=eviction_hook - ) - -# Test custom LRU -custom_cache = create_python_lru(1024*1024) -reader = lcs.SyntheticReader(num_of_req=1000, obj_size=1024) - -hits = 0 -for req in reader: - if custom_cache.get(req): - hits += 1 - -print(f"Custom LRU hit ratio: {hits/1000:.4f}") -``` - -### Time-based Cache with TTL - -```python -import libcachesim as lcs -import time - -def create_ttl_cache(cache_size, ttl_seconds=300): - """Create a cache with time-to-live (TTL) expiration.""" - - def init_hook(size): - return { - 'data': {}, - 'timestamps': {}, - 'size': 0, - 'capacity': size, - 'ttl': ttl_seconds - } - - def is_expired(cache_dict, obj_id): - """Check if object has expired.""" - if obj_id not in cache_dict['timestamps']: - return True - return time.time() - cache_dict['timestamps'][obj_id] > cache_dict['ttl'] - - def hit_hook(cache_dict, obj_id, obj_size): - """Handle cache hit.""" - if is_expired(cache_dict, obj_id): - # Expired, treat as miss - if obj_id in cache_dict['data']: - del cache_dict['data'][obj_id] - del cache_dict['timestamps'][obj_id] - cache_dict['size'] -= obj_size - return False - return True - - def miss_hook(cache_dict, obj_id, obj_size): - """Handle cache miss.""" - current_time = time.time() - cache_dict['data'][obj_id] = obj_size - cache_dict['timestamps'][obj_id] = current_time - cache_dict['size'] += obj_size - - def eviction_hook(cache_dict, obj_id, obj_size): - """Handle eviction.""" - # First try to evict expired items - current_time = time.time() - expired_items = [] - - for oid, timestamp in cache_dict['timestamps'].items(): - if current_time - timestamp > cache_dict['ttl']: - expired_items.append(oid) - - for oid in expired_items: - if oid in cache_dict['data']: - cache_dict['size'] -= cache_dict['data'][oid] - del cache_dict['data'][oid] - del cache_dict['timestamps'][oid] - - # If still need space, evict oldest items - while cache_dict['size'] + obj_size > cache_dict['capacity']: - if not cache_dict['data']: - break - # Find oldest item - oldest_id = min(cache_dict['timestamps'].keys(), - key=lambda x: cache_dict['timestamps'][x]) - cache_dict['size'] -= cache_dict['data'][oldest_id] - del cache_dict['data'][oldest_id] - del cache_dict['timestamps'][oldest_id] - - return lcs.PythonHookCache( - cache_size=cache_size, - init_hook=init_hook, - hit_hook=hit_hook, - miss_hook=miss_hook, - eviction_hook=eviction_hook - ) - -# Test TTL cache -ttl_cache = create_ttl_cache(1024*1024, ttl_seconds=60) -``` - -## Performance Optimization - -### Batch Processing for Large Workloads - -```python -import libcachesim as lcs - -def batch_simulation(trace_file, batch_size=10000): - """Process large traces in batches to optimize memory usage.""" - - reader = lcs.TraceReader(trace_file, lcs.TraceType.CSV_TRACE) - cache = lcs.LRU(cache_size=10*1024*1024) - - total_requests = 0 - total_hits = 0 - batch_count = 0 - - while True: - batch_hits = 0 - batch_requests = 0 - - # Process a batch of requests - for _ in range(batch_size): - try: - req = reader.read_one_req() - if req.valid: - if cache.get(req): - batch_hits += 1 - batch_requests += 1 - else: - break # End of trace - except: - break - - if batch_requests == 0: - break - - total_hits += batch_hits - total_requests += batch_requests - batch_count += 1 - - # Print progress - hit_ratio = batch_hits / batch_requests - print(f"Batch {batch_count}: {batch_requests} requests, " - f"hit ratio: {hit_ratio:.4f}") - - overall_hit_ratio = total_hits / total_requests - print(f"Overall: {total_requests} requests, hit ratio: {overall_hit_ratio:.4f}") - -# Process in batches -batch_simulation("large_trace.csv", batch_size=50000) -``` - -### Multi-threaded Analysis - -```python -import libcachesim as lcs -import concurrent.futures -import threading - -def parallel_cache_comparison(trace_file, algorithms, cache_size): - """Compare cache algorithms in parallel.""" - - def simulate_algorithm(algo_name): - """Simulate single algorithm.""" - reader = lcs.TraceReader(trace_file, lcs.TraceType.CSV_TRACE) - cache = getattr(lcs, algo_name)(cache_size=cache_size) - - hits = 0 - total = 0 - - for req in reader: - if cache.get(req): - hits += 1 - total += 1 - - hit_ratio = hits / total if total > 0 else 0 - return algo_name, hit_ratio - - # Run simulations in parallel - with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: - futures = {executor.submit(simulate_algorithm, algo): algo - for algo in algorithms} - - results = {} - for future in concurrent.futures.as_completed(futures): - algo_name, hit_ratio = future.result() - results[algo_name] = hit_ratio - print(f"{algo_name}: {hit_ratio:.4f}") - - return results - -# Compare algorithms in parallel -algorithms = ["LRU", "LFU", "FIFO", "ARC", "S3FIFO"] -results = parallel_cache_comparison("trace.csv", algorithms, 1024*1024) -``` - -These examples demonstrate the versatility and power of libCacheSim Python bindings for cache simulation, analysis, and research. You can modify and extend these examples for your specific use cases. diff --git a/docs/src/en/examples/analysis.md b/docs/src/en/examples/analysis.md new file mode 100644 index 0000000..ccdcb6f --- /dev/null +++ b/docs/src/en/examples/analysis.md @@ -0,0 +1,3 @@ +# Trace Analysis + +[TBD] \ No newline at end of file diff --git a/docs/src/en/plugin.md b/docs/src/en/examples/plugins.md similarity index 100% rename from docs/src/en/plugin.md rename to docs/src/en/examples/plugins.md diff --git a/docs/src/en/examples/simulation.md b/docs/src/en/examples/simulation.md new file mode 100644 index 0000000..03d5e76 --- /dev/null +++ b/docs/src/en/examples/simulation.md @@ -0,0 +1,3 @@ +# Cache Simulation + +[TBD] \ No newline at end of file diff --git a/docs/src/en/faq.md b/docs/src/en/faq.md new file mode 100644 index 0000000..dd82326 --- /dev/null +++ b/docs/src/en/faq.md @@ -0,0 +1,5 @@ +# Frequently Asked Questions + +1. How to resolve when pip install fails? + +See [installation](https://cachemon.github.io/libCacheSim-python/getting_started/installation/). \ No newline at end of file diff --git a/docs/src/en/getting_started/installation.md b/docs/src/en/getting_started/installation.md new file mode 100644 index 0000000..7e0f4ef --- /dev/null +++ b/docs/src/en/getting_started/installation.md @@ -0,0 +1,3 @@ +# Installation + +[TBD] \ No newline at end of file diff --git a/docs/src/en/getting_started/quickstart.md b/docs/src/en/getting_started/quickstart.md new file mode 100644 index 0000000..b913a9d --- /dev/null +++ b/docs/src/en/getting_started/quickstart.md @@ -0,0 +1,205 @@ +# Quickstart + +This guide will help you get started with libCacheSim. + +## Prerequisites + +- OS: Linux / macOS +- Python: 3.9 -- 3.13 + +## Installation + +You can install libCacheSim using [pip](https://pypi.org/project/libcachesim/) directly. + +It's recommended to use [uv](https://docs.astral.sh/uv/), a very fast Python environment manager, to create and manage Python environments. Please follow the [documentation](https://docs.astral.sh/uv/#getting-started) to install `uv`. After installing `uv`, you can create a new Python environment and install libCacheSim using the following commands: + +```bash +uv venv --python 3.12 --seed +source .venv/bin/activate +uv pip install libcachesim +``` + +For users who want to run LRB, ThreeLCache, and GLCache eviction algorithms: + +!!! important + if `uv` cannot find built wheels for your machine, the building system will skip these algorithms by default. + +To enable them, you need to install all third-party dependencies first. + +!!! note + To install all dependencies, you can use these scripts provided. + ```bash + git clone https://github.com/cacheMon/libCacheSim-python.git + cd libCacheSim-python + bash scripts/install_deps.sh + + # If you cannot install software directly (e.g., no sudo access) + bash scripts/install_deps_user.sh + ``` + +Then, you can reinstall libcachesim using the following commands: + +```bash +# Enable LRB +CMAKE_ARGS="-DENABLE_LRB=ON" uv pip install libcachesim +# Enable ThreeLCache +CMAKE_ARGS="-DENABLE_3L_CACHE=ON" uv pip install libcachesim +# Enable GLCache +CMAKE_ARGS="-DENABLE_GLCACHE=ON" uv pip install libcachesim +``` + +## Cache Simulation + +With libcachesim installed, you can start cache simulation for some eviction algorithm and cache traces. See the example script: + +??? code + ```python + import libcachesim as lcs + + # Step 1: Get one trace from S3 bucket + URI = "cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" + dl = lcs.DataLoader() + dl.load(URI) + + # Step 2: Open trace and process efficiently + reader = lcs.TraceReader( + trace = dl.get_cache_path(URI), + trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) + ) + + # Step 3: Initialize cache + cache = lcs.S3FIFO(cache_size=1024*1024) + + # Step 4: Process entire trace efficiently (C++ backend) + obj_miss_ratio, byte_miss_ratio = cache.process_trace(reader) + print(f"Object miss ratio: {obj_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") + + # Step 4.1: Process with limited number of requests + cache = lcs.S3FIFO(cache_size=1024*1024) + obj_miss_ratio, byte_miss_ratio = cache.process_trace( + reader, + start_req=0, + max_req=1000 + ) + print(f"Object miss ratio: {obj_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") + ``` + +The above example demonstrates the basic workflow of using `libcachesim` for cache simulation: + +1. Use `DataLoader` to download a cache trace file from an S3 bucket. +2. Open and efficiently process the trace file with `TraceReader`. +3. Initialize a cache object (here, `S3FIFO`) with a specified cache size (e.g., 1MB). +4. Run the simulation on the entire trace using `process_trace` to obtain object and byte miss ratios. +5. Optionally, process only a portion of the trace by specifying `start_req` and `max_req` for partial simulation. + +This workflow applies to most cache algorithms and trace types, making it easy to get started and customize your experiments. + +## Trace Analysis + +Here is an example demonstrating how to use `TraceAnalyzer`. + +??? code + ```python + import libcachesim as lcs + + # Step 1: Get one trace from S3 bucket + URI = "cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" + dl = lcs.DataLoader() + dl.load(URI) + + reader = lcs.TraceReader( + trace = dl.get_cache_path(URI), + trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) + ) + + analysis_option = lcs.AnalysisOption( + req_rate=True, # Keep basic request rate analysis + access_pattern=False, # Disable access pattern analysis + size=True, # Keep size analysis + reuse=False, # Disable reuse analysis for small datasets + popularity=False, # Disable popularity analysis for small datasets (< 200 objects) + ttl=False, # Disable TTL analysis + popularity_decay=False, # Disable popularity decay analysis + lifetime=False, # Disable lifetime analysis + create_future_reuse_ccdf=False, # Disable experimental features + prob_at_age=False, # Disable experimental features + size_change=False, # Disable size change analysis + ) + + analysis_param = lcs.AnalysisParam() + + analyzer = lcs.TraceAnalyzer( + reader, "example_analysis", analysis_option=analysis_option, analysis_param=analysis_param + ) + + analyzer.run() + ``` + +The above code demonstrates how to perform trace analysis using `libcachesim`. The workflow is as follows: + +1. Download a trace file from an S3 bucket using `DataLoader`. +2. Open the trace file with `TraceReader`, specifying the trace type and any reader initialization parameters. +3. Configure the analysis options with `AnalysisOption` to enable or disable specific analyses (such as request rate, size, etc.). +4. Optionally, set additional analysis parameters with `AnalysisParam`. +5. Create a `TraceAnalyzer` object with the reader, output directory, and the chosen options and parameters. +6. Run the analysis with `analyzer.run()`. + +After running, you can access the analysis results, such as summary statistics (`stat`) or detailed results (e.g., `example_analysis.size`). + +## Plugin System + +libCacheSim also allows user to develop their own cache eviction algorithms and test them via the plugin system. + +Here is an example of implement `LRU` via the plugin system. + +??? code + ```python + from collections import OrderedDict + from typing import Any + + from libcachesim import PluginCache, LRU, CommonCacheParams, Request + + def init_hook(_: CommonCacheParams) -> Any: + return OrderedDict() + + def hit_hook(data: Any, req: Request) -> None: + data.move_to_end(req.obj_id, last=True) + + def miss_hook(data: Any, req: Request) -> None: + data.__setitem__(req.obj_id, req.obj_size) + + def eviction_hook(data: Any, _: Request) -> int: + return data.popitem(last=False)[0] + + def remove_hook(data: Any, obj_id: int) -> None: + data.pop(obj_id, None) + + def free_hook(data: Any) -> None: + data.clear() + + + plugin_lru_cache = PluginCache( + cache_size=128, + cache_init_hook=init_hook, + cache_hit_hook=hit_hook, + cache_miss_hook=miss_hook, + cache_eviction_hook=eviction_hook, + cache_remove_hook=remove_hook, + cache_free_hook=free_hook, + cache_name="Plugin_LRU", + ) + + reader = lcs.SyntheticReader(num_objects=1000, num_of_req=10000, obj_size=1) + req_miss_ratio, byte_miss_ratio = plugin_lru_cache.process_trace(reader) + ref_req_miss_ratio, ref_byte_miss_ratio = LRU(128).process_trace(reader) + print(f"plugin req miss ratio {req_miss_ratio}, ref req miss ratio {ref_req_miss_ratio}") + print(f"plugin byte miss ratio {byte_miss_ratio}, ref byte miss ratio {ref_byte_miss_ratio}") + ``` + +By defining custom hook functions for cache initialization, hit, miss, eviction, removal, and cleanup, users can easily prototype and test their own cache eviction algorithms. + + + + diff --git a/docs/src/en/index.md b/docs/src/en/index.md index 2eba51f..fbf84ae 100644 --- a/docs/src/en/index.md +++ b/docs/src/en/index.md @@ -1,68 +1,35 @@ -# libCacheSim Python Bindings +# Welcome to libCacheSim Python -Welcome to libCacheSim Python bindings! This is a high-performance cache simulation library with Python interface. +!!! note + For convenience, we refer to the *libCacheSim Python Package* (this repo) as *libCacheSim* and the *C library* as *libCacheSim lib* in the following documentation. -## Overview +

+ ![](../assets/logos/logo.jpg){ align="center" alt="libCacheSim Light" class="logo-light" width="60%" } +
-libCacheSim is a high-performance cache simulation framework that supports various cache algorithms and trace formats. The Python bindings provide an easy-to-use interface for cache simulation, analysis, and research. +

+A high-performance library for building and running cache simulations + +

-## Key Features +

+ +Star +Watch +Fork +

-- **High Performance**: Built on top of the optimized C++ libCacheSim library -- **Multiple Cache Algorithms**: Support for LRU, LFU, FIFO, ARC, Clock, S3FIFO, Sieve, and many more -- **Trace Support**: Read various trace formats (CSV, binary, OracleGeneral, etc.) -- **Synthetic Traces**: Generate synthetic workloads with Zipf and uniform distributions -- **Analysis Tools**: Built-in trace analysis and cache performance evaluation -- **Easy Integration**: Simple Python API for research and production use +libCacheSim is an easy-to-use python binding of [libCachesim lib](https://github.com/1a1a11a/libCacheSim) for building and running cache simulations. -## Quick Example +libCacheSim is fast with the features from [underlying libCacheSim lib](https://github.com/1a1a11a/libCacheSim): -```python -import libcachesim as lcs +- High performance - over 20M requests/sec for a realistic trace replay. +- High memory efficiency - predictable and small memory footprint. +- Parallelism out-of-the-box - uses the many CPU cores to speed up trace analysis and cache simulations. -# Create a cache -cache = lcs.LRU(cache_size=1024*1024) # 1MB cache +libCacheSim is flexible and easy to use with: -# Generate synthetic trace -reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0 -) - -# Simulate cache behavior -hit_count = 0 -for req in reader: - if cache.get(req): - hit_count += 1 - -hit_ratio = hit_count / reader.get_num_of_req() -print(f"Hit ratio: {hit_ratio:.4f}") -``` - -## Installation - -```bash -pip install libcachesim -``` - -Or install from source: - -```bash -git clone https://github.com/cacheMon/libCacheSim-python.git -cd libCacheSim-python -pip install -e . -``` - -## Getting Started - -Check out our [Quick Start Guide](quickstart.md) to begin using libCacheSim Python bindings, or explore the [API Reference](api.md) for detailed documentation. - -## Contributing - -We welcome contributions! Please see our [GitHub repository](https://github.com/cacheMon/libCacheSim-python) for more information. - -## License - -This project is licensed under the GPL-3.0 License. +- Seamless integration with [open-source cache dataset](https://github.com/cacheMon/cache_dataset) consisting of thousands traces hosted on S3. +- High-throughput simulation with the [underlying libCacheSim lib](https://github.com/1a1a11a/libCacheSim) +- Detailed cache requests and other internal data control +- Customized plugin cache development without any compilation \ No newline at end of file diff --git a/docs/src/en/quickstart.md b/docs/src/en/quickstart.md deleted file mode 100644 index 2e32f4d..0000000 --- a/docs/src/en/quickstart.md +++ /dev/null @@ -1,183 +0,0 @@ -# Quick Start Guide - -This guide will help you get started with libCacheSim Python bindings. - -## Installation - -### From PyPI (Recommended) - -```bash -pip install libcachesim -``` - -### From Source - -```bash -git clone https://github.com/cacheMon/libCacheSim-python.git -cd libCacheSim-python -git submodule update --init --recursive -pip install -e . -``` - -## Basic Usage - -### 1. Creating a Cache - -```python -import libcachesim as lcs - -# Create different types of caches -lru_cache = lcs.LRU(cache_size=1024*1024) # 1MB LRU cache -lfu_cache = lcs.LFU(cache_size=1024*1024) # 1MB LFU cache -fifo_cache = lcs.FIFO(cache_size=1024*1024) # 1MB FIFO cache -``` - -### 2. Using Synthetic Traces - -```python -# Generate Zipf-distributed requests -reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0, - num_objects=1000, - seed=42 -) - -# Simulate cache behavior -cache = lcs.LRU(cache_size=50*1024) -hit_count = 0 - -for req in reader: - if cache.get(req): - hit_count += 1 - -print(f"Hit ratio: {hit_count/reader.get_num_of_req():.4f}") -``` - -### 3. Reading Real Traces - -```python -# Read CSV trace -reader = lcs.TraceReader( - trace="path/to/trace.csv", - trace_type=lcs.TraceType.CSV_TRACE, - has_header=True, - delimiter=",", - obj_id_is_num=True -) - -# Process requests -cache = lcs.LRU(cache_size=1024*1024) -for req in reader: - result = cache.get(req) - # Process result... -``` - -### 4. Cache Performance Analysis - -```python -# Run comprehensive analysis -analyzer = lcs.TraceAnalyzer(reader, "output_prefix") -analyzer.run() - -# This generates various analysis files: -# - Hit ratio curves -# - Access pattern analysis -# - Temporal locality analysis -# - And more... -``` - -## Available Cache Algorithms - -libCacheSim supports numerous cache algorithms: - -### Basic Algorithms -- **LRU**: Least Recently Used -- **LFU**: Least Frequently Used -- **FIFO**: First In, First Out -- **Clock**: Clock algorithm -- **Random**: Random replacement - -### Advanced Algorithms -- **ARC**: Adaptive Replacement Cache -- **S3FIFO**: Simple, Fast, Fair FIFO -- **Sieve**: Sieve eviction algorithm -- **TinyLFU**: Tiny LFU with admission control -- **TwoQ**: Two-Queue algorithm -- **LRB**: Learning Relaxed Belady - -### Experimental Algorithms -- **3LCache**: Three-Level Cache -- **And many more...** - -## Trace Formats - -Supported trace formats include: - -- **CSV**: Comma-separated values -- **Binary**: Custom binary format -- **OracleGeneral**: Oracle general format -- **Vscsi**: VMware vSCSI format -- **And more...** - -## Advanced Features - -### Custom Cache Policies - -You can implement custom cache policies using Python hooks: - -```python -from collections import OrderedDict - -def create_custom_lru(): - def init_hook(cache_size): - return OrderedDict() - - def hit_hook(cache_dict, obj_id, obj_size): - cache_dict.move_to_end(obj_id) - - def miss_hook(cache_dict, obj_id, obj_size): - cache_dict[obj_id] = obj_size - - def eviction_hook(cache_dict, obj_id, obj_size): - if cache_dict: - cache_dict.popitem(last=False) - - return lcs.PythonHookCache( - cache_size=1024*1024, - init_hook=init_hook, - hit_hook=hit_hook, - miss_hook=miss_hook, - eviction_hook=eviction_hook - ) - -custom_cache = create_custom_lru() -``` - -### Trace Sampling - -```python -# Sample 10% of requests spatially -reader = lcs.TraceReader( - trace="large_trace.csv", - trace_type=lcs.TraceType.CSV_TRACE, - sampling_ratio=0.1, - sampling_type=lcs.SamplerType.SPATIAL_SAMPLER -) -``` - -### Multi-threaded Analysis - -```python -# Use multiple threads for analysis -analyzer = lcs.TraceAnalyzer(reader, "output", n_threads=4) -analyzer.run() -``` - -## Next Steps - -- Explore the [API Reference](api.md) for detailed documentation -- Check out [Examples](examples.md) for more complex use cases -- Visit our [GitHub repository](https://github.com/cacheMon/libCacheSim-python) for source code and issues diff --git a/examples/basic_usage.py b/examples/basic_usage.py index e8dd208..2a4bd60 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -7,23 +7,19 @@ # Step 2: Open trace and process efficiently reader = lcs.TraceReader( - trace = dl.get_cache_path(URI), - trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, - reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) + trace=dl.get_cache_path(URI), + trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params=lcs.ReaderInitParam(ignore_obj_size=False), ) # Step 3: Initialize cache -cache = lcs.S3FIFO(cache_size=1024*1024) +cache = lcs.S3FIFO(cache_size=1024 * 1024) # Step 4: Process entire trace efficiently (C++ backend) obj_miss_ratio, byte_miss_ratio = cache.process_trace(reader) print(f"Object miss ratio: {obj_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") # Step 4.1: Process with limited number of requests -cache = lcs.S3FIFO(cache_size=1024*1024) -obj_miss_ratio, byte_miss_ratio = cache.process_trace( - reader, - start_req=0, - max_req=1000 -) -print(f"Object miss ratio: {obj_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") \ No newline at end of file +cache = lcs.S3FIFO(cache_size=1024 * 1024) +obj_miss_ratio, byte_miss_ratio = cache.process_trace(reader, start_req=0, max_req=1000) +print(f"Object miss ratio: {obj_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") diff --git a/examples/plugin_cache/s3fifo.py b/examples/plugin_cache/s3fifo.py index 1207e23..aa1fcdf 100644 --- a/examples/plugin_cache/s3fifo.py +++ b/examples/plugin_cache/s3fifo.py @@ -8,13 +8,16 @@ from collections import deque from libcachesim import PluginCache, CommonCacheParams, Request, S3FIFO, FIFO, SyntheticReader + # NOTE(haocheng): we only support ignore object size for now class StandaloneS3FIFO: - def __init__(self, - small_size_ratio: float = 0.1, - ghost_size_ratio: float = 0.9, - move_to_main_threshold: int = 2, - cache_size: int = 1024): + def __init__( + self, + small_size_ratio: float = 0.1, + ghost_size_ratio: float = 0.9, + move_to_main_threshold: int = 2, + cache_size: int = 1024, + ): self.cache_size = cache_size small_fifo_size = int(small_size_ratio * cache_size) main_fifo_size = cache_size - small_fifo_size @@ -27,15 +30,15 @@ def __init__(self, self.small_fifo = FIFO(small_fifo_size) self.main_fifo = FIFO(main_fifo_size) self.ghost_fifo = FIFO(ghost_fifo_size) - + # Frequency tracking self.freq = {} - + # Other parameters self.max_freq = 3 self.move_to_main_threshold = move_to_main_threshold - self.has_evicted = False # Mark if we start to evict, only after full we will start eviction + self.has_evicted = False # Mark if we start to evict, only after full we will start eviction self.hit_on_ghost = False def cache_hit(self, req: Request): @@ -46,7 +49,7 @@ def cache_hit(self, req: Request): if self.main_fifo.find(req, update_cache=False): self.freq[req.obj_id] += 1 - + def cache_miss(self, req: Request): if not self.hit_on_ghost: obj = self.ghost_fifo.find(req, update_cache=False) @@ -56,14 +59,13 @@ def cache_miss(self, req: Request): self.ghost_fifo.remove(req.obj_id) self.ghost_set.remove(req.obj_id) - # NOTE(haocheng): first we need to know this miss object has record in ghost or not if not self.hit_on_ghost: if req.obj_size >= self.small_fifo.cache_size: # If object is too large, we do not process it return - # If is initialization state, we need to insert to small fifo, + # If is initialization state, we need to insert to small fifo, # then we can insert to main fifo if not self.has_evicted and self.small_fifo.get_occupied_byte() >= self.small_fifo.cache_size: obj = self.main_fifo.insert(req) @@ -76,7 +78,7 @@ def cache_miss(self, req: Request): self.main_set.add(req.obj_id) self.hit_on_ghost = False self.freq[obj.obj_id] = 0 - + def cache_evict_small(self, req: Request): has_evicted = False evicted_id = None @@ -100,7 +102,7 @@ def cache_evict_small(self, req: Request): self.small_set.remove(evicted_id) assert flag, "Should be able to remove" return real_evicted_id - + def cache_evict_main(self, req: Request): has_evicted = False evicted_id = None @@ -134,15 +136,15 @@ def cache_evict(self, req: Request): self.ghost_set.remove(req.obj_id) self.has_evicted = True - cond = (self.main_fifo.get_occupied_byte() > self.main_fifo.cache_size) - if (cond or (self.small_fifo.get_occupied_byte() == 0)): + cond = self.main_fifo.get_occupied_byte() > self.main_fifo.cache_size + if cond or (self.small_fifo.get_occupied_byte() == 0): obj_id = self.cache_evict_main(req) else: obj_id = self.cache_evict_small(req) if obj_id is not None: del self.freq[obj_id] - + return obj_id def cache_remove(self, obj_id): @@ -151,28 +153,35 @@ def cache_remove(self, obj_id): removed |= self.ghost_fifo.remove(obj_id) removed |= self.main_fifo.remove(obj_id) return removed - + + def cache_init_hook(common_cache_params: CommonCacheParams): return StandaloneS3FIFO(cache_size=common_cache_params.cache_size) + def cache_hit_hook(cache, request: Request): cache.cache_hit(request) + def cache_miss_hook(cache, request: Request): cache.cache_miss(request) + def cache_eviction_hook(cache, request: Request): evicted_id = None while evicted_id is None: evicted_id = cache.cache_evict(request) return evicted_id + def cache_remove_hook(cache, obj_id): cache.cache_remove(obj_id) + def cache_free_hook(cache): pass + cache = PluginCache( cache_size=1024, cache_init_hook=cache_init_hook, @@ -181,7 +190,8 @@ def cache_free_hook(cache): cache_eviction_hook=cache_eviction_hook, cache_remove_hook=cache_remove_hook, cache_free_hook=cache_free_hook, - cache_name="S3FIFO") + cache_name="S3FIFO", +) URI = "cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" dl = lcs.DataLoader() @@ -189,9 +199,9 @@ def cache_free_hook(cache): # Step 2: Open trace and process efficiently reader = lcs.TraceReader( - trace = dl.get_cache_path(URI), - trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, - reader_init_params = lcs.ReaderInitParam(ignore_obj_size=True) + trace=dl.get_cache_path(URI), + trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params=lcs.ReaderInitParam(ignore_obj_size=True), ) ref_s3fifo = S3FIFO(cache_size=1024, small_size_ratio=0.1, ghost_size_ratio=0.9, move_to_main_threshold=2) @@ -208,4 +218,4 @@ def cache_free_hook(cache): assert req_miss_ratio == ref_req_miss_ratio assert byte_miss_ratio == ref_byte_miss_ratio -print("All requests processed successfully. Plugin cache matches reference S3FIFO cache.") \ No newline at end of file +print("All requests processed successfully. Plugin cache matches reference S3FIFO cache.") diff --git a/examples/trace_analysis.py b/examples/trace_analysis.py new file mode 100644 index 0000000..0318171 --- /dev/null +++ b/examples/trace_analysis.py @@ -0,0 +1,32 @@ +import libcachesim as lcs + +# Step 1: Get one trace from S3 bucket +URI = "cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +dl = lcs.DataLoader() +dl.load(URI) + +reader = lcs.TraceReader( + trace=dl.get_cache_path(URI), + trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params=lcs.ReaderInitParam(ignore_obj_size=False), +) + +analysis_option = lcs.AnalysisOption( + req_rate=True, # Keep basic request rate analysis + access_pattern=False, # Disable access pattern analysis + size=True, # Keep size analysis + reuse=False, # Disable reuse analysis for small datasets + popularity=False, # Disable popularity analysis for small datasets (< 200 objects) + ttl=False, # Disable TTL analysis + popularity_decay=False, # Disable popularity decay analysis + lifetime=False, # Disable lifetime analysis + create_future_reuse_ccdf=False, # Disable experimental features + prob_at_age=False, # Disable experimental features + size_change=False, # Disable size change analysis +) + +analysis_param = lcs.AnalysisParam() + +analyzer = lcs.TraceAnalyzer(reader, "example_analysis", analysis_option=analysis_option, analysis_param=analysis_param) + +analyzer.run() diff --git a/libcachesim/cache.py b/libcachesim/cache.py index b61a512..94087e9 100644 --- a/libcachesim/cache.py +++ b/libcachesim/cache.py @@ -284,6 +284,7 @@ def __init__( def insert(self, req: Request) -> Optional[CacheObject]: return super().insert(req) + class TwoQ(CacheBase): """2Q replacement algorithm @@ -454,18 +455,24 @@ def __init__( class LRUProb(CacheBase): """LRU with Probabilistic Replacement - + Special parameters: prob: probability of promoting an object to the head of the queue (default: 0.5) """ def __init__( - self, cache_size: int, default_ttl: int = 86400 * 300, hashpower: int = 24, consider_obj_metadata: bool = False, + self, + cache_size: int, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, prob: float = 0.5, ): cache_specific_params = f"prob={prob}" super().__init__( - _cache=LRU_Prob_init(_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata), cache_specific_params) + _cache=LRU_Prob_init( + _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata), cache_specific_params + ) ) @@ -551,7 +558,9 @@ def __init__( try: from .libcachesim_python import ThreeLCache_init except ImportError: - raise ImportError("ThreeLCache is not installed. Please install it with `pip install libcachesim[all]`") + raise ImportError( + 'ThreeLCache is not installed. Please install it with `CMAKE_ARGS="-DENABLE_3L_CACHE=ON" pip install libcachesim --force-reinstall`' + ) cache_specific_params = f"objective={objective}" super().__init__( @@ -592,7 +601,9 @@ def __init__( try: from .libcachesim_python import GLCache_init except ImportError: - raise ImportError("GLCache is not installed. Please install it with `pip install libcachesim[all]`") + raise ImportError( + 'GLCache is not installed. Please install it with `CMAKE_ARGS="-DENABLE_GLCACHE=ON" pip install libcachesim --force-reinstall`' + ) cache_specific_params = f"segment-size={segment_size}, n-merge={n_merge}, type={type}, rank-intvl={rank_intvl}, merge-consecutive-segs={merge_consecutive_segs}, train-source-y={train_source_y}, retrain-intvl={retrain_intvl}" super().__init__( @@ -621,7 +632,9 @@ def __init__( try: from .libcachesim_python import LRB_init except ImportError: - raise ImportError("LRB is not installed. Please install it with `pip install libcachesim[all]`") + raise ImportError( + 'LRB is not installed. Please install it with `CMAKE_ARGS="-DENABLE_LRB=ON" pip install libcachesim --force-reinstall`' + ) cache_specific_params = f"objective={objective}" super().__init__( diff --git a/libcachesim/synthetic_reader.py b/libcachesim/synthetic_reader.py index b429242..936f29d 100644 --- a/libcachesim/synthetic_reader.py +++ b/libcachesim/synthetic_reader.py @@ -90,7 +90,7 @@ def read_one_req(self) -> Request: req = Request() if self.current_pos >= self.num_of_req: req.valid = False - return req # return invalid request + return req # return invalid request obj_id = self.obj_ids[self.current_pos] req.obj_id = obj_id diff --git a/libcachesim/trace_reader.py b/libcachesim/trace_reader.py index 20a2aba..d282a68 100644 --- a/libcachesim/trace_reader.py +++ b/libcachesim/trace_reader.py @@ -169,7 +169,7 @@ def get_num_of_req(self) -> int: def read_one_req(self) -> Request: req = Request() - ret = self._reader.read_one_req(req) # return 0 if success + ret = self._reader.read_one_req(req) # return 0 if success if ret != 0: raise RuntimeError("Failed to read one request") return req diff --git a/pyproject.toml b/pyproject.toml index 3618995..d71659c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "scikit_build_core.build" [project] name = "libcachesim" -version = "0.3.3" +version = "0.3.3.post2" description="Python bindings for libCacheSim" readme = "README.md" requires-python = ">=3.9" @@ -30,29 +30,14 @@ dependencies = [ "pytest>=8.4.1", ] + [project.optional-dependencies] test = ["pytest"] -dev = [ - "pytest", - "pre-commit", - "ruff>=0.7.0", - "mypy>=1.0.0", -] -all = [ - "xgboost", - "lightgbm" -] +dev = ["pytest", "pre-commit", "ruff>=0.7.0", "mypy>=1.0.0"] - -[tool.scikit-build] -wheel.expand-macos-universal-tags = true -build-dir = "build" -cmake.build-type = "Release" -cmake.args = ["-G", "Ninja"] -cmake.define = { CMAKE_OSX_DEPLOYMENT_TARGET = "14.0" } -cmake.version = ">=3.15" -cmake.source-dir = "." -install.strip = false +# ============================================================ +# pytest +# ============================================================ [tool.pytest.ini_options] minversion = "8.0" @@ -71,6 +56,23 @@ python_files = ["test.py", "test_*.py", "*_test.py"] python_classes = ["Test*"] python_functions = ["test_*"] +# ============================================================ +# scikit-build +# ============================================================ + +[tool.scikit-build] +build-dir = "build" + +[tool.scikit-build.cmake] +build-type = "Release" +args = ["-G", "Ninja"] +define = { CMAKE_OSX_DEPLOYMENT_TARGET = "14.0" } +version = ">=3.15" +source-dir = "." + +[tool.scikit-build.install] +strip = false + [tool.cibuildwheel] manylinux-x86_64-image = "quay.io/pypa/manylinux_2_34_x86_64" @@ -80,10 +82,11 @@ build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*"] skip = ["*-win32", "*-manylinux_i686", "*-musllinux*", "pp*"] # Set the environment variable for the wheel build step. -environment = { LCS_BUILD_DIR = "{project}/src/libCacheSim/build", MACOSX_DEPLOYMENT_TARGET = "14.0" } +# NOTE(haocheng): we enable all the optional features for the wheel build. +environment = { LCS_BUILD_DIR = "{project}/src/libCacheSim/build", MACOSX_DEPLOYMENT_TARGET = "14.0", CMAKE_ARGS = "-DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON -DENABLE_LRB=ON" } # Test that the wheel can be imported -test-command = "python -c 'import libcachesim; print(\"Import successful\")'" +test-command = "python -c 'import libcachesim; print(\"Import successful\")'; cp -r {project}/tests .; python -m pytest tests/ -v -m 'not optional'; python -m pytest tests/ -v -m 'optional'" [tool.cibuildwheel.linux] before-all = "yum install -y yum-utils && yum-config-manager --set-enabled crb && yum install -y git && git submodule update --init --recursive && bash scripts/install_deps.sh" diff --git a/scripts/detect_deps.py b/scripts/detect_deps.py index ab66642..5ef26a7 100644 --- a/scripts/detect_deps.py +++ b/scripts/detect_deps.py @@ -9,11 +9,13 @@ import sys import subprocess + def fix_pybind11(): """Fix pybind11 installation""" print("Checking pybind11 installation...") try: import pybind11 + print("✓ pybind11 is installed") # Check CMake config try: @@ -29,6 +31,7 @@ def fix_pybind11(): subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "pybind11"], check=True) print("✓ pybind11 reinstalled successfully") import pybind11 + cmake_dir = pybind11.get_cmake_dir() print(f"✓ pybind11 CMake directory: {cmake_dir}") return True @@ -36,25 +39,28 @@ def fix_pybind11(): print(f"✗ pybind11 installation failed: {e}") return False + def fix_xgboost(): """Fix xgboost installation""" print("Checking xgboost installation...") try: import xgboost + print("✓ xgboost is installed") # Try to find CMake directory (if available) - cmake_dir = getattr(xgboost, 'cmake_dir', None) + cmake_dir = getattr(xgboost, "cmake_dir", None) if cmake_dir: print(f"✓ xgboost CMake directory: {cmake_dir}") else: # Try common install locations import os + possible_dirs = [ - os.path.join(xgboost.__path__[0], 'cmake'), - os.path.join(xgboost.__path__[0], '..', 'cmake'), - '/usr/local/lib/cmake/xgboost', - '/usr/local/share/cmake/xgboost', - '/opt/homebrew/lib/cmake/xgboost', + os.path.join(xgboost.__path__[0], "cmake"), + os.path.join(xgboost.__path__[0], "..", "cmake"), + "/usr/local/lib/cmake/xgboost", + "/usr/local/share/cmake/xgboost", + "/opt/homebrew/lib/cmake/xgboost", ] found = False for d in possible_dirs: @@ -72,19 +78,21 @@ def fix_xgboost(): subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "xgboost"], check=True) print("✓ xgboost reinstalled successfully") import xgboost + print("✓ xgboost is installed after reinstall") # Repeat CMake dir check after reinstall - cmake_dir = getattr(xgboost, 'cmake_dir', None) + cmake_dir = getattr(xgboost, "cmake_dir", None) if cmake_dir: print(f"✓ xgboost CMake directory: {cmake_dir}") else: import os + possible_dirs = [ - os.path.join(xgboost.__path__[0], 'cmake'), - os.path.join(xgboost.__path__[0], '..', 'cmake'), - '/usr/local/lib/cmake/xgboost', - '/usr/local/share/cmake/xgboost', - '/opt/homebrew/lib/cmake/xgboost', + os.path.join(xgboost.__path__[0], "cmake"), + os.path.join(xgboost.__path__[0], "..", "cmake"), + "/usr/local/lib/cmake/xgboost", + "/usr/local/share/cmake/xgboost", + "/opt/homebrew/lib/cmake/xgboost", ] found = False for d in possible_dirs: @@ -99,24 +107,27 @@ def fix_xgboost(): print(f"✗ xgboost installation failed: {e}") return False + def fix_lightgbm(): """Fix lightgbm installation""" print("Checking lightgbm installation...") try: import lightgbm + print("✓ lightgbm is installed") # Try to find CMake directory (if available) - cmake_dir = getattr(lightgbm, 'cmake_dir', None) + cmake_dir = getattr(lightgbm, "cmake_dir", None) if cmake_dir: print(f"✓ lightgbm CMake directory: {cmake_dir}") else: import os + possible_dirs = [ - os.path.join(lightgbm.__path__[0], 'cmake'), - os.path.join(lightgbm.__path__[0], '..', 'cmake'), - '/usr/local/lib/cmake/LightGBM', - '/usr/local/share/cmake/LightGBM', - '/opt/homebrew/lib/cmake/LightGBM', + os.path.join(lightgbm.__path__[0], "cmake"), + os.path.join(lightgbm.__path__[0], "..", "cmake"), + "/usr/local/lib/cmake/LightGBM", + "/usr/local/share/cmake/LightGBM", + "/opt/homebrew/lib/cmake/LightGBM", ] found = False for d in possible_dirs: @@ -134,19 +145,21 @@ def fix_lightgbm(): subprocess.run([sys.executable, "-m", "pip", "install", "--force-reinstall", "lightgbm"], check=True) print("✓ lightgbm reinstalled successfully") import lightgbm + print("✓ lightgbm is installed after reinstall") # Repeat CMake dir check after reinstall - cmake_dir = getattr(lightgbm, 'cmake_dir', None) + cmake_dir = getattr(lightgbm, "cmake_dir", None) if cmake_dir: print(f"✓ lightgbm CMake directory: {cmake_dir}") else: import os + possible_dirs = [ - os.path.join(lightgbm.__path__[0], 'cmake'), - os.path.join(lightgbm.__path__[0], '..', 'cmake'), - '/usr/local/lib/cmake/LightGBM', - '/usr/local/share/cmake/LightGBM', - '/opt/homebrew/lib/cmake/LightGBM', + os.path.join(lightgbm.__path__[0], "cmake"), + os.path.join(lightgbm.__path__[0], "..", "cmake"), + "/usr/local/lib/cmake/LightGBM", + "/usr/local/share/cmake/LightGBM", + "/opt/homebrew/lib/cmake/LightGBM", ] found = False for d in possible_dirs: @@ -161,6 +174,7 @@ def fix_lightgbm(): print(f"✗ lightgbm installation failed: {e}") return False + def detect_dependencies(): """Detect dependencies for the project""" print("Detecting dependencies...") @@ -170,5 +184,6 @@ def detect_dependencies(): fix_xgboost() fix_lightgbm() + if __name__ == "__main__": - detect_dependencies() \ No newline at end of file + detect_dependencies() diff --git a/scripts/smart_build.py b/scripts/smart_build.py index 0efb783..871845f 100644 --- a/scripts/smart_build.py +++ b/scripts/smart_build.py @@ -9,17 +9,17 @@ import os import platform + def get_macos_deployment_target(): """Get appropriate macOS deployment target""" if sys.platform != "darwin": return None - + try: - result = subprocess.run(["sw_vers", "-productVersion"], - capture_output=True, text=True, check=True) + result = subprocess.run(["sw_vers", "-productVersion"], capture_output=True, text=True, check=True) macos_version = result.stdout.strip() - major_version = macos_version.split('.')[0] - + major_version = macos_version.split(".")[0] + # Set deployment target to current version deployment_target = f"{major_version}.0" print(f"Detected macOS version: {macos_version}, set deployment target: {deployment_target}") @@ -28,6 +28,7 @@ def get_macos_deployment_target(): print(f"Failed to detect macOS version, using default: {e}") return "14.0" + def check_dependency(module_name): """Check if a Python module is installed""" try: @@ -36,77 +37,81 @@ def check_dependency(module_name): except ImportError: return False + def fix_pybind11(): """Fix pybind11 installation""" print("Checking pybind11...") subprocess.run([sys.executable, "scripts/fix_pybind11.py"], check=True) + def build_with_flags(): """Build according to dependencies""" # Fix pybind11 fix_pybind11() - + # Check ML dependencies xgboost_available = check_dependency("xgboost") lightgbm_available = check_dependency("lightgbm") - + print(f"XGBoost available: {xgboost_available}") print(f"LightGBM available: {lightgbm_available}") - + # Build CMake args cmake_args = ["-G", "Ninja"] - + # Add pybind11 path try: import pybind11 + pybind11_dir = pybind11.get_cmake_dir() cmake_args.extend([f"-Dpybind11_DIR={pybind11_dir}"]) print(f"Set pybind11 path: {pybind11_dir}") except Exception as e: print(f"Warning: failed to set pybind11 path: {e}") - + # Enable GLCache if XGBoost is available if xgboost_available: cmake_args.extend(["-DENABLE_GLCACHE=ON"]) print("Enable GLCache (requires XGBoost)") - + # Enable LRB and 3LCache if LightGBM is available if lightgbm_available: cmake_args.extend(["-DENABLE_LRB=ON", "-DENABLE_3L_CACHE=ON"]) print("Enable LRB and 3LCache (requires LightGBM)") - + # Set macOS deployment target deployment_target = get_macos_deployment_target() if deployment_target: cmake_args.extend([f"-DCMAKE_OSX_DEPLOYMENT_TARGET={deployment_target}"]) - + # Build commands build_dir = "src/libCacheSim/build" source_dir = "." - + # Clean build directory if os.path.exists(build_dir): print("Cleaning build directory...") subprocess.run(["rm", "-rf", build_dir], check=True) - + # Run CMake configure cmake_cmd = ["cmake", "-S", source_dir, "-B", build_dir] + cmake_args print(f"Running: {' '.join(cmake_cmd)}") subprocess.run(cmake_cmd, check=True) - + # Run build build_cmd = ["cmake", "--build", build_dir] print(f"Running: {' '.join(build_cmd)}") subprocess.run(build_cmd, check=True) - + print("✓ Build completed!") + def main(): print("=== libCacheSim Smart Build ===") print(f"Platform: {platform.platform()}") print(f"Python: {sys.version}") print() - + try: build_with_flags() except subprocess.CalledProcessError as e: @@ -116,5 +121,6 @@ def main(): print(f"✗ Build exception: {e}") sys.exit(1) + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/reference.csv b/tests/reference.csv deleted file mode 100644 index cb569d0..0000000 --- a/tests/reference.csv +++ /dev/null @@ -1,20 +0,0 @@ -FIFO,0.01,0.8368 -ARC,0.01,0.8222 -Clock,0.01,0.8328 -LRB,0.01,0.8339 -LRU,0.01,0.8339 -S3FIFO,0.01,0.8235 -Sieve,0.01,0.8231 -3LCache,0.01,0.8339 -TinyLFU,0.01,0.8262 -TwoQ,0.01,0.8276 -FIFO,0.1,0.8075 -ARC,0.1,0.7688 -Clock,0.1,0.8086 -LRB,0.1,0.8097 -LRU,0.1,0.8097 -S3FIFO,0.1,0.7542 -Sieve,0.1,0.7903 -3LCache,0.1,0.8097 -TinyLFU,0.1,0.7666 -TwoQ,0.1,0.7695 diff --git a/tests/test_cache.py b/tests/test_cache.py index 108e6fd..c339b91 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -9,40 +9,57 @@ import os from libcachesim import ( # Basic algorithms - LRU, FIFO, LFU, ARC, Clock, Random, + LRU, + FIFO, + LFU, + ARC, + Clock, + Random, # Advanced algorithms - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, + S3FIFO, + Sieve, + LIRS, + TwoQ, + SLRU, + WTinyLFU, # Request and other utilities - Request, ReqOp, SyntheticReader + Request, + ReqOp, + SyntheticReader, ) # Try to import optional algorithms that might not be available try: from libcachesim import LeCaR, LFUDA, ClockPro, Cacheus + OPTIONAL_ALGORITHMS = [LeCaR, LFUDA, ClockPro, Cacheus] except ImportError: OPTIONAL_ALGORITHMS = [] try: from libcachesim import Belady, BeladySize + OPTIMAL_ALGORITHMS = [Belady, BeladySize] except ImportError: OPTIMAL_ALGORITHMS = [] try: from libcachesim import LRUProb, FlashProb + PROBABILISTIC_ALGORITHMS = [LRUProb, FlashProb] except ImportError: PROBABILISTIC_ALGORITHMS = [] try: from libcachesim import Size, GDSF + SIZE_BASED_ALGORITHMS = [Size, GDSF] except ImportError: SIZE_BASED_ALGORITHMS = [] try: from libcachesim import Hyperbolic + HYPERBOLIC_ALGORITHMS = [Hyperbolic] except ImportError: HYPERBOLIC_ALGORITHMS = [] @@ -51,43 +68,63 @@ class TestCacheBasicFunctionality: """Test basic cache functionality across different algorithms""" - @pytest.mark.parametrize("cache_class", [ - LRU, FIFO, LFU, ARC, Clock, Random, - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, LeCaR, LFUDA, ClockPro, Cacheus, - LRUProb, FlashProb, Size, GDSF, Hyperbolic - ]) + @pytest.mark.parametrize( + "cache_class", + [ + LRU, + FIFO, + LFU, + ARC, + Clock, + Random, + S3FIFO, + Sieve, + LIRS, + TwoQ, + SLRU, + WTinyLFU, + LeCaR, + LFUDA, + ClockPro, + Cacheus, + LRUProb, + FlashProb, + Size, + GDSF, + Hyperbolic, + ], + ) def test_cache_initialization(self, cache_class): """Test that all cache types can be initialized with different sizes""" - cache_sizes = [1024, 1024*1024, 1024*1024*1024] # 1KB, 1MB, 1GB - + cache_sizes = [1024, 1024 * 1024, 1024 * 1024 * 1024] # 1KB, 1MB, 1GB + for size in cache_sizes: try: cache = cache_class(size) assert cache is not None - assert hasattr(cache, 'get') - assert hasattr(cache, 'insert') - assert hasattr(cache, 'find') + assert hasattr(cache, "get") + assert hasattr(cache, "insert") + assert hasattr(cache, "find") except Exception as e: pytest.skip(f"Cache {cache_class.__name__} failed to initialize: {e}") - @pytest.mark.parametrize("cache_class", [ - LRU, FIFO, LFU, ARC, Clock, Random, - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU - ]) + @pytest.mark.parametrize( + "cache_class", [LRU, FIFO, LFU, ARC, Clock, Random, S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU] + ) def test_basic_get_and_insert(self, cache_class): """Test basic get and insert operations""" - cache = cache_class(1024*1024) # 1MB cache - + cache = cache_class(1024 * 1024) # 1MB cache + # Create a request req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Initially, object should not be in cache hit = cache.get(req) assert hit == False - + # Insert the object if cache_class != LIRS: cache_obj = cache.insert(req) @@ -96,20 +133,41 @@ def test_basic_get_and_insert(self, cache_class): assert cache_obj.obj_size == 100 else: assert cache.insert(req) is None - + # Now it should be a hit hit = cache.get(req) assert hit == True - @pytest.mark.parametrize("cache_class", [ - LRU, FIFO, LFU, ARC, Clock, Random, - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, LeCaR, LFUDA, ClockPro, Cacheus, - LRUProb, FlashProb, Size, GDSF, Hyperbolic - ]) + @pytest.mark.parametrize( + "cache_class", + [ + LRU, + FIFO, + LFU, + ARC, + Clock, + Random, + S3FIFO, + Sieve, + LIRS, + TwoQ, + SLRU, + WTinyLFU, + LeCaR, + LFUDA, + ClockPro, + Cacheus, + LRUProb, + FlashProb, + Size, + GDSF, + Hyperbolic, + ], + ) def test_cache_eviction(self, cache_class): """Test that cache eviction works when cache is full""" - cache = cache_class(1024*1024) # 1MB cache - + cache = cache_class(1024 * 1024) # 1MB cache + if cache_class == GDSF: pytest.skip("GDSF should be used with find/get but not insert") @@ -120,9 +178,9 @@ def test_cache_eviction(self, cache_class): req.obj_size = 50 # Each object is 50 bytes req.op = ReqOp.OP_GET req.next_access_vtime = 100 + i - + cache.insert(req) - + # Try to insert one more object req = Request() req.obj_id = 999 @@ -131,59 +189,101 @@ def test_cache_eviction(self, cache_class): req.op = ReqOp.OP_GET cache.insert(req) - @pytest.mark.parametrize("cache_class", [ - LRU, FIFO, LFU, ARC, Clock, Random, - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, LeCaR, LFUDA, ClockPro, Cacheus, - LRUProb, FlashProb, Size, GDSF, Hyperbolic - ]) + @pytest.mark.parametrize( + "cache_class", + [ + LRU, + FIFO, + LFU, + ARC, + Clock, + Random, + S3FIFO, + Sieve, + LIRS, + TwoQ, + SLRU, + WTinyLFU, + LeCaR, + LFUDA, + ClockPro, + Cacheus, + LRUProb, + FlashProb, + Size, + GDSF, + Hyperbolic, + ], + ) def test_cache_find_method(self, cache_class): """Test the find method functionality""" cache = cache_class(1024) - + req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Initially should not find the object cache_obj = cache.find(req, update_cache=False) assert cache_obj is None - + # Insert the object cache.insert(req) - + # Now should find it cache_obj = cache.find(req, update_cache=False) assert cache_obj is not None assert cache_obj.obj_id == 1 - @pytest.mark.parametrize("cache_class", [ - LRU, FIFO, LFU, ARC, Clock, Random, - S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, LeCaR, LFUDA, ClockPro, Cacheus, - LRUProb, FlashProb, Size, GDSF, Hyperbolic - ]) + @pytest.mark.parametrize( + "cache_class", + [ + LRU, + FIFO, + LFU, + ARC, + Clock, + Random, + S3FIFO, + Sieve, + LIRS, + TwoQ, + SLRU, + WTinyLFU, + LeCaR, + LFUDA, + ClockPro, + Cacheus, + LRUProb, + FlashProb, + Size, + GDSF, + Hyperbolic, + ], + ) def test_cache_can_insert(self, cache_class): """Test can_insert method""" - cache = cache_class(1024*1024) - + cache = cache_class(1024 * 1024) + req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Should be able to insert initially can_insert = cache.can_insert(req) assert can_insert == True - + # Insert the object cache.insert(req) - + # Try to insert a larger object that won't fit req2 = Request() req2.obj_id = 2 req2.obj_size = 150 # Too large for remaining space req2.op = ReqOp.OP_GET - + can_insert = cache.can_insert(req2) # Some algorithms might still return True if they can evict assert can_insert in [True, False] @@ -195,12 +295,12 @@ class TestCacheEdgeCases: def test_zero_size_cache(self): """Test cache with zero size""" cache = LRU(0) - + req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Should not be able to insert can_insert = cache.can_insert(req) assert can_insert == False @@ -208,12 +308,12 @@ def test_zero_size_cache(self): def test_large_object(self): """Test inserting object larger than cache size""" cache = LRU(100) - + req = Request() req.obj_id = 1 req.obj_size = 200 # Larger than cache req.op = ReqOp.OP_GET - + # Should not be able to insert can_insert = cache.can_insert(req) assert can_insert == False @@ -227,12 +327,12 @@ def test_string_object_id(self): def test_zero_size_object(self): """Test with zero size object""" cache = LRU(1024) - + req = Request() req.obj_id = 1 req.obj_size = 0 req.op = ReqOp.OP_GET - + # Should work fine cache.insert(req) hit = cache.get(req) @@ -245,46 +345,33 @@ class TestCacheWithSyntheticTrace: def test_cache_with_zipf_trace(self): """Test cache performance with Zipf distribution""" # Create synthetic reader with Zipf distribution - reader = SyntheticReader( - num_of_req=1000, - obj_size=100, - alpha=1.0, - dist="zipf", - num_objects=100, - seed=42 - ) - + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + # Test with different cache algorithms cache_algorithms = [LRU, FIFO, LFU, S3FIFO, Sieve] - + for cache_class in cache_algorithms: cache = cache_class(1024) # 1KB cache - + # Process the trace miss_ratio, _ = cache.process_trace(reader) - + # Basic sanity checks assert 0.0 <= miss_ratio <= 1.0 - + # Reset reader for next test reader.reset() def test_cache_with_uniform_trace(self): """Test cache performance with uniform distribution""" # Create synthetic reader with uniform distribution - reader = SyntheticReader( - num_of_req=500, - obj_size=50, - dist="uniform", - num_objects=50, - seed=123 - ) - + reader = SyntheticReader(num_of_req=500, obj_size=50, dist="uniform", num_objects=50, seed=123) + cache = LRU(512) # 512B cache - + # Process the trace miss_ratio, _ = cache.process_trace(reader) - + # Basic sanity checks assert 0.0 <= miss_ratio <= 1.0 @@ -295,18 +382,18 @@ class TestCacheStatistics: def test_cache_occupied_bytes(self): """Test get_occupied_byte method""" cache = LRU(1024) - + # Initially should be 0 occupied = cache.get_occupied_byte() assert occupied == 0 - + # Insert an object req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET cache.insert(req) - + # Should reflect the inserted object size occupied = cache.get_occupied_byte() assert occupied >= 100 # May include metadata overhead @@ -314,11 +401,11 @@ def test_cache_occupied_bytes(self): def test_cache_object_count(self): """Test get_n_obj method""" cache = LRU(1024) - + # Initially should be 0 n_obj = cache.get_n_obj() assert n_obj == 0 - + # Insert objects for i in range(3): req = Request() @@ -326,7 +413,7 @@ def test_cache_object_count(self): req.obj_size = 100 req.op = ReqOp.OP_GET cache.insert(req) - + # Should have 3 objects n_obj = cache.get_n_obj() assert n_obj == 3 @@ -334,14 +421,14 @@ def test_cache_object_count(self): def test_cache_print(self): """Test print_cache method""" cache = LRU(1024) - + # Insert an object req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET cache.insert(req) - + # Should return a string representation cache.print_cache() @@ -352,22 +439,22 @@ class TestCacheOperations: def test_cache_remove(self): """Test remove method""" cache = LRU(1024) - + # Insert an object req = Request() req.obj_id = 1 req.obj_size = 100 req.op = ReqOp.OP_GET cache.insert(req) - + # Verify it's in cache hit = cache.get(req) assert hit == True - + # Remove it removed = cache.remove(1) assert removed == True - + # Verify it's no longer in cache hit = cache.get(req) assert hit == False @@ -375,7 +462,7 @@ def test_cache_remove(self): def test_cache_need_eviction(self): """Test need_eviction method""" cache = LRU(200) - + # Insert objects until cache is nearly full for i in range(3): req = Request() @@ -383,13 +470,13 @@ def test_cache_need_eviction(self): req.obj_size = 50 req.op = ReqOp.OP_GET cache.insert(req) - + # Try to insert a larger object req = Request() req.obj_id = 999 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Should need eviction need_eviction = cache.need_eviction(req) assert need_eviction == True @@ -397,7 +484,7 @@ def test_cache_need_eviction(self): def test_cache_to_evict(self): """Test to_evict method""" cache = LRU(200) - + # Insert objects for i in range(3): req = Request() @@ -405,14 +492,42 @@ def test_cache_to_evict(self): req.obj_size = 50 req.op = ReqOp.OP_GET cache.insert(req) - + # Try to insert a larger object req = Request() req.obj_id = 999 req.obj_size = 100 req.op = ReqOp.OP_GET - + # Should return an object to evict evict_obj = cache.to_evict(req) assert evict_obj is not None - assert hasattr(evict_obj, 'obj_id') \ No newline at end of file + assert hasattr(evict_obj, "obj_id") + + +class TestCacheOptionalAlgorithms: + """Test optional algorithms""" + + @pytest.mark.optional + def test_glcache(self): + """Test GLCache algorithm""" + from libcachesim import GLCache + + cache = GLCache(1024) + assert cache is not None + + @pytest.mark.optional + def test_lrb(self): + """Test LRB algorithm""" + from libcachesim import LRB + + cache = LRB(1024) + assert cache is not None + + @pytest.mark.optional + def test_3lcache(self): + """Test 3LCache algorithm""" + from libcachesim import ThreeLCache + + cache = ThreeLCache(1024) + assert cache is not None