diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 115a631..8b67b2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,21 +26,14 @@ jobs: - name: Set up Python ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }} - - name: Install system dependencies (Ubuntu) - if: matrix.os == 'ubuntu-latest' + - name: Install system dependencies run: | - sudo apt-get update - sudo apt-get install -y pkg-config libglib2.0-dev libzstd-dev - - - name: Install system dependencies (macOS) - if: matrix.os == 'macos-latest' || matrix.os == 'macos-14' - run: | - brew install glib pkg-config zstd argp-standalone + bash src/libCacheSim/scripts/install_dependency.sh - name: Build and test with uv run: | uv venv --python ${{ matrix.python-version }} - uv pip install -e .[dev] + CMAKE_ARGS="-DENABLE_GLCACHE=OFF -DENABLE_LRB=OFF -DENABLE_3L_CACHE=OFF" 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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 309e7ee..b80a6b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,35 +1,105 @@ cmake_minimum_required(VERSION 3.15...3.27) project(libCacheSim-python) + +# Project metadata set(DESCRIPTION "The libCacheSim Python Package") -set(PROJECT_WEB "http://cachemon.github.io/libCacheSim-python") - -# Auto-initialize submodules if not already done -find_package(Git QUIET) -if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") - # Check if submodule is initialized - if(NOT EXISTS "${PROJECT_SOURCE_DIR}/src/libCacheSim/CMakeLists.txt") - message(STATUS "Initializing git submodules...") - execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive - WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} - RESULT_VARIABLE GIT_SUBMOD_RESULT) - if(NOT GIT_SUBMOD_RESULT EQUAL "0") - message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}") +set(PROJECT_WEB "https://docs.libcachesim.com/python") + +# Build type configuration +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Debug CACHE STRING "Debug" FORCE) +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) + +# C++ standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# ============================================================================= +# Compiler Flags Configuration +# ============================================================================= + +# Base flags for all compilers +set(BASE_C_FLAGS "-fPIC -fno-strict-overflow -fno-strict-aliasing") +set(BASE_CXX_FLAGS "-fPIC -fno-strict-overflow -fno-strict-aliasing") + +# Compiler-specific flags +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(COMPILER_SPECIFIC_FLAGS "-Wno-cast-user-defined -Wno-array-bounds") +elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + set(COMPILER_SPECIFIC_FLAGS "-Wno-array-bounds") +else() + set(COMPILER_SPECIFIC_FLAGS "") +endif() + +# Apply flags +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${BASE_C_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${BASE_CXX_FLAGS} ${COMPILER_SPECIFIC_FLAGS}") + +# ============================================================================= +# Git Submodule Management +# ============================================================================= + +function(initialize_submodules) + find_package(Git QUIET) + if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git") + if(NOT EXISTS "${PROJECT_SOURCE_DIR}/src/libCacheSim/CMakeLists.txt") + message(STATUS "Initializing git submodules...") + execute_process( + COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + RESULT_VARIABLE GIT_SUBMOD_RESULT + ) + if(NOT GIT_SUBMOD_RESULT EQUAL "0") + message(FATAL_ERROR "git submodule update --init --recursive failed with ${GIT_SUBMOD_RESULT}") + endif() endif() endif() -endif() +endfunction() + +initialize_submodules() -# Auto-build libCacheSim if needed +# ============================================================================= +# libCacheSim Build Configuration +# ============================================================================= + +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim/cmake/Modules/") set(LIBCACHESIM_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim") +set(LIBCACHESIM_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim/build") + if(NOT EXISTS "${LIBCACHESIM_SOURCE_DIR}/CMakeLists.txt") message(FATAL_ERROR "libCacheSim submodule not found. Please run 'git submodule update --init --recursive'") endif() -# Build libCacheSim first -set(LIBCACHESIM_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim/build") -if(NOT EXISTS "${LIBCACHESIM_BUILD_DIR}/export_vars.cmake") +function(build_libcachesim) message(STATUS "Building libCacheSim...") + + # Prepare CMake arguments for subproject + set(CMAKE_ARGS + "-DCMAKE_C_FLAGS=${BASE_C_FLAGS}" + "-DCMAKE_CXX_FLAGS=${BASE_CXX_FLAGS} ${COMPILER_SPECIFIC_FLAGS}" + "-DCMAKE_CXX_FLAGS_DEBUG=-g ${BASE_CXX_FLAGS} ${COMPILER_SPECIFIC_FLAGS}" + "-DCMAKE_CXX_FLAGS_RELEASE=-O3 ${BASE_CXX_FLAGS} ${COMPILER_SPECIFIC_FLAGS}" + "-DCMAKE_CXX_FLAGS_RELWITHDEBINFO=-O2 -g -DNDEBUG ${BASE_CXX_FLAGS} ${COMPILER_SPECIFIC_FLAGS}" + ) + + # Configure libCacheSim execute_process( - COMMAND ${CMAKE_COMMAND} -S ${LIBCACHESIM_SOURCE_DIR} -B ${LIBCACHESIM_BUILD_DIR} -G Ninja + COMMAND ${CMAKE_COMMAND} + -S ${LIBCACHESIM_SOURCE_DIR} + -B ${LIBCACHESIM_BUILD_DIR} + -G Ninja + -DENABLE_LRB=${ENABLE_LRB} + -DENABLE_GLCACHE=${ENABLE_GLCACHE} + -DENABLE_3L_CACHE=${ENABLE_3L_CACHE} + -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} + ${CMAKE_ARGS} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} RESULT_VARIABLE CMAKE_CONFIG_RESULT ) @@ -37,6 +107,7 @@ if(NOT EXISTS "${LIBCACHESIM_BUILD_DIR}/export_vars.cmake") message(FATAL_ERROR "Failed to configure libCacheSim") endif() + # Build libCacheSim execute_process( COMMAND ${CMAKE_COMMAND} --build ${LIBCACHESIM_BUILD_DIR} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} @@ -45,93 +116,140 @@ if(NOT EXISTS "${LIBCACHESIM_BUILD_DIR}/export_vars.cmake") if(NOT CMAKE_BUILD_RESULT EQUAL "0") message(FATAL_ERROR "Failed to build libCacheSim") endif() -endif() - -# Note(haocheng): now we still utilize the exported cache from -# the main project, which should be deprecated soon - -# Include exported variables from cache -if(DEFINED LIBCB_BUILD_DIR) - set(MAIN_PROJECT_BUILD_DIR "${LIBCB_BUILD_DIR}") - message(STATUS "Using provided LIBCB_BUILD_DIR: ${LIBCB_BUILD_DIR}") -else() - set(MAIN_PROJECT_BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim/build") -endif() -set(EXPORT_FILE "${MAIN_PROJECT_BUILD_DIR}/export_vars.cmake") +endfunction() -if(EXISTS "${EXPORT_FILE}") - include("${EXPORT_FILE}") - message(STATUS "Loaded variables from export_vars.cmake") -else() - message(FATAL_ERROR "export_vars.cmake not found at ${EXPORT_FILE}. Please build the main project first (e.g. cd .. && cmake -G Ninja -B build)") -endif() +build_libcachesim() -# Force enable -fPIC -set(CMAKE_POSITION_INDEPENDENT_CODE ON) -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC") +# ============================================================================= +# Logging Configuration +# ============================================================================= -project(libCacheSim-python VERSION "${LIBCACHESIM_VERSION}") - -if(LOG_LEVEL_LOWER STREQUAL "default") - if(CMAKE_BUILD_TYPE_LOWER MATCHES "debug") +function(configure_logging) + string(TOLOWER "${LOG_LEVEL}" LOG_LEVEL_LOWER) + string(TOLOWER "${CMAKE_BUILD_TYPE}" CMAKE_BUILD_TYPE_LOWER) + + if(LOG_LEVEL_LOWER STREQUAL "default") + if(CMAKE_BUILD_TYPE_LOWER MATCHES "debug") + add_compile_definitions(LOGLEVEL=6) + else() + add_compile_definitions(LOGLEVEL=7) + endif() + elseif(LOG_LEVEL_LOWER STREQUAL "verbose") + add_compile_definitions(LOGLEVEL=5) + elseif(LOG_LEVEL_LOWER STREQUAL "debug") add_compile_definitions(LOGLEVEL=6) + elseif(LOG_LEVEL_LOWER STREQUAL "info") + add_compile_definitions(LOGLEVEL=7) + elseif(LOG_LEVEL_LOWER STREQUAL "warn") + add_compile_definitions(LOGLEVEL=8) + elseif(LOG_LEVEL_LOWER STREQUAL "error") + add_compile_definitions(LOGLEVEL=9) else() add_compile_definitions(LOGLEVEL=7) endif() -elseif(LOG_LEVEL_LOWER STREQUAL "verbose") - add_compile_definitions(LOGLEVEL=5) -elseif(LOG_LEVEL_LOWER STREQUAL "debug") - add_compile_definitions(LOGLEVEL=6) -elseif(LOG_LEVEL_LOWER STREQUAL "info") - add_compile_definitions(LOGLEVEL=7) -elseif(LOG_LEVEL_LOWER STREQUAL "warn") - add_compile_definitions(LOGLEVEL=8) -elseif(LOG_LEVEL_LOWER STREQUAL "error") - add_compile_definitions(LOGLEVEL=9) -else() - add_compile_definitions(LOGLEVEL=7) -endif() +endfunction() -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) +configure_logging() + +# ============================================================================= +# Dependency Management +# ============================================================================= -# Find python and pybind11 +# Find required packages find_package(Python REQUIRED COMPONENTS Interpreter Development.Module) find_package(pybind11 CONFIG REQUIRED) +find_package(PkgConfig REQUIRED) + +# Initialize dependency lists +set(required_libs "") +set(optional_libs "") -# Include directories for dependencies +# GLib dependency +pkg_check_modules(GLib REQUIRED glib-2.0) include_directories(${GLib_INCLUDE_DIRS}) -include_directories(${GLib_CONFIG_INCLUDE_DIR}) -include_directories(${XGBOOST_INCLUDE_DIR}) -include_directories(${LIGHTGBM_PATH}) +link_directories(${GLib_LIBRARY_DIRS}) +list(APPEND required_libs ${GLib_LIBRARIES}) + +# ZSTD dependency +find_package(ZSTD REQUIRED) +message(STATUS "ZSTD_INCLUDE_DIR: ${ZSTD_INCLUDE_DIR}, ZSTD_LIBRARIES: ${ZSTD_LIBRARIES}") +if("${ZSTD_LIBRARIES}" STREQUAL "") + message(FATAL_ERROR "zstd not found") +endif() include_directories(${ZSTD_INCLUDE_DIR}) -include_directories(${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/bin) +link_directories(${ZSTD_LIBRARY_DIRS}) +list(APPEND required_libs ${ZSTD_LIBRARIES}) -# Find the main libCacheSim library -set(MAIN_PROJECT_BUILD_DIR "${MAIN_PROJECT_BUILD_DIR}") -set(MAIN_PROJECT_LIB_PATH "${MAIN_PROJECT_BUILD_DIR}/liblibCacheSim.a") +# Optional dependencies based on features +if(ENABLE_GLCACHE) + 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() -if(EXISTS "${MAIN_PROJECT_LIB_PATH}") - message(STATUS "Found pre-built libCacheSim library at ${MAIN_PROJECT_LIB_PATH}") +# LightGBM for LRB and 3L_CACHE +set(LIGHTGBM_FEATURES ENABLE_LRB ENABLE_3L_CACHE) +set(LIGHTGBM_NEEDED FALSE) - # Import the main library as an imported target - add_library(libCacheSim_main STATIC IMPORTED) - set_target_properties(libCacheSim_main PROPERTIES - IMPORTED_LOCATION "${MAIN_PROJECT_LIB_PATH}" - INTERFACE_INCLUDE_DIRECTORIES "${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/include;${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/utils/include;${MAIN_PROJECT_SOURCE_DIR}/libCacheSim" - ) - link_directories(${GLib_LIBRARY_DIRS}) - link_directories(${ZSTD_LIBRARY_DIRS}) - set(LIBCACHESIM_TARGET libCacheSim_main) +foreach(FEATURE ${LIGHTGBM_FEATURES}) + if(${FEATURE}) + set(LIGHTGBM_NEEDED TRUE) + add_compile_definitions(${FEATURE}=1) + endif() +endforeach() -else() - message(FATAL_ERROR "Pre-built libCacheSim library not found. Please build the main project first: cd .. && cmake -G Ninja -B build && ninja -C build") +if(LIGHTGBM_NEEDED) + if(NOT DEFINED LIGHTGBM_PATH) + find_path(LIGHTGBM_PATH LightGBM) + endif() + if(NOT LIGHTGBM_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") + endif() + + include_directories(${LIGHTGBM_PATH}) + list(APPEND optional_libs ${LIGHTGBM_LIB}) +endif() + +# ============================================================================= +# libCacheSim Library Target +# ============================================================================= + +set(MAIN_PROJECT_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/libCacheSim") +set(MAIN_PROJECT_BUILD_DIR "${LIBCACHESIM_BUILD_DIR}") +set(MAIN_PROJECT_LIB_PATH "${MAIN_PROJECT_BUILD_DIR}/liblibCacheSim.a") + +if(NOT EXISTS "${MAIN_PROJECT_LIB_PATH}") + message(FATAL_ERROR "Pre-built libCacheSim library not found at ${MAIN_PROJECT_LIB_PATH}. Please build the main project first.") endif() +message(STATUS "Found pre-built libCacheSim library at ${MAIN_PROJECT_LIB_PATH}") + +# Import the main library as an imported target +add_library(libCacheSim_main STATIC IMPORTED) +set_target_properties(libCacheSim_main PROPERTIES + IMPORTED_LOCATION "${MAIN_PROJECT_LIB_PATH}" + INTERFACE_INCLUDE_DIRECTORIES + "${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/include;${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/utils/include;${MAIN_PROJECT_SOURCE_DIR}/libCacheSim" +) + +# ============================================================================= +# Python Module Configuration +# ============================================================================= + include_directories(src) +include_directories(${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/bin) -python_add_library(libcachesim_python MODULE +# Define source files +set(PYTHON_MODULE_SOURCES src/export.cpp src/export_cache.cpp src/export_reader.cpp @@ -142,9 +260,12 @@ python_add_library(libcachesim_python MODULE ${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/bin/traceUtils/traceConvLCS.cpp ${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/bin/traceUtils/traceConvOracleGeneral.cpp ${MAIN_PROJECT_SOURCE_DIR}/libCacheSim/bin/traceUtils/utils.cpp - WITH_SOABI ) +# Create Python module +python_add_library(libcachesim_python MODULE ${PYTHON_MODULE_SOURCES} WITH_SOABI) + +# Configure target properties set_target_properties(libcachesim_python PROPERTIES POSITION_INDEPENDENT_CODE ON INSTALL_RPATH_USE_LINK_PATH TRUE @@ -154,42 +275,48 @@ set_target_properties(libcachesim_python PROPERTIES target_compile_definitions(libcachesim_python PRIVATE VERSION_INFO=${PROJECT_VERSION}) +# ============================================================================= +# Platform-Specific Configuration +# ============================================================================= + +function(configure_platform_specific_linking target) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(${target} PRIVATE -Wl,--no-as-needed) + target_link_libraries(${target} PRIVATE dl) + elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") + # Find argp library on macOS + find_library(ARGP_LIBRARY argp PATHS /opt/homebrew/lib /usr/local/lib) + if(ARGP_LIBRARY) + target_link_libraries(${target} PRIVATE ${ARGP_LIBRARY}) + endif() + + # Find and link intl library + find_library(INTL_LIBRARY intl PATHS /opt/homebrew/lib /usr/local/lib) + if(INTL_LIBRARY) + target_link_libraries(${target} PRIVATE ${INTL_LIBRARY}) + endif() + else() + # Other platforms - try to link dl if available + find_library(DL_LIBRARY dl) + if(DL_LIBRARY) + target_link_libraries(${target} PRIVATE ${DL_LIBRARY}) + endif() + endif() +endfunction() + +# Link libraries target_link_libraries(libcachesim_python PRIVATE - ${LIBCACHESIM_TARGET} + libCacheSim_main pybind11::headers pybind11::module - ${GLib_LIBRARIES} - ${ZSTD_LIBRARIES} + ${required_libs} + ${optional_libs} ) -# Add platform-specific link options and libraries -if(CMAKE_SYSTEM_NAME STREQUAL "Linux") - # GNU ld option, only available on Linux - target_link_options(libcachesim_python PRIVATE -Wl,--no-as-needed) - target_link_libraries(libcachesim_python PRIVATE dl) -elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin") - # macOS doesn't need --no-as-needed - # dl functions are part of the system library on macOS - # No need to explicitly link dl - - # Find argp library on macOS - find_library(ARGP_LIBRARY argp PATHS /opt/homebrew/lib /usr/local/lib) - if(ARGP_LIBRARY) - target_link_libraries(libcachesim_python PRIVATE ${ARGP_LIBRARY}) - endif() +configure_platform_specific_linking(libcachesim_python) - # Find and link other dependencies that might be needed - find_library(INTL_LIBRARY intl PATHS /opt/homebrew/lib /usr/local/lib) - if(INTL_LIBRARY) - target_link_libraries(libcachesim_python PRIVATE ${INTL_LIBRARY}) - endif() -else() - # Other platforms - try to link dl if available - find_library(DL_LIBRARY dl) - if(DL_LIBRARY) - target_link_libraries(libcachesim_python PRIVATE ${DL_LIBRARY}) - endif() -endif() +# ============================================================================= +# Installation +# ============================================================================= -# install to wheel directory -install(TARGETS libcachesim_python LIBRARY DESTINATION libcachesim) +install(TARGETS libcachesim_python LIBRARY DESTINATION libcachesim) \ No newline at end of file diff --git a/libcachesim/cache.py b/libcachesim/cache.py index 31c4e4a..99a17aa 100644 --- a/libcachesim/cache.py +++ b/libcachesim/cache.py @@ -281,6 +281,8 @@ def __init__( _cache=LIRS_init(_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata)) ) + def insert(self, req: Request) -> Optional[CacheObject]: + return super().insert(req) class TwoQ(CacheBase): """2Q replacement algorithm @@ -360,7 +362,7 @@ def __init__( update_weight: bool = True, lru_weight: float = 0.5, ): - cache_specific_params = f"update-weight={update_weight}, lru-weight={lru_weight}" + cache_specific_params = f"update-weight={int(update_weight)}, lru-weight={lru_weight}" super().__init__( _cache=LeCaR_init( _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata), cache_specific_params @@ -383,7 +385,7 @@ class ClockPro(CacheBase): """Clock-Pro replacement algorithm Special parameters: - init_req: initial reference count (default: 0) + init_ref: initial reference count (default: 0) init_ratio_cold: initial ratio of cold pages (default: 1) """ @@ -393,10 +395,10 @@ def __init__( default_ttl: int = 86400 * 300, hashpower: int = 24, consider_obj_metadata: bool = False, - init_req: int = 0, + init_ref: int = 0, init_ratio_cold: float = 0.5, ): - cache_specific_params = f"init-req={init_req}, init-ratio-cold={init_ratio_cold}" + cache_specific_params = f"init-ref={init_ref}, init-ratio-cold={init_ratio_cold}" super().__init__( _cache=ClockPro_init( _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata), cache_specific_params @@ -451,13 +453,19 @@ def __init__( class LRUProb(CacheBase): - """LRU with Probabilistic Replacement (no special parameters)""" + """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=LRU_Prob_init(_create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata), cache_specific_params) ) diff --git a/pyproject.toml b/pyproject.toml index 3c6c7a4..e2fd470 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,12 +86,12 @@ environment = { LCS_BUILD_DIR = "{project}/src/libCacheSim/build", MACOSX_DEPLOY test-command = "python -c 'import libcachesim; print(\"Import successful\")'" [tool.cibuildwheel.linux] -before-all = "yum install -y yum-utils && yum-config-manager --set-enabled crb && yum install -y ninja-build cmake libzstd-devel glib2-devel" -before-build = "pip install pybind11 && git submodule update --init --recursive && python {project}/scripts/smart_build.py" +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" +before-build = "pip install pybind11 && rm -rf src/libCacheSim/build" [tool.cibuildwheel.macos] -before-all = "brew install glib google-perftools argp-standalone xxhash llvm wget cmake ninja zstd xgboost lightgbm" -before-build = "pip install pybind11 && git submodule update --init --recursive && python {project}/scripts/smart_build.py" +before-all = "brew install git && git submodule update --init --recursive && bash scripts/install_deps.sh" +before-build = "pip install pybind11 && rm -rf src/libCacheSim/build" [tool.ruff] # Allow lines to be as long as 120. diff --git a/scripts/install.sh b/scripts/install.sh index 879704c..0c6f9a7 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,17 +1,40 @@ +#!/bin/bash +set -euo pipefail + +function usage() { + echo "Usage: $0 [options]" + echo "Options:" + echo " -h, --help Show this help message" + echo " -b, --build-wheels Build the Python wheels" + exit 1 +} +# Parse command line arguments +BUILD_WHEELS=0 + +while [[ $# -gt 0 ]]; do + case $1 in + -h|--help) + usage + ;; + -b|--build-wheels) + BUILD_WHEELS=1 + shift + ;; + *) + echo "Unknown option: $1" + usage + ;; + esac +done + +# For submodule update git submodule update --init --recursive +git submodule update --recursive --remote +if [ $? -ne 0 ]; then + echo "Error: git submodule update failed" + exit 1 +fi -# Build the main libCacheSim C++ library first -echo "Building main libCacheSim library..." -pushd src/libCacheSim -bash scripts/install_dependency.sh -rm -rf build -cmake -G Ninja -B build -ninja -C build -popd - -# Now build and install the Python binding -echo "Building Python binding..." -echo "Sync python version..." python scripts/sync_version.py python -m pip install -e . -vvv @@ -21,4 +44,55 @@ python -c "import libcachesim" # Run tests python -m pip install pytest -python -m pytest tests \ No newline at end of file +python -m pytest tests + +# Build wheels if requested +if [[ $BUILD_WHEELS -eq 1 ]]; then + echo "--- Building Python wheels for distribution ---" + + # --- Environment and dependency checks --- + echo "Checking dependencies: python3, pip, docker, cibuildwheel..." + + if ! command -v python3 &> /dev/null; then + echo "Error: python3 is not installed. Please install it and run this script again." + exit 1 + fi + + if ! python3 -m pip --version &> /dev/null; then + echo "Error: pip for python3 is not available. Please install it." + exit 1 + fi + + if ! command -v docker &> /dev/null; then + echo "Error: docker is not installed. Please install it and ensure the docker daemon is running." + exit 1 + fi + + # Check if user can run docker without sudo, otherwise use sudo + SUDO_CMD="" + if ! docker ps &> /dev/null; then + echo "Warning: Current user cannot run docker. Trying with sudo." + if sudo docker ps &> /dev/null; then + SUDO_CMD="sudo" + else + echo "Error: Failed to run docker, even with sudo. Please check your docker installation and permissions." + exit 1 + fi + fi + + if ! python3 -m cibuildwheel --version &> /dev/null; then + echo "cibuildwheel not found, installing..." + python3 -m pip install cibuildwheel + fi + + echo "Dependency check completed." + + # --- Run cibuildwheel --- + # The project to build is specified as an argument. + # cibuildwheel should be run from the repository root. + # The output directory will be 'wheelhouse/' by default. + echo "Starting the wheel build process for Linux..." + ${SUDO_CMD} python3 -m cibuildwheel --platform linux . + + echo "Build process completed successfully. Wheels are in the 'wheelhouse' directory." +fi \ No newline at end of file diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh new file mode 100755 index 0000000..5deedf5 --- /dev/null +++ b/scripts/install_deps.sh @@ -0,0 +1,182 @@ +#!/bin/bash + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Logging functions +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" >&2 +} + +# Install and configure g++ version +install_gcc() { + log_info "Installing and configuring g++ version..." + + # Install gcc-c++ and devtoolset if available + if yum search devtoolset 2>/dev/null | grep -q devtoolset; then + log_info "Installing devtoolset for newer g++ version..." + yum install -y centos-release-scl + yum install -y devtoolset-11-gcc-c++ + + # Set environment variables globally for this script + export CC=/opt/rh/devtoolset-11/root/usr/bin/gcc + export CXX=/opt/rh/devtoolset-11/root/usr/bin/g++ + export PATH=/opt/rh/devtoolset-11/root/usr/bin:$PATH + export LD_LIBRARY_PATH=/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:$LD_LIBRARY_PATH + + # Create symlinks to make it the default + ln -sf /opt/rh/devtoolset-11/root/usr/bin/gcc /usr/local/bin/gcc + ln -sf /opt/rh/devtoolset-11/root/usr/bin/g++ /usr/local/bin/g++ + ln -sf /opt/rh/devtoolset-11/root/usr/bin/gcc /usr/local/bin/cc + ln -sf /opt/rh/devtoolset-11/root/usr/bin/g++ /usr/local/bin/c++ + + # Add to PATH permanently + echo 'export PATH=/opt/rh/devtoolset-11/root/usr/bin:$PATH' >> /etc/environment + echo 'export LD_LIBRARY_PATH=/opt/rh/devtoolset-11/root/usr/lib64:/opt/rh/devtoolset-11/root/usr/lib:$LD_LIBRARY_PATH' >> /etc/environment + + log_info "Using g++ from devtoolset-11: $(g++ --version | head -n1)" + else + log_info "Using system g++: $(g++ --version | head -n1)" + fi +} + +# Install CMake +install_cmake() { + log_info "Installing CMake..." + local cmake_version="3.31.0" + local cmake_dir="${HOME}/software/cmake" + + pushd /tmp/ >/dev/null + if [[ ! -f "cmake-${cmake_version}-linux-x86_64.sh" ]]; then + wget "https://github.com/Kitware/CMake/releases/download/v${cmake_version}/cmake-${cmake_version}-linux-x86_64.sh" + fi + + mkdir -p "${cmake_dir}" 2>/dev/null || true + bash "cmake-${cmake_version}-linux-x86_64.sh" --skip-license --prefix="${cmake_dir}" + + # Add to shell config files if not already present + for shell_rc in "${HOME}/.bashrc" "${HOME}/.zshrc"; do + # trunk-ignore(shellcheck/SC2016) + if [[ -f ${shell_rc} ]] && ! grep -q 'PATH=$HOME/software/cmake/bin:$PATH' "${shell_rc}"; then + # trunk-ignore(shellcheck/SC2016) + echo 'export PATH=$HOME/software/cmake/bin:$PATH' >>"${shell_rc}" + fi + done + + # Source the updated PATH + export PATH="${cmake_dir}/bin:${PATH}" + popd >/dev/null +} + +# Install Zstd from source +install_zstd() { + log_info "Installing Zstd from source..." + local zstd_version="1.5.0" + + pushd /tmp/ >/dev/null + if [[ ! -f "zstd-${zstd_version}.tar.gz" ]]; then + wget "https://github.com/facebook/zstd/releases/download/v${zstd_version}/zstd-${zstd_version}.tar.gz" + tar xf "zstd-${zstd_version}.tar.gz" + fi + pushd "zstd-${zstd_version}/build/cmake/" >/dev/null + mkdir -p _build + pushd _build >/dev/null + cmake -G Ninja .. + ninja + ninja install + popd >/dev/null + popd >/dev/null + popd >/dev/null +} + +# Install XGBoost from source +install_xgboost() { + log_info "Installing XGBoost from source..." + pushd /tmp/ >/dev/null + if [[ ! -d "xgboost" ]]; then + git clone --recursive https://github.com/dmlc/xgboost + fi + pushd xgboost >/dev/null + mkdir -p build + pushd build >/dev/null + cmake -G Ninja .. + ninja + ninja install + popd >/dev/null + popd >/dev/null + popd >/dev/null +} + +# Install LightGBM from source +install_lightgbm() { + log_info "Installing LightGBM from source..." + pushd /tmp/ >/dev/null + if [[ ! -d "LightGBM" ]]; then + git clone --recursive https://github.com/microsoft/LightGBM + fi + pushd LightGBM >/dev/null + mkdir -p build + pushd build >/dev/null + cmake -G Ninja .. + ninja + ninja install + popd >/dev/null + popd >/dev/null + popd >/dev/null +} + +# Detect OS and install dependencies +if [[ "$OSTYPE" == "linux-gnu"* ]]; then + log_info "Detected Linux system, installing dependencies via yum..." + + # Enable EPEL repository + yum install -y epel-release + + # Enable PowerTools/CRB repository + if yum repolist | grep -q powertools; then + yum-config-manager --set-enabled powertools + elif yum repolist | grep -q crb; then + yum-config-manager --set-enabled crb + fi + + # Install development tools + yum groupinstall -y "Development Tools" + yum install -y glib2-devel google-perftools-devel + yum install -y ninja-build git wget + + # Install and configure g++ version + install_gcc + + # Install CMake + install_cmake + + # Install dependencies from source + install_zstd + install_xgboost + install_lightgbm + +elif [[ "$OSTYPE" == "darwin"* ]]; then + log_info "Detected macOS system, installing dependencies via brew..." + + # Install basic dependencies via Homebrew + brew install glib google-perftools argp-standalone xxhash llvm wget cmake ninja zstd xgboost lightgbm + +else + log_error "Unsupported operating system: $OSTYPE" + exit 1 +fi + +log_info "Dependencies installation completed!" diff --git a/src/export_cache.cpp b/src/export_cache.cpp index fb383a2..cff2031 100644 --- a/src/export_cache.cpp +++ b/src/export_cache.cpp @@ -256,7 +256,7 @@ auto make_cache_wrapper(const std::string& fn_name) { cache_t* ptr = InitFn(cc_params, params_cstr); return std::unique_ptr(ptr); }, - "cc_params"_a, "cache_specific_params"_a = ""); + "cc_params"_a, "cache_specific_params"_a = ""); }; } @@ -280,7 +280,8 @@ void export_cache(py::module& m) { .def( "find", [](cache_t& self, const request_t& req, const bool update_cache) { - return self.find(&self, &req, update_cache); + cache_obj_t* obj = self.find(&self, &req, update_cache); + return py::cast(obj, py::return_value_policy::reference); }, "req"_a, "update_cache"_a = true) .def( @@ -289,16 +290,23 @@ void export_cache(py::module& m) { return self.can_insert(&self, &req); }, "req"_a) - .def( - "insert", - [](cache_t& self, const request_t& req) { - return self.insert(&self, &req); - }, - "req"_a) + .def( + "insert", + [](cache_t& self, const request_t& req) -> std::optional { + cache_obj_t* inserted = self.insert(&self, &req); + if (inserted == nullptr) { + return std::nullopt; + } + return inserted; + }, + "req"_a, + py::return_value_policy::reference // optional still respected + ) + .def( "need_eviction", [](cache_t& self, const request_t& req) { - return self.need_eviction(&self, &req); + return self.get_occupied_byte(&self) + req.obj_size > self.cache_size; }, "req"_a) .def( @@ -316,7 +324,8 @@ void export_cache(py::module& m) { .def( "to_evict", [](cache_t& self, const request_t& req) { - return self.to_evict(&self, &req); + cache_obj_t* obj = self.to_evict(&self, &req); + return py::cast(obj, py::return_value_policy::reference); }, "req"_a) .def("get_occupied_byte", @@ -348,7 +357,7 @@ void export_cache(py::module& m) { params->default_ttl = default_ttl; params->hashpower = hashpower; params->consider_obj_metadata = consider_obj_metadata; - return params; + return std::unique_ptr(params); }), "cache_size"_a, "default_ttl"_a = 86400 * 300, "hashpower"_a = 24, "consider_obj_metadata"_a = false) @@ -407,7 +416,7 @@ void export_cache(py::module& m) { req->hv = hv; req->next_access_vtime = next_access_vtime; req->ttl = ttl; - return req; + return std::unique_ptr(req); }), "obj_size"_a = 1, "op"_a = OP_NOP, "valid"_a = true, "obj_id"_a = 0, "clock_time"_a = 0, "hv"_a = 0, "next_access_vtime"_a = -2, diff --git a/src/libCacheSim b/src/libCacheSim index 2868881..4a2627d 160000 --- a/src/libCacheSim +++ b/src/libCacheSim @@ -1 +1 @@ -Subproject commit 2868881a82e48010b29524f92d4995e0c9f2e78b +Subproject commit 4a2627d4221cd49c0182cc6aa1ddd82f3006cf83 diff --git a/tests/test_cache.py b/tests/test_cache.py index e69de29..108e6fd 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -0,0 +1,418 @@ +""" +Test cases for cache implementations in libCacheSim Python bindings. + +This module tests various cache algorithms and their functionality. +""" + +import pytest +import tempfile +import os +from libcachesim import ( + # Basic algorithms + LRU, FIFO, LFU, ARC, Clock, Random, + # Advanced algorithms + S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU, + # Request and other utilities + 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 = [] + + +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 + ]) + 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 + + 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') + 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 + ]) + def test_basic_get_and_insert(self, cache_class): + """Test basic get and insert operations""" + 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) + assert cache_obj is not None + assert cache_obj.obj_id == 1 + 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 + ]) + def test_cache_eviction(self, cache_class): + """Test that cache eviction works when cache is full""" + cache = cache_class(1024*1024) # 1MB cache + + if cache_class == GDSF: + pytest.skip("GDSF should be used with find/get but not insert") + + # Insert objects until cache is full + for i in range(5): + req = Request() + req.obj_id = i + 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 + req.obj_size = 50 + req.next_access_vtime = 200 + 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 + ]) + 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 + ]) + def test_cache_can_insert(self, cache_class): + """Test can_insert method""" + 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] + + +class TestCacheEdgeCases: + """Test edge cases and error conditions""" + + 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 + + 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 + + def test_string_object_id(self): + """Test with string object ID""" + req = Request() + with pytest.raises(Exception): + req.obj_id = "1" + + 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) + assert hit == True + + +class TestCacheWithSyntheticTrace: + """Test cache performance with synthetic traces""" + + 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 + ) + + # 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 + ) + + cache = LRU(512) # 512B cache + + # Process the trace + miss_ratio, _ = cache.process_trace(reader) + + # Basic sanity checks + assert 0.0 <= miss_ratio <= 1.0 + + +class TestCacheStatistics: + """Test cache statistics and metrics""" + + 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 + + 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() + req.obj_id = i + 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 + + 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() + + +class TestCacheOperations: + """Test various cache operations""" + + 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 + + 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() + req.obj_id = i + 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 + + def test_cache_to_evict(self): + """Test to_evict method""" + cache = LRU(200) + + # Insert objects + for i in range(3): + req = Request() + req.obj_id = i + 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