From c0522edc088d15832fccca163a59131e1559ee43 Mon Sep 17 00:00:00 2001 From: Percy Date: Fri, 14 Aug 2026 17:10:05 -0500 Subject: [PATCH 1/4] [Docs] Fill in stub pages, fix drift, and add a real Chinese translation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four English pages were `[TBD]` placeholders and `examples/simulation.md` carried 19 more `[TBD]` sections. Fill them in, verified against the wrapper source rather than assumed: - `getting_started/installation.md`: requirements, PyPI install, the three optional ML-backed algorithms and their CMake flags, source builds. - `examples/analysis.md`: all 11 `AnalysisOption` fields and 5 `AnalysisParam` fields with real defaults, output files, and the C-reader-only restriction. - `api.md`: hand-written reference for the full exported surface, including the actual enum members (`ReqOp` is `OP_*`, not `READ`/`WRITE`). - `developer.md`: layout, build system, testing, adding an algorithm, docs workflow, CI, releasing. - `examples/simulation.md`: real parameters and defaults for every algorithm, plus the missing `Size` section, the `cache_size`-as-ratio behaviour, and a `CacheBase` method table. Corrections to existing content: Python floor is 3.10 (not 3.9) per `requires-python`; `int_freq` -> `init_freq`; a quickstart snippet used an undefined `lcs`; the FAQ used an absolute URL that broke under locale fallback. Two lists rendered as literal `*` characters because they lacked a preceding blank line. The Chinese tree was three orphaned files at non-mirroring paths, so they never rendered, and they documented an API that does not exist (`cache.get_hit_ratio()`, `PythonHookCache`, `SyntheticReader(dist=...)`). Delete them and translate all ten pages at paths mirroring `en/`. Link-target headings carry explicit `attr_list` anchor IDs so cross-page anchors resolve identically in both languages. `nav_translations` gains the three labels that were still English, and `Getting Started` becomes "入门指南" so it no longer collides with the Quickstart page's own title. `mkdocs build --strict` passes and all 11 anchors are present in the built HTML. Also ignore `docs/site/`, since the existing rule only covered `/site` at the repo root. [Fix] Correct wrong signatures in the type stubs These are the signatures users see in their IDE, and four were wrong: - `TraceAnalyzer.__init__` declared a leading `analyzer` parameter that does not exist and made `analysis_param`/`analysis_option` required. The real signature is `(reader, output_path, analysis_param=None, analysis_option=None)` per `trace_analyzer.py`. - `TraceReader.__init__` declared `**kwargs`; it actually takes `(trace, trace_type=UNKNOWN_TRACE, reader_init_params=None)`, and exposes `c_reader`, which callers rely on to pick the C fast path. - `Request.op` defaulted to `ReqOp.READ`, which is not a member of the enum. `export_cache.cpp` binds the default as `OP_NOP`. - `CacheBase.set_cache_size` was missing. [Feat] Add MQ, LRU-K and Clock2QPlus bindings Bump the libCacheSim submodule to the latest develop (dbf8423..373aee9), which brings three new eviction algorithms, and expose them in Python. New algorithms: - LRUK: evicts by largest backward K-distance (k) - MQ: multi-queue hierarchy with a Qout ghost queue (n_queue, lifetime, qout_size_ratio) - Clock2QPlus: 2Q variant over a Clock main cache (fifo_size_ratio, ghost_size_ratio, move_to_main_threshold, corr_window_ratio) Each is wired through export_cache.cpp, cache.py, __init__.py and the type stubs, covered by the shared parametrized tests, and documented in both the English and Chinese algorithm reference. Two fixes to existing bindings found while checking the docs against the implementation: - BeladySize was unusable: it emitted the key "n-samples" while the C parser only accepts "n-sample", so every construction hit ERROR() and aborted the interpreter. It was absent from every test parametrize list, which is why this went unnoticed. Added regression tests for BeladySize and Belady. - WTinyLFU's window_size was described as a fraction of the main cache. WTinyLFU.c scales the total cache size by it and gives the main cache the remainder, so the docstring and both translations now say total. Also completed the CacheBase type stub, which was missing its admissioner parameter. Co-Authored-By: Claude Opus 5 (1M context) [Fix] Lower macOS deployment target to 11.0 The pin tracked the GitHub runner OS rather than a real requirement (see 77790b4, "As GitHub upgrades its macOS machine to 15.0"). Those are unrelated: the deployment target sets the minimum macOS a wheel supports, so following the runner silently dropped every macOS below it. scikit-build-core gives cmake.define the highest precedence when deriving the wheel's platform tag, so every build produced a macosx_15_0_arm64 wheel. That is incompatible with the macos-14 matrix leg, and with the python-build-standalone interpreters from `uv python install`, which target macOS 11.0 -- so `uv pip install -e .` rejected the wheel it had just built. libCacheSim itself pins no deployment target anywhere and needs only C++17, so 11.0 (the floor for Apple Silicon) is safe. Wheels are now tagged macosx_11_0_arm64 and install on macOS 11+ instead of 15+ only. --- .gitignore | 1 + docs/mkdocs.yml | 9 +- docs/src/en/api.md | 373 ++++++++++++- docs/src/en/developer.md | 215 +++++++- docs/src/en/examples/analysis.md | 153 +++++- docs/src/en/examples/reader.md | 1 + docs/src/en/examples/simulation.md | 242 ++++++++- docs/src/en/faq.md | 3 +- docs/src/en/getting_started/installation.md | 118 ++++- docs/src/en/getting_started/quickstart.md | 61 +-- docs/src/zh/api.md | 559 +++++++++----------- docs/src/zh/developer.md | 184 +++++++ docs/src/zh/examples.md | 488 ----------------- docs/src/zh/examples/analysis.md | 133 +++++ docs/src/zh/examples/plugins.md | 57 ++ docs/src/zh/examples/reader.md | 56 ++ docs/src/zh/examples/simulation.md | 346 ++++++++++++ docs/src/zh/faq.md | 21 + docs/src/zh/getting_started/installation.md | 102 ++++ docs/src/zh/getting_started/quickstart.md | 194 +++++++ docs/src/zh/index.md | 85 +-- docs/src/zh/quickstart.md | 183 ------- libcachesim/__init__.py | 6 + libcachesim/__init__.pyi | 35 +- libcachesim/cache.py | 116 +++- pyproject.toml | 4 +- src/export_cache.cpp | 3 + src/libCacheSim | 2 +- tests/test_cache.py | 91 +++- 29 files changed, 2728 insertions(+), 1113 deletions(-) create mode 100644 docs/src/zh/developer.md delete mode 100644 docs/src/zh/examples.md create mode 100644 docs/src/zh/examples/analysis.md create mode 100644 docs/src/zh/examples/plugins.md create mode 100644 docs/src/zh/examples/reader.md create mode 100644 docs/src/zh/examples/simulation.md create mode 100644 docs/src/zh/faq.md create mode 100644 docs/src/zh/getting_started/installation.md create mode 100644 docs/src/zh/getting_started/quickstart.md delete mode 100644 docs/src/zh/quickstart.md diff --git a/.gitignore b/.gitignore index 0816e08..0388f44 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,7 @@ venv.bak/ # mkdocs documentation /site +docs/site/ # mypy .mypy_cache/ diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e7ee002..8c306aa 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -9,9 +9,9 @@ docs_dir: src nav: - Home: - libCacheSim Python: index.md - - Getting Started: - - getting_started/quickstart.md + - Getting Started: - getting_started/installation.md + - getting_started/quickstart.md - Examples: - examples/reader.md - examples/simulation.md @@ -85,11 +85,14 @@ plugins: build: true nav_translations: Home: 首页 - Getting Started: 快速开始 + Getting Started: 入门指南 User Guide: 用户指南 Developer Guide: 开发者指南 API Reference: API参考 Examples: 使用示例 + FAQ: 常见问题 + General: 总览 + API Documentation: API 文档 markdown_extensions: - attr_list diff --git a/docs/src/en/api.md b/docs/src/en/api.md index 8c3fc1b..6c5e97a 100644 --- a/docs/src/en/api.md +++ b/docs/src/en/api.md @@ -1,3 +1,374 @@ # API Reference -[TBD] \ No newline at end of file +This page documents everything exported from the `libcachesim` package. For task-oriented +guides, see [Cache Simulation](examples/simulation.md), [Trace Reader](examples/reader.md), +[Trace Analysis](examples/analysis.md), and [Plugin System](examples/plugins.md). + +```python +import libcachesim as lcs +``` + +## Requests and objects + +### `Request` + +A single access in a trace. Readers fill and return `Request` objects; caches consume them. + +```python +Request( + obj_size: int = 1, + op: ReqOp = ReqOp.OP_NOP, + valid: bool = True, + obj_id: int = 0, + clock_time: int = 0, + hv: int = 0, + next_access_vtime: int = -2, + ttl: int = 0, +) +``` + +| Attribute | Type | Description | +|---|---|---| +| `obj_id` | `int` | Object identifier | +| `obj_size` | `int` | Object size in bytes | +| `clock_time` | `int` | Wall-clock timestamp of the request | +| `next_access_vtime` | `int` | Logical time of this object's next access; only present in oracle traces, and required by `Belady` / `BeladySize` | +| `op` | `ReqOp` | Operation type | +| `ttl` | `int` | Time-to-live in seconds | +| `hv` | `int` | Hash value | +| `valid` | `bool` | `False` marks the end of a trace; iteration stops on it | + +### `CacheObject` + +Returned by `Cache.find`, `insert`, `evict`, and `to_evict`. Exposes read-only `obj_id` and +`obj_size`. + +## Enumerations + +### `ReqOp` + +``` +OP_NOP OP_GET OP_GETS OP_SET OP_ADD +OP_CAS OP_REPLACE OP_APPEND OP_PREPEND OP_DELETE +OP_INCR OP_DECR OP_READ OP_WRITE OP_UPDATE +OP_INVALID +``` + +### `TraceType` + +``` +CSV_TRACE BIN_TRACE PLAIN_TXT_TRACE +ORACLE_GENERAL_TRACE LCS_TRACE VSCSI_TRACE +TWR_TRACE TWRNS_TRACE ORACLE_SIM_TWR_TRACE +ORACLE_SYS_TWR_TRACE ORACLE_SIM_TWRNS_TRACE ORACLE_SYS_TWRNS_TRACE +VALPIN_TRACE UNKNOWN_TRACE +``` + +`UNKNOWN_TRACE` is the default and asks `TraceReader` to infer the format from the file name. + +### `SamplerType` + +``` +SPATIAL_SAMPLER TEMPORAL_SAMPLER SHARDS_SAMPLER INVALID_SAMPLER +``` + +## Configuration objects + +### `ReaderInitParam` + +Controls how a trace file is parsed. Passed to `TraceReader` as `reader_init_params`. + +```python +ReaderInitParam( + binary_fmt_str: str = "", + ignore_obj_size: bool = False, + ignore_size_zero_req: bool = True, + obj_id_is_num: bool = True, + obj_id_is_num_set: bool = False, + cap_at_n_req: int = -1, + block_size: int = -1, + has_header: bool = False, + has_header_set: bool = False, + delimiter: str = ",", + trace_start_offset: int = 0, + sampler: Optional[Sampler] = None, +) +``` + +| Attribute | Description | +|---|---| +| `ignore_obj_size` | Treat every object as size 1, so the byte miss ratio equals the request miss ratio | +| `ignore_size_zero_req` | Skip requests whose object size is zero | +| `obj_id_is_num` | Parse object IDs as integers rather than strings | +| `cap_at_n_req` | Stop after this many requests; `-1` for no cap | +| `block_size` | Block size for block-level traces; `-1` to disable | +| `has_header` | Whether a CSV trace has a header row | +| `delimiter` | Field separator for CSV traces | +| `trace_start_offset` | Byte offset at which to start reading | +| `binary_fmt_str` | Struct format string for `BIN_TRACE` | +| `sampler` | Optional `Sampler` applied while reading | + +CSV field positions are set as attributes after construction, and are **1-indexed**: +`time_field`, `obj_id_field`, `obj_size_field`, `op_field`, `ttl_field`, `cnt_field`, +`tenant_field`, `next_access_vtime_field`, `n_feature_fields`. + +### `CommonCacheParams` + +The parameter bundle every cache is built from. Constructed for you by the cache classes; you +only encounter it directly as the argument to a `PluginCache` init hook. + +| Attribute | Type | +|---|---| +| `cache_size` | `int` | +| `default_ttl` | `int` | +| `hashpower` | `int` | +| `consider_obj_metadata` | `bool` | + +### `AnalysisOption` and `AnalysisParam` + +Configuration for `TraceAnalyzer`. Fields and defaults are documented in +[Trace Analysis](examples/analysis.md#selecting-analyses). + +## Caches + +### `CacheBase` + +Base class of every cache. See +[Working with individual requests](examples/simulation.md#working-with-individual-requests) for +the full method table. + +```python +process_trace(reader: ReaderProtocol, start_req: int = 0, max_req: int = -1) -> tuple[float, float] +``` + +Replays the trace and returns `(request_miss_ratio, byte_miss_ratio)`. With a C-backed reader +the whole loop runs in C++ with the GIL released; with a Python reader it falls back to a +Python loop. + +Other methods: `get`, `find`, `can_insert`, `insert`, `need_eviction`, `evict`, `remove`, +`to_evict`, `get_occupied_byte`, `get_n_obj`, `set_cache_size`, `print_cache`. Read-only +properties: `cache_size`, `cache_name`. + +### Cache algorithms + +All take the common arguments `cache_size`, `default_ttl=25920000`, `hashpower=24`, +`consider_obj_metadata=False`, `admissioner=None`, `reader=None`, plus the extras below. See +[Cache Simulation](examples/simulation.md#caches) for what each algorithm does. + +| Class | Algorithm-specific parameters | +|---|---| +| `LHD` | — | +| `LRU` | — | +| `FIFO` | — | +| `LFU` | — | +| `ARC` | — | +| `Clock` | `init_freq=0`, `n_bit_counter=1` | +| `Random` | — | +| `S3FIFO` | `small_size_ratio=0.1`, `ghost_size_ratio=0.9`, `move_to_main_threshold=2` | +| `Sieve` | — | +| `LIRS` | — | +| `TwoQ` | `a_in_size_ratio=0.25`, `a_out_size_ratio=0.5` | +| `SLRU` | — | +| `WTinyLFU` | `main_cache="SLRU"`, `window_size=0.01` | +| `LeCaR` | `update_weight=True`, `lru_weight=0.5` | +| `LFUDA` | — | +| `ClockPro` | `init_ref=0`, `init_ratio_cold=0.5` | +| `Cacheus` | — | +| `Belady` | — | +| `BeladySize` | `n_samples=128` | +| `LRUProb` | `prob=0.5` | +| `FlashProb` | `ram_size_ratio=0.05`, `disk_admit_prob=0.2`, `ram_cache="LRU"`, `disk_cache="FIFO"` | +| `Size` | — | +| `GDSF` | — | +| `Hyperbolic` | — | +| `ThreeLCache` | `objective="byte-miss-ratio"` — requires `-DENABLE_3L_CACHE=ON` | +| `GLCache` | `segment_size=100`, `n_merge=2`, `type="learned"`, `rank_intvl=0.02`, `merge_consecutive_segs=True`, `train_source_y="online"`, `retrain_intvl=86400` — requires `-DENABLE_GLCACHE=ON` | +| `LRB` | `objective="byte-miss-ratio"` — requires `-DENABLE_LRB=ON` | + +`cache_size` may be an `int` (bytes) or a `float` in `(0, 1]` (a fraction of the working set, +which requires `reader`). See +[Cache size as a ratio](examples/simulation.md#cache-size-as-a-ratio). + +### `PluginCache` + +```python +PluginCache( + cache_size: int | float, + cache_init_hook: Callable, + cache_hit_hook: Callable, + cache_miss_hook: Callable, + cache_eviction_hook: Callable, + cache_remove_hook: Callable, + cache_free_hook: Optional[Callable] = None, + cache_name: str = "PythonHookCache", + default_ttl: int = 25920000, + hashpower: int = 24, + consider_obj_metadata: bool = False, + admissioner: Optional[AdmissionerBase] = None, + reader: Optional[ReaderProtocol] = None, +) +``` + +Hook signatures are documented in [Plugin System](examples/plugins.md#plugincache). +`set_hooks(...)` replaces the hooks on an existing instance. + +## Admission policies + +Every admissioner derives from `AdmissionerBase`, which exposes `admit(req)`, `update(req, +cache_size)`, `clone()`, and `free()`. Pass an instance as the `admissioner` argument of any +cache. + +| Class | Parameters | +|---|---| +| `BloomFilterAdmissioner` | — | +| `ProbAdmissioner` | `prob: float = None` | +| `SizeAdmissioner` | `size_threshold: int = None` | +| `SizeProbabilisticAdmissioner` | `exponent: float = None` | +| `AdaptSizeAdmissioner` | `max_iteration: int = None`, `reconf_interval: int = None` | +| `PluginAdmissioner` | `admissioner_name` plus five hooks | + +Leaving a parameter as `None` uses the C library's own default; those defaults are listed in +[Admission Policies](examples/simulation.md#admission-policies). + +```python +PluginAdmissioner( + admissioner_name: str, + admissioner_init_hook: Callable, + admissioner_admit_hook: Callable, + admissioner_clone_hook: Callable, + admissioner_update_hook: Callable, + admissioner_free_hook: Callable, +) +``` + +## Readers + +### `ReaderProtocol` + +A `runtime_checkable` protocol describing what any reader must provide, so custom readers can be +used wherever `TraceReader` is accepted: + +```python +get_num_of_req() -> int +read_one_req() -> Request +skip_n_req(n: int) -> int +reset() -> None +close() -> None +clone() -> ReaderProtocol +get_working_set_size() -> tuple[int, int] +__iter__() / __next__() / __len__() +``` + +### `TraceReader` + +```python +TraceReader( + trace: str | Reader, + trace_type: TraceType = TraceType.UNKNOWN_TRACE, + reader_init_params: Optional[ReaderInitParam] = None, +) +``` + +`trace` is a local path or an `s3://bucket/key` URI; S3 objects are downloaded and cached +locally on first use. See [Trace Reader](examples/reader.md). + +- Iteration and `len()` work directly on the reader. +- Indexing and slicing are supported: `reader[0]`, `reader[:100]`, `reader[-100:]`. A slice + returns an iterator over a cloned reader, leaving the original position untouched. +- Navigation: `read_one_req()`, `read_first_req(req)`, `read_last_req(req)`, `skip_n_req(n)`, + `go_back_one_req()`, `read_one_req_above()`, `set_read_pos(pos)`, `reset()`, `close()`, + `clone()`. +- `get_working_set_size()` returns `(n_object, n_byte)`. +- Read-only properties include `n_read_req`, `n_total_req`, `n_req_left`, `trace_path`, + `file_size`, `trace_type`, `trace_format`, `is_zstd_file`, `cloned`, `sampler`, + `read_direction`, `lcs_ver`, `init_params`. `ignore_obj_size`, `ignore_size_zero_req`, and + `block_size` are writable. + +`read_one_req()` raises `RuntimeError` at end of trace, whereas iteration stops cleanly. + +### `SyntheticReader` + +Generates requests in memory — no trace file needed. + +```python +SyntheticReader( + num_of_req: int, + obj_size: int = 4000, + time_span: int = 604800, + start_obj_id: int = 0, + seed: Optional[int] = None, + alpha: float = 1.0, + dist: str = "zipf", + num_objects: Optional[int] = None, +) +``` + +`dist` is `"zipf"` or `"uniform"`; `alpha` only applies to Zipf. `num_objects` defaults to +`num_of_req`. Invalid arguments raise `ValueError`. + +!!! note + `SyntheticReader` is a pure-Python reader (`c_reader = False`). `process_trace` still works, + but falls back to a Python loop, and `TraceAnalyzer` rejects it outright. + +### Trace generators + +```python +create_zipf_requests(num_objects, num_requests, alpha=1.0, obj_size=4000, + time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] + +create_uniform_requests(num_objects, num_requests, obj_size=4000, + time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] +``` + +Both return an **iterator**, not a list; wrap in `list(...)` if you need to replay them twice. + +## `TraceAnalyzer` + +```python +TraceAnalyzer( + reader: ReaderProtocol, + output_path: str, + analysis_param: Optional[AnalysisParam] = None, + analysis_option: Optional[AnalysisOption] = None, +) +``` + +Methods: `run()`, `cleanup()`. Requires a C-backed reader; anything else raises +`ReaderException`. See [Trace Analysis](examples/analysis.md). + +## `Util` + +Static helpers for trace conversion and simulation. + +```python +Util.convert_to_oracleGeneral(reader, ofilepath, output_txt=False, remove_size_change=False) +Util.convert_to_lcs(reader, ofilepath, output_txt=False, remove_size_change=False, lcs_ver=1) +Util.process_trace(cache, reader, start_req=0, max_req=-1) -> tuple[float, float] +``` + +- `convert_to_oracleGeneral` rewrites a trace into the oracleGeneral format, computing the + next-access field needed by `Belady`. +- `convert_to_lcs` writes the LCS format; `lcs_ver` selects the version (1–8). +- `Util.process_trace` is equivalent to `cache.process_trace(...)` but requires a C-backed + reader, raising `ValueError` otherwise. + +## Metadata + +`libcachesim.__version__` is the installed package version; `libcachesim.__doc__` is the +extension module's docstring. + +## Exceptions + +The bindings use standard Python exceptions: + +| Exception | Raised when | +|---|---| +| `ValueError` | Invalid arguments — a malformed S3 URI, a `cache_size` float outside `(0, 1]` or without a `reader`, an unsupported `dist`, or a non-C reader passed to `Util.process_trace` | +| `TypeError` | `reader_init_params` is not a `ReaderInitParam`; a reader is indexed with something other than an `int` or `slice` | +| `IndexError` | Reader index out of range, or end of trace reached while seeking | +| `RuntimeError` | `read_one_req()` called past the end of a trace | +| `ImportError` | Constructing `LRB`, `ThreeLCache`, or `GLCache` in a build compiled without the corresponding flag | +| `ReaderException` | A non-C reader was passed to `TraceAnalyzer` | + +`ReaderException` is not re-exported at package level; import it from +`libcachesim.trace_analyzer` if you need to catch it by type. diff --git a/docs/src/en/developer.md b/docs/src/en/developer.md index 8fcc019..e494d07 100644 --- a/docs/src/en/developer.md +++ b/docs/src/en/developer.md @@ -1,3 +1,216 @@ # Developer Guide -[TBD] \ No newline at end of file +This page is for people working *on* libCacheSim Python, rather than with it. If you only want +to use the library, start with [Installation](getting_started/installation.md). + +## Repository layout + +``` +libCacheSim-python/ +├── libcachesim/ # The Python package +│ ├── __init__.py # Public API surface (__all__) +│ ├── __init__.pyi # Type stubs for the compiled extension +│ ├── cache.py # Cache wrapper classes +│ ├── admissioner.py # Admission policy wrappers +│ ├── trace_reader.py # TraceReader, with S3 support +│ ├── synthetic_reader.py +│ ├── trace_analyzer.py +│ ├── protocols.py # ReaderProtocol +│ └── util.py # Trace conversion helpers +├── src/ # pybind11 bindings (C++) +│ ├── export_cache.cpp +│ ├── export_reader.cpp +│ ├── export_analyzer.cpp +│ ├── export_admissioner.cpp +│ └── libCacheSim/ # git submodule: the C library +├── tests/ +├── examples/ +├── scripts/ +└── docs/ +``` + +The general shape is: the C library does the work, `src/*.cpp` exposes it through pybind11, and +`libcachesim/*.py` wraps that in an ergonomic Python API. + +## Getting set up + +The C library is a git submodule, so the checkout must be recursive: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +git submodule update --init --recursive +pip install -e ".[dev]" +``` + +The `dev` extra brings in `pytest`, `ruff`, `mypy`, and `pre-commit`. + +`scripts/install.sh` does all of the above and then runs the tests. Pass `--all` to enable the +optional learned algorithms: + +```bash +bash scripts/install.sh --all +``` + +### Build system + +Builds go through [scikit-build-core](https://scikit-build-core.readthedocs.io/), configured in +`pyproject.toml`. It configures and builds the bundled C library first, then compiles the +bindings with Ninja. Optional features are toggled with `CMAKE_ARGS`: + +```bash +CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" pip install -e . +``` + +Note that `pyproject.toml` sets `build-dir = "build"`, so incremental rebuilds reuse previous +output. If a rebuild picks up stale artefacts, remove `build/` and `src/libCacheSim/build/`. + +## Testing + +```bash +python -m pytest tests/ +``` + +!!! important + `pyproject.toml` sets `addopts = [..., "-m", "not optional"]`, so a plain `pytest` run + **skips** the tests for the optional learned algorithms. To run those, you need a build with + the corresponding CMake flags and an explicit marker selection: + + ```bash + python -m pytest tests/ -m optional + ``` + +The suite also sets `filterwarnings = ["error", ...]`, so a new warning will fail the build. + +Several tests download traces from the public S3 bucket, so they need network access on first +run; subsequent runs use the local cache. + +## Code style + +There is no `pre-commit` configuration checked in, so run the tools directly: + +```bash +# Lint and auto-fix Python +ruff check libcachesim/ tests/ examples/ +ruff format libcachesim/ + +# Type-check +mypy libcachesim/ + +# Format C++ (uses the checked-in .clang-format) +clang-format -i src/*.cpp src/*.h +``` + +`ruff` is configured in `pyproject.toml` with a 120-character line length and the `E`, `F`, +`UP`, `B`, `SIM`, and `G` rule sets. + +## Adding a cache algorithm + +Adding an algorithm that already exists in the C library takes five steps: + +1. **Bind it.** In `src/export_cache.cpp`, expose the algorithm's `*_init` function following + the pattern used by the existing ones. +2. **Wrap it.** In `libcachesim/cache.py`, add a class deriving from `CacheBase`. Build the + common parameters with the existing `_create_common_params(...)` helper — this is also what + gives every cache the fractional `cache_size` behaviour for free — and pass any + algorithm-specific settings as a `cache_specific_params` string: + + ```python + class MyAlgo(CacheBase): + """My algorithm + + Special parameters: + my_param: what it controls (default: 0.5) + """ + + def __init__( + self, + cache_size: int | float, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, + my_param: float = 0.5, + admissioner: AdmissionerBase = None, + reader: ReaderProtocol = None, + ): + cache_specific_params = f"my-param={my_param}" + super().__init__( + _cache=MyAlgo_init( + _create_common_params( + cache_size, default_ttl, hashpower, consider_obj_metadata, reader + ), + cache_specific_params, + ), + admissioner=admissioner, + ) + ``` + + Note that the C library's parameter names are hyphenated (`my-param`) even though the Python + keyword is underscored. +3. **Export it.** Add the class to both the import block and `__all__` in + `libcachesim/__init__.py`, and add a stub to `libcachesim/__init__.pyi`. +4. **Test it.** Add a case to `tests/test_cache.py`. If the algorithm depends on an optional + build flag, mark it `@pytest.mark.optional`. +5. **Document it.** Add a section to `docs/src/en/examples/simulation.md` and a row to the table + in `docs/src/en/api.md`. + +If the algorithm needs a third-party library, guard the import as `ThreeLCache` and `GLCache` +do — a `try`/`except ImportError` that re-raises with the `CMAKE_ARGS` incantation the user +needs. + +## Documentation + +The site is built with [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) plus +`mkdocs-static-i18n`. Sources live in `docs/src//`, and the locale folders must mirror +each other: a page at `en/examples/reader.md` is translated by a file at +`zh/examples/reader.md`. A file at any other path is simply never rendered. Missing +translations fall back to English, so partial translation is fine. + +To build and preview locally: + +```bash +bash scripts/build_docs.sh --serve # http://127.0.0.1:8000 +``` + +Or directly: + +```bash +pip install -r docs/requirements.txt +cd docs && mkdocs build --clean --strict +``` + +Always build with `--strict` before opening a PR — that is what CI runs, and it turns broken +internal links into build failures. + +Prefer relative links between pages (`../faq.md`) over absolute URLs to the published site, so +that links keep working in local builds and under the locale fallback. + +## Continuous integration + +| Workflow | Trigger | What it does | +|---|---|---| +| `.github/workflows/build.yml` | changes under `src/`, `libcachesim/`, `tests/` | Builds and tests on Ubuntu and macOS (Intel and Apple Silicon) across Python 3.10–3.13, and separately builds the docs | +| `.github/workflows/docs.yml` | changes under `docs/` | Builds with `--strict` and deploys to GitHub Pages on `main` | +| `.github/workflows/pypi-release.yml` | published release, or manual dispatch | Builds wheels with cibuildwheel and publishes to PyPI | + +Note that `build.yml` only triggers on changes to code paths, and `docs.yml` only on `docs/`, so +a docs-only PR will not run the test suite and vice versa. + +## Releasing + +Releases are cut by publishing a GitHub release, which triggers `pypi-release.yml`. + +Wheels are built by [cibuildwheel](https://cibuildwheel.pypa.io/) using the configuration in +`pyproject.toml`, which builds manylinux and macOS wheels for every supported CPython version +with **all three** optional algorithms enabled, and verifies each wheel by importing it and +running both the default and `optional` test selections. + +`scripts/sync_version.py` keeps the version in `pyproject.toml` in step with +`src/libCacheSim/version.txt` from the submodule. + +## Contributing + +Bug reports and feature requests belong in +[GitHub issues](https://github.com/cacheMon/libCacheSim-python/issues/new/choose). For changes +to the simulation core itself rather than the bindings, the right repository is +[1a1a11a/libCacheSim](https://github.com/1a1a11a/libCacheSim). diff --git a/docs/src/en/examples/analysis.md b/docs/src/en/examples/analysis.md index ccdcb6f..18563f9 100644 --- a/docs/src/en/examples/analysis.md +++ b/docs/src/en/examples/analysis.md @@ -1,3 +1,154 @@ # Trace Analysis -[TBD] \ No newline at end of file +Beyond simulating caches, libCacheSim can characterise a workload directly: request rate, object +size distribution, reuse distance, popularity, and more. This is done with `TraceAnalyzer`, a +thin wrapper over the analyzer in the underlying libCacheSim lib. + +## Basic usage + +`TraceAnalyzer` takes a reader, an output path prefix, and two optional configuration objects: + +```python +import libcachesim as lcs + +# Step 1: Open a trace (see the Trace Reader page for details) +URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +reader = lcs.TraceReader( + trace=URI, + trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params=lcs.ReaderInitParam(ignore_obj_size=False), +) + +# Step 2: Run the analysis +analyzer = lcs.TraceAnalyzer(reader, "example_analysis") +analyzer.run() +``` + +The constructor arguments are: + +- `reader: ReaderProtocol` — the trace to analyse. +- `output_path: str` — prefix for the generated result files. +- `analysis_option: AnalysisOption` (optional) — which analyses to run. Defaults to + `AnalysisOption()`. +- `analysis_param: AnalysisParam` (optional) — tuning knobs for those analyses. Defaults to + `AnalysisParam()`. + +!!! important + The analyzer runs entirely in the C++ backend, so it only accepts a C-backed reader — in + practice, [`TraceReader`](reader.md). Passing a `SyntheticReader` raises + `ReaderException: Only C/C++ reader is supported`. To analyse a synthetic workload, write it + out first with `Util.convert_to_oracleGeneral` and reopen it with `TraceReader`. + +## Selecting analyses + +Each field of `AnalysisOption` toggles one analysis. Five are on by default: + +| Option | Default | What it measures | +|---|---|---| +| `req_rate` | `True` | Request and object rate over time | +| `access_pattern` | `True` | Access pattern of individual objects over time | +| `size` | `True` | Object size distribution, by request and by object | +| `reuse` | `True` | Reuse time / reuse distance distribution | +| `popularity` | `True` | Object popularity distribution (Zipf fit) | +| `ttl` | `False` | TTL distribution (only meaningful for traces with TTLs) | +| `popularity_decay` | `False` | How object popularity decays with age | +| `lifetime` | `False` | Object lifetime distribution | +| `create_future_reuse_ccdf` | `False` | Experimental — CCDF of future reuse | +| `prob_at_age` | `False` | Experimental — access probability as a function of age | +| `size_change` | `False` | How object sizes change across accesses | + +Analyses are independent, so disabling the ones you do not need makes the run considerably +faster on large traces: + +```python +analysis_option = lcs.AnalysisOption( + req_rate=True, # Keep basic request rate analysis + access_pattern=False, + size=True, # Keep size analysis + reuse=False, + popularity=False, + ttl=False, + popularity_decay=False, + lifetime=False, + create_future_reuse_ccdf=False, + prob_at_age=False, + size_change=False, +) + +analyzer = lcs.TraceAnalyzer(reader, "example_analysis", analysis_option=analysis_option) +analyzer.run() +``` + +## Tuning the analyses + +`AnalysisParam` controls how the enabled analyses behave: + +| Parameter | Default | Meaning | +|---|---|---| +| `access_pattern_sample_ratio_inv` | `10` | Inverse sampling ratio for the access-pattern analysis — a value of `n` keeps roughly `1/n` of the data | +| `track_n_popular` | `10` | How many of the most popular objects to report request counts for | +| `track_n_hit` | `5` | How many "X-hit wonder" buckets to track, i.e. the number of objects accessed exactly once, twice, ... `track_n_hit` times | +| `time_window` | `60` | Width, in seconds, of the buckets used for time-series output | +| `warmup_time` | `0` | Seconds of trace to skip before collecting statistics | + +```python +analysis_param = lcs.AnalysisParam( + track_n_popular=4, + track_n_hit=4, + time_window=300, +) + +analyzer = lcs.TraceAnalyzer( + reader, "example_analysis", + analysis_option=analysis_option, + analysis_param=analysis_param, +) +analyzer.run() +``` + +!!! warning + Two constraints are easy to trip over: + + - `warmup_time` must be an exact multiple of `time_window`; the analyzer errors out + otherwise, because the popularity-decay computation depends on that relationship. + - The popularity and reuse analyses need a reasonably large working set to produce + meaningful output. On a tiny trace — a handful of distinct objects — set + `track_n_popular` and `track_n_hit` no higher than the number of objects, or disable + `popularity` and `reuse` altogether. + +## Results + +`run()` writes plain-text result files, all sharing the `output_path` prefix. Each enabled +analysis contributes at least one file — the size analysis writes `example_analysis.size`, and +some analyses additionally emit time-windowed variants such as +`example_analysis.sizeWindow_w60_req`. + +```python +with open("example_analysis.size") as f: + print(f.read()) +``` + +A summary of the run — trace path, request and object counts, compulsory miss ratio, mean object +size, mean frequency, time span, and the X-hit-wonder and popularity histograms — is written to +a file named `stat` in the **current working directory**. Note that this path is fixed rather +than derived from `output_path`, and the analyzer *appends* to it, so results from successive +runs accumulate in the same file. + +When you are finished, `cleanup()` releases the analyzer's internal state: + +```python +analyzer.cleanup() +``` + +## Working set size + +For the single most common statistic — how much data the trace touches — you do not need the +analyzer at all. `TraceReader` exposes it directly: + +```python +n_obj, n_byte = reader.get_working_set_size() +print(f"{n_obj} unique objects, {n_byte} bytes") +``` + +This is also what a fractional `cache_size` is measured against; see +[Cache Simulation](simulation.md#cache-size-as-a-ratio). diff --git a/docs/src/en/examples/reader.md b/docs/src/en/examples/reader.md index fd8e84d..ae2d348 100644 --- a/docs/src/en/examples/reader.md +++ b/docs/src/en/examples/reader.md @@ -7,6 +7,7 @@ We support a unified trace reader to open trace files in different format and re `TraceReader` class is the core of this functionality. When we create an instance of `TraceReader`, we open a trace file for read requests. `TraceReader` accepts three arguments: + - `trace: str | TraceReader`: A trace path or other trace instance. The trace path can be a file path on your local machine (e.g., ~/data/trace.oracleGeneral.zst) or an S3 URI (e.g., s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst). - `trace_type: TraceType` (optional): If not given, it will be infered according to the file name. - `reader_init_params: ReaderInitParam` (optional): If not given, will use default params for reader initialization. diff --git a/docs/src/en/examples/simulation.md b/docs/src/en/examples/simulation.md index 1378339..c036d9c 100644 --- a/docs/src/en/examples/simulation.md +++ b/docs/src/en/examples/simulation.md @@ -2,7 +2,7 @@ ## Basic Usage -The cache classes are the core of cache simulation. When an instance of a cache is creates (e.g., `LRU`, `S3FIFO`), we can configure the cache size and any cache-specific parameters such as promotion thresholds. +The cache classes are the core of cache simulation. When an instance of a cache is created (e.g., `LRU`, `S3FIFO`), we can configure the cache size and any cache-specific parameters such as promotion thresholds. ```py import libcachesim as lcs @@ -45,17 +45,81 @@ req_miss_ratio, byte_miss_ratio = cache.process_trace(reader) print(f"Request miss ratio: {req_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") ``` +`process_trace` accepts two further arguments for restricting the replay to part of the trace: + +```py +# Skip the first 10,000 requests, then process the next 1,000 +req_miss_ratio, byte_miss_ratio = cache.process_trace(reader, start_req=10_000, max_req=1_000) +``` + +- `start_req: int` - Index of the first request to process (default: `0`) +- `max_req: int` - Maximum number of requests to process; `-1` means the whole trace (default: `-1`) + +!!! note + `process_trace` rewinds the reader before replaying, so you do not need to call `reset()` + yourself. The *cache*, however, keeps its state across calls — create a fresh cache for each + configuration you measure, rather than reusing one already warmed by a previous run. + +## Cache size as a ratio + +`cache_size` accepts either an absolute byte count (`int`) or a fraction of the trace's working +set (`float`). A float must be in `(0, 1]` and requires the `reader` argument, which is used to +call `reader.get_working_set_size()`: + +```py +# 10% of the trace's total working set size in bytes +cache = lcs.S3FIFO( + cache_size=0.1, + reader=reader, # Required when cache_size is a float +) +``` + +Passing a float without a `reader`, or a float outside `(0, 1]`, raises `ValueError`. Note that +`1024` and `1024.0` therefore mean very different things - the former is 1 KiB, the latter is +rejected. + +## Working with individual requests + +`process_trace` runs the whole replay in the C++ backend and is by far the fastest option. When +you need to observe or intervene per request, `CacheBase` exposes the underlying operations: + +```py +for req in reader: + hit = cache.get(req) # Look up, and insert on miss (evicting as needed) + if not hit: + print(f"miss on {req.obj_id}, cache now holds {cache.get_n_obj()} objects") +``` + +| Method | Description | +|---|---| +| `get(req)` | Full request path: look up `req`, and on a miss insert it, evicting if necessary. Returns `True` on a hit. | +| `find(req, update_cache=True)` | Look up an object without inserting on miss. Set `update_cache=False` for a side-effect-free probe. | +| `can_insert(req)` | Whether the object would be admitted. | +| `insert(req)` | Insert an object without checking for space. | +| `need_eviction(req)` | Whether inserting `req` would require an eviction. | +| `to_evict(req)` | The object that would be evicted next, without evicting it. | +| `evict(req)` | Evict one object according to the policy. | +| `remove(obj_id)` | Remove a specific object. Returns `False` if it was not cached. | +| `get_occupied_byte()` | Bytes currently occupied. | +| `get_n_obj()` | Number of objects currently cached. | +| `set_cache_size(new_size)` | Resize the cache in place. | +| `print_cache()` | A string describing the current cache state, useful when debugging. | + +The `cache_size` and `cache_name` properties are read-only. + ## Caches + The following cache classes all inherit from `CacheBase` and share a common interface, sharing the following arguments in all cache classes unless otherwise specified: -- `cache_size: int` -- `default_ttl: int` (optional) -- `hashpower: int` (optional) -- `consider_obj_metadata: bool` (optional) -- `admissioner: AdmissionerBase` (optional) +- `cache_size: int | float` - Cache size in bytes, or a fraction of the working set (see [above](#cache-size-as-a-ratio)) +- `default_ttl: int` (optional) - Default TTL in seconds (default: `25920000`, i.e. 300 days) +- `hashpower: int` (optional) - Log2 of the initial hash table size (default: `24`) +- `consider_obj_metadata: bool` (optional) - Whether per-object cache metadata counts against the cache size (default: `False`) +- `admissioner: AdmissionerBase` (optional) - Admission policy placed in front of the cache (default: `None`) +- `reader: ReaderProtocol` (optional) - Only needed when `cache_size` is a fraction (default: `None`) ### LHD -**Lest Hit Density** evicts objects based on each objects expected hits-per-space-consumed (hit density). +**Least Hit Density** evicts objects based on each objects expected hits-per-space-consumed (hit density). - *No additional parameters beyond the common arguments* @@ -64,6 +128,11 @@ The following cache classes all inherit from `CacheBase` and share a common inte - *No additional parameters beyond the common arguments* +### LRUK +**LRU-K** evicts the object with the largest backward K-distance, that is the one whose K-th most recent access is oldest. Objects seen fewer than K times have an infinite backward K-distance and are evicted first in FIFO order, so a single access is not enough to earn a place in the cache. + +- `k: int` - Number of recent accesses tracked per object (default: `2`) + ### FIFO **First-In, First-Out** evicts objects in order regardless of frequency or recency. @@ -74,7 +143,7 @@ The following cache classes all inherit from `CacheBase` and share a common inte - *No additional parameters beyond the common arguments* -### Arc +### ARC **Adaptive Replacement Cache** a hybrid algorithm which balances recency and frequency. - *No additional parameters beyond the common arguments* @@ -82,7 +151,7 @@ The following cache classes all inherit from `CacheBase` and share a common inte ### Clock **Clock** is an low-complexity approximation of `LRU`. -- `int_freq: int` - Initial frequency counter value which is used for new objects (default: `0`) +- `init_freq: int` - Initial frequency counter value which is used for new objects (default: `0`) - `n_bit_counter: int` - Number of bits used for the frequency counter (default: `1`) ### Random @@ -91,61 +160,155 @@ The following cache classes all inherit from `CacheBase` and share a common inte - *No additional parameters beyond the common arguments* ### S3FIFO -[TBD] +**Simple, Scalable FIFO** splits the cache into a small FIFO queue for newly admitted objects and a main FIFO queue for objects that prove popular, backed by a ghost queue of recently evicted identifiers. One-hit wonders are demoted quickly out of the small queue instead of polluting the main one. + +- `small_size_ratio: float` - Fraction of the cache given to the small queue (default: `0.1`) +- `ghost_size_ratio: float` - Size of the ghost queue as a fraction of the cache (default: `0.9`) +- `move_to_main_threshold: int` - Number of accesses in the small queue before an object is promoted to the main queue (default: `2`) ### Sieve -[TBD] +**Sieve** sweeps a hand over a FIFO queue and evicts the first object whose visited bit is unset, clearing the bits it passes. It achieves LRU-like miss ratios while keeping FIFO's simplicity, with no promotion on hit. + +- *No additional parameters beyond the common arguments* ### LIRS -[TBD] +**Low Inter-reference Recency Set** ranks objects by the recency of their second-to-last access rather than their last, which lets it distinguish genuinely hot objects from ones touched a single time during a scan. + +- *No additional parameters beyond the common arguments* ### TwoQ -[TBD] +**2Q** admits new objects to a FIFO queue (`Ain`), promoting them into an LRU main queue only if they are accessed again while their identifier is still in the ghost queue (`Aout`). + +- `a_in_size_ratio: float` - Size of the `Ain` queue as a fraction of the cache (default: `0.25`) +- `a_out_size_ratio: float` - Size of the `Aout` ghost queue as a fraction of the cache (default: `0.5`) ### SLRU -[TBD] +**Segmented LRU** partitions the cache into ordered LRU segments; an object is promoted one segment on each hit and demoted towards eviction as newer objects arrive. + +- *No additional parameters beyond the common arguments* + +### MQ +**Multi-Queue** spreads objects over a hierarchy of LRU queues ordered by access frequency, promoting an object a queue at a time as it is reused and demoting it again if it goes untouched for its lifetime. Eviction always takes the tail of the lowest non-empty queue, and a FIFO ghost queue (`Qout`) remembers the frequency of evicted objects so a quick return restores an object to its former queue. + +- `n_queue: int` - Number of queues in the hierarchy, must be in `[1, 64]` (default: `8`) +- `lifetime: int` - Requests an object may go unaccessed before it is demoted a queue (default: `10000`) +- `qout_size_ratio: float` - Size of the `Qout` ghost queue as a multiple of the cache size, must be in `(0, 64]` (default: `4.0`) ### WTinyLFU -[TBD] +**Window TinyLFU** places a small LRU window in front of a larger main cache, and uses a frequency sketch to decide whether an object leaving the window deserves to displace the main cache's eviction candidate. + +- `main_cache: str` - Eviction algorithm used for the main cache (default: `"SLRU"`) +- `window_size: float` - Size of the LRU window as a fraction of the **total** cache size, must be in `[0, 1)`; the main cache receives the remainder (default: `0.01`) ### LeCaR -[TBD] +**Learning Cache Replacement** maintains both an LRU and an LFU candidate and picks between them using weights updated by regret minimisation, so it adapts as the workload shifts between recency- and frequency-friendly. + +- `update_weight: bool` - Whether to keep learning the weights during the replay (default: `True`) +- `lru_weight: float` - Initial probability of choosing the LRU candidate; the LFU weight is `1 - lru_weight` (default: `0.5`) ### LFUDA -[TBD] +**LFU with Dynamic Aging** is `LFU` plus a global age value added to each object's priority on access, so objects that were popular long ago eventually age out instead of pinning the cache. + +- *No additional parameters beyond the common arguments* ### ClockPro -[TBD] +**CLOCK-Pro** approximates `LIRS` using CLOCK hands, tracking hot and cold pages plus a test period for recently evicted cold pages. + +- `init_ref: int` - Initial reference count given to newly admitted objects (default: `0`) +- `init_ratio_cold: float` - Initial fraction of the cache designated as cold (default: `0.5`) + +### Clock2QPlus +**Clock-2Q+** is a 2Q variant that puts a small FIFO probationary queue in front of a Clock main cache. A ghost queue tracks recently evicted identifiers, and an adaptive correlation window tunes how long an object must survive the FIFO queue before it is considered worth promoting. + +- `fifo_size_ratio: float` - Size of the FIFO queue as a fraction of the cache (default: `0.1`) +- `ghost_size_ratio: float` - Size of the ghost queue as a fraction of the cache (default: `0.9`) +- `move_to_main_threshold: int` - Number of hits before an object is promoted to the main cache (default: `1`) +- `corr_window_ratio: float` - Initial size of the correlation window as a fraction of the FIFO queue (default: `0.5`) ### Cacheus -[TBD] +**Cacheus** builds on `LeCaR`, adding lightweight adaptation of the learning rate and scan/churn detection so that it degrades gracefully on the workloads where `LeCaR` struggles. + +- *No additional parameters beyond the common arguments* ### Belady -[TBD] +**Belady's MIN** is the optimal offline policy: it evicts the object whose next access is furthest in the future. It is not implementable online and exists as a lower bound on the achievable miss ratio. + +- *No additional parameters beyond the common arguments* + +!!! important + `Belady` and `BeladySize` read `req.next_access_vtime`, which only oracle traces carry. Use a + trace in `ORACLE_GENERAL_TRACE` format (as in the examples on this page); on an ordinary trace + the future-access field is absent and the results are meaningless. ### BeladySize -[TBD] +**Size-aware Belady** extends `Belady` to variable object sizes, choosing among a sample of candidates by both next access time and size. + +- `n_samples: int` - Number of objects sampled when picking a victim (default: `128`) ### LRUProb -[TBD] +**LRU with Probabilistic Promotion** behaves like `LRU`, except an object is only moved to the head of the queue with probability `prob`. Lower values make it behave more like `FIFO` at lower promotion cost. + +- `prob: float` - Probability of promoting an object on a hit (default: `0.5`) ### FlashProb -[TBD] +**FlashProb** models a two-tier RAM-plus-flash cache, admitting objects evicted from RAM to the flash tier only probabilistically so as to limit write amplification on the flash device. + +- `ram_size_ratio: float` - Size of the RAM tier as a fraction of the total cache (default: `0.05`) +- `disk_admit_prob: float` - Probability of admitting an object to the disk tier (default: `0.2`) +- `ram_cache: str` - Eviction algorithm used for the RAM tier (default: `"LRU"`) +- `disk_cache: str` - Eviction algorithm used for the disk tier (default: `"FIFO"`) + +### Size +**Size** evicts the largest object first, maximising the number of objects retained. Useful as a baseline on workloads with highly variable object sizes. + +- *No additional parameters beyond the common arguments* ### GDSF -[TBD] +**GreedyDual-Size with Frequency** ranks objects by frequency divided by size, offset by a global aging factor, favouring small and frequently accessed objects. + +- *No additional parameters beyond the common arguments* ### Hyperbolic -[TBD] +**Hyperbolic** samples a few objects on each eviction and evicts the one with the lowest access count divided by time resident in the cache, approximating a priority ordering without maintaining a global structure. + +- *No additional parameters beyond the common arguments* ### ThreeLCache -[TBD] +**3LCache** is a learned policy that predicts, for each object, how valuable it is to retain, and organises objects across three levels accordingly. + +- `objective: str` - Metric the learned model optimises for (default: `"byte-miss-ratio"`) + +!!! warning + Requires a build with `-DENABLE_3L_CACHE=ON`. See [Installation](../getting_started/installation.md#optional-eviction-algorithms). + Constructing it in a build without the flag raises `ImportError`. ### GLCache -[TBD] +**Group-Learned Cache** groups objects into segments, learns to predict each segment's future utility, and evicts by merging the least useful segments rather than making per-object decisions. + +- `segment_size: int` - Number of objects per segment (default: `100`) +- `n_merge: int` - Number of segments merged in one eviction (default: `2`) +- `type: str` - Cache type, e.g. the learned variant or a baseline (default: `"learned"`) +- `rank_intvl: float` - How often segments are re-ranked, as a fraction of the cache (default: `0.02`) +- `merge_consecutive_segs: bool` - Whether merges are restricted to consecutive segments (default: `True`) +- `train_source_y: str` - Source of the training labels (default: `"online"`) +- `retrain_intvl: int` - Seconds between model retraining (default: `86400`) + +!!! warning + Requires a build with `-DENABLE_GLCACHE=ON`. See [Installation](../getting_started/installation.md#optional-eviction-algorithms). + Constructing it in a build without the flag raises `ImportError`. ### LRB -[TBD] +**Learning Relaxed Belady** trains a model to approximate Belady's decision online, evicting objects predicted to have a distant next access. + +- `objective: str` - Metric the learned model optimises for (default: `"byte-miss-ratio"`) + +!!! warning + Requires a build with `-DENABLE_LRB=ON`. See [Installation](../getting_started/installation.md#optional-eviction-algorithms). + Constructing it in a build without the flag raises `ImportError`. + +### PluginCache +**PluginCache** lets you implement an eviction policy in pure Python via hook functions, with no +compilation. It is documented separately in [Plugin System](plugins.md). ## Admission Policies @@ -174,3 +337,26 @@ Implements **AdaptSize**, a feedback-driven policy that periodically adjusts its - `max_iteration: int` (optional) - Maximum number of iterators for parameter tuning (default: `15`) - `reconf_interval: int` (optional) - Interval (with respect to request count) at which the threshold is re-evaluated (default: `30_000`) + +### PluginAdmissioner +Lets you implement an admission policy in Python via hook functions. See +[Plugin System](plugins.md#pluginadmissioner). + +## Comparing algorithms + +Because every cache exposes the same interface, sweeping over algorithms is straightforward. +`process_trace` rewinds the reader before it starts, so the same reader can be handed to each +run without an explicit `reset()`: + +```py +import libcachesim as lcs + +URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +reader = lcs.TraceReader(trace=URI, trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE) + +CACHE_SIZE = 1024 * 1024 +for cls in (lcs.LRU, lcs.FIFO, lcs.ARC, lcs.S3FIFO, lcs.Sieve): + cache = cls(cache_size=CACHE_SIZE) + req_miss_ratio, byte_miss_ratio = cache.process_trace(reader) + print(f"{cache.cache_name:>10}: req {req_miss_ratio:.4f} byte {byte_miss_ratio:.4f}") +``` diff --git a/docs/src/en/faq.md b/docs/src/en/faq.md index 7f23120..d0f0efe 100644 --- a/docs/src/en/faq.md +++ b/docs/src/en/faq.md @@ -2,13 +2,14 @@ 1. How to resolve when pip install fails? - See [installation](https://cachemon.github.io/libCacheSim-python/getting_started/installation/). + See [Installation](getting_started/installation.md). 2. Get an error message like "cannot find Python package" when building. The reason is that building Python bindings requires Python's development headers and libraries. If you have administrative privileges, you can use your system's package manager to install the required package. For example: + * **Debian/Ubuntu**: `sudo apt install python3-dev` * **RHEL/CentOS/Fedora**: `sudo yum install python3-devel` * **macOS**: Installing Python with Homebrew (`brew install python`) is usually sufficient. diff --git a/docs/src/en/getting_started/installation.md b/docs/src/en/getting_started/installation.md index 7e0f4ef..e8a7280 100644 --- a/docs/src/en/getting_started/installation.md +++ b/docs/src/en/getting_started/installation.md @@ -1,3 +1,119 @@ # Installation -[TBD] \ No newline at end of file +## Requirements + +| | | +|---|---| +| **OS** | Linux / macOS | +| **Python** | 3.10 -- 3.13 | +| **Architecture** | x86_64 / aarch64 | + +Windows is not supported. + +## Install from PyPI + +Pre-built wheels are published to [PyPI](https://pypi.org/project/libcachesim/), so in most +cases no compiler is needed: + +```bash +pip install libcachesim +``` + +We recommend [uv](https://docs.astral.sh/uv/) to create and manage the environment: + +```bash +uv venv --python 3.12 --seed +source .venv/bin/activate +uv pip install libcachesim +``` + +Verify the installation: + +```bash +python -c "import libcachesim; print(libcachesim.__version__)" +``` + +## Optional eviction algorithms + +Three algorithms depend on third-party machine-learning libraries and are therefore guarded by +CMake options, all of which default to `OFF`: + +| Algorithm | CMake option | Depends on | +|---|---|---| +| [`LRB`](../examples/simulation.md#lrb) | `ENABLE_LRB` | LightGBM | +| [`ThreeLCache`](../examples/simulation.md#threelcache) | `ENABLE_3L_CACHE` | LightGBM | +| [`GLCache`](../examples/simulation.md#glcache) | `ENABLE_GLCACHE` | XGBoost | + +!!! note + The wheels released on PyPI are built with all three enabled. You only need the steps below + if no wheel matches your platform and pip falls back to building from source, or if you are + building from a source checkout yourself. + +Install the third-party dependencies first: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +bash scripts/install_deps.sh + +# If you cannot install system packages (e.g., no sudo access) +bash scripts/install_deps_user.sh +``` + +Then reinstall, passing the options through `CMAKE_ARGS`. Add `--no-cache-dir` to force a +rebuild rather than reusing a cached wheel: + +```bash +# Enable one algorithm +CMAKE_ARGS="-DENABLE_LRB=ON" pip install libcachesim --no-cache-dir + +# Or enable all three +CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" \ + pip install libcachesim --no-cache-dir +``` + +!!! important + Because the options default to `OFF`, a plain source build silently **omits** these three + algorithms — constructing `LRB`, `ThreeLCache`, or `GLCache` then fails at runtime. + Conversely, turning an option `ON` without its dependency installed makes CMake fail during + configuration (`LIGHTGBM_PATH not found`, or a missing `xgboost` package). Run the + dependency script first. + +## Install from source + +The C library [libCacheSim](https://github.com/1a1a11a/libCacheSim) is vendored as a git +submodule, so the checkout must be recursive: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +git submodule update --init --recursive +pip install . +``` + +`scripts/install.sh` wraps the whole flow — it updates the submodule, installs the package in +editable mode, checks that the import works, and runs the test suite: + +```bash +bash scripts/install.sh + +# Same, but with all optional algorithms enabled +bash scripts/install.sh --all +``` + +Building the extension requires a C++17 compiler, CMake ≥ 3.15, and Ninja. The build is driven +by [scikit-build-core](https://scikit-build-core.readthedocs.io/), which configures and builds +the bundled C library before compiling the [pybind11](https://pybind11.readthedocs.io/) +bindings. + +## Troubleshooting + +Two failures are common enough to have their own entries in the +[FAQ](../faq.md): + +- `pip install` fails to find a suitable wheel and the source build errors out. +- The build reports `cannot find Python package` — Python's development headers are missing, or + Python lives in a non-standard location. + +For anything else, please +[open an issue](https://github.com/cacheMon/libCacheSim-python/issues/new/choose). diff --git a/docs/src/en/getting_started/quickstart.md b/docs/src/en/getting_started/quickstart.md index c3f9f63..02cd549 100644 --- a/docs/src/en/getting_started/quickstart.md +++ b/docs/src/en/getting_started/quickstart.md @@ -5,7 +5,7 @@ This guide will help you get started with libCacheSim. ## Prerequisites - OS: Linux / macOS -- Python: 3.9 -- 3.13 +- Python: 3.10 -- 3.13 ## Installation @@ -19,34 +19,8 @@ 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 (may need to add `--no-cache-dir` to force it to build from scratch): - -```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 -``` +See [Installation](installation.md) for building from source, and for enabling the LRB, +ThreeLCache, and GLCache eviction algorithms, which are excluded from source builds by default. ## Cache Simulation @@ -98,6 +72,22 @@ The above example demonstrates the basic workflow of using `libcachesim` for cac This workflow applies to most cache algorithms and trace types, making it easy to get started and customize your experiments. +### Sizing the cache relative to the trace + +Absolute byte counts are awkward when comparing traces of very different sizes. Passing +`cache_size` as a `float` in `(0, 1]` instead interprets it as a fraction of the trace's working +set, which requires handing the cache a `reader`: + +```python +cache = lcs.S3FIFO( + cache_size=0.1, # 10% of the trace's working set size in bytes + reader=reader, # Required whenever cache_size is a float +) +``` + +An `int` is always an absolute byte count, so `1024` means 1 KiB while `1024.0` is out of range +and raises `ValueError`. + ## Trace Analysis Here is an example demonstrating how to use `TraceAnalyzer`. @@ -158,7 +148,7 @@ Here is an example of implement `LRU` via the plugin system. from collections import OrderedDict from typing import Any - from libcachesim import PluginCache, LRU, CommonCacheParams, Request + from libcachesim import PluginCache, LRU, CommonCacheParams, Request, SyntheticReader def init_hook(_: CommonCacheParams) -> Any: return OrderedDict() @@ -190,7 +180,7 @@ Here is an example of implement `LRU` via the plugin system. cache_name="Plugin_LRU", ) - reader = lcs.SyntheticReader(num_objects=1000, num_of_req=10000, obj_size=1) + reader = 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}") @@ -199,6 +189,11 @@ Here is an example of implement `LRU` via the plugin system. 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. +## Next steps - - +- [Trace Reader](../examples/reader.md) — opening local and S3 traces, slicing, and iteration +- [Cache Simulation](../examples/simulation.md) — every eviction algorithm and admission policy, + with their parameters +- [Trace Analysis](../examples/analysis.md) — workload characterisation with `TraceAnalyzer` +- [Plugin System](../examples/plugins.md) — hook signatures for custom caches and admissioners +- [API Reference](../api.md) — the complete exported surface diff --git a/docs/src/zh/api.md b/docs/src/zh/api.md index 5bb9814..30dc559 100644 --- a/docs/src/zh/api.md +++ b/docs/src/zh/api.md @@ -1,385 +1,338 @@ # API 参考 -本页面提供 libCacheSim Python 绑定的详细 API 文档。 +本页记录 `libcachesim` 包对外导出的全部内容。若需面向任务的教程,请参阅[缓存模拟](examples/simulation.md)、[Trace Reader](examples/reader.md)、[Trace 分析](examples/analysis.md)和[插件系统](examples/plugins.md)。 -## 核心类 +```python +import libcachesim as lcs +``` + +## 请求与对象 -### 缓存类 +### `Request` -所有缓存类都继承自基础缓存接口,并提供以下方法: +trace 中的一次访问。reader 负责填充并返回 `Request` 对象,缓存则消费它们。 ```python -class Cache: - """基础缓存接口。""" - - def get(self, obj_id: int, obj_size: int = 1) -> bool: - """从缓存请求对象。 - - 参数: - obj_id: 对象标识符 - obj_size: 对象大小(字节) - - 返回: - 如果缓存命中返回 True,缓存缺失返回 False - """ - - def get_hit_ratio(self) -> float: - """获取当前缓存命中率。""" - - def get_miss_ratio(self) -> float: - """获取当前缓存缺失率。""" - - def get_num_hits(self) -> int: - """获取缓存命中总数。""" - - def get_num_misses(self) -> int: - """获取缓存缺失总数。""" +Request( + obj_size: int = 1, + op: ReqOp = ReqOp.OP_NOP, + valid: bool = True, + obj_id: int = 0, + clock_time: int = 0, + hv: int = 0, + next_access_vtime: int = -2, + ttl: int = 0, +) ``` -### 可用的缓存算法 +| 属性 | 类型 | 说明 | +|---|---|---| +| `obj_id` | `int` | 对象标识 | +| `obj_size` | `int` | 对象大小(字节) | +| `clock_time` | `int` | 该请求的墙钟时间戳 | +| `next_access_vtime` | `int` | 该对象下一次访问的逻辑时间;仅 oracle trace 中存在,`Belady` / `BeladySize` 需要它 | +| `op` | `ReqOp` | 操作类型 | +| `ttl` | `int` | 存活时间(秒) | +| `hv` | `int` | 哈希值 | +| `valid` | `bool` | `False` 表示 trace 结束,遍历会随之停止 | -```python -# 基础算法 -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: ... - -# 高级算法 -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: ... -``` +### `CacheObject` -### TraceReader +由 `Cache.find`、`insert`、`evict` 和 `to_evict` 返回,暴露只读的 `obj_id` 和 `obj_size`。 -```python -class TraceReader: - """读取各种格式的跟踪文件。""" - - def __init__(self, trace_path: str, trace_type: TraceType, - reader_params: ReaderInitParam = None): - """初始化跟踪读取器。 - - 参数: - trace_path: 跟踪文件路径 - trace_type: 跟踪格式类型 - reader_params: 可选的读取器配置 - """ - - def __iter__(self): - """迭代跟踪中的请求。""" - - def reset(self): - """重置读取器到跟踪开始。""" - - def skip(self, n: int): - """跳过 n 个请求。""" - - def clone(self): - """创建读取器的副本。""" -``` +## 枚举类型 -### SyntheticReader +### `ReqOp` -```python -class SyntheticReader: - """生成合成工作负载。""" - - def __init__(self, num_objects: int, num_requests: int, - distribution: str = "zipf", alpha: float = 1.0, - obj_size: int = 1, seed: int = None): - """初始化合成读取器。 - - 参数: - num_objects: 唯一对象数量 - num_requests: 要生成的总请求数 - distribution: 分布类型("zipf","uniform") - alpha: Zipf 偏斜参数 - obj_size: 对象大小(字节) - seed: 用于可重现性的随机种子 - """ +``` +OP_NOP OP_GET OP_GETS OP_SET OP_ADD +OP_CAS OP_REPLACE OP_APPEND OP_PREPEND OP_DELETE +OP_INCR OP_DECR OP_READ OP_WRITE OP_UPDATE +OP_INVALID ``` -### TraceAnalyzer +### `TraceType` -```python -class TraceAnalyzer: - """分析跟踪特征。""" - - def __init__(self, trace_path: str, trace_type: TraceType, - reader_params: ReaderInitParam = None): - """初始化跟踪分析器。""" - - def get_num_requests(self) -> int: - """获取总请求数。""" - - def get_num_objects(self) -> int: - """获取唯一对象数。""" - - def get_working_set_size(self) -> int: - """获取工作集大小。""" +``` +CSV_TRACE BIN_TRACE PLAIN_TXT_TRACE +ORACLE_GENERAL_TRACE LCS_TRACE VSCSI_TRACE +TWR_TRACE TWRNS_TRACE ORACLE_SIM_TWR_TRACE +ORACLE_SYS_TWR_TRACE ORACLE_SIM_TWRNS_TRACE ORACLE_SYS_TWRNS_TRACE +VALPIN_TRACE UNKNOWN_TRACE ``` -## 枚举和常量 +`UNKNOWN_TRACE` 是默认值,它要求 `TraceReader` 根据文件名推断格式。 -### TraceType +### `SamplerType` -```python -class TraceType: - """支持的跟踪文件格式。""" - CSV_TRACE = "csv" - BINARY_TRACE = "binary" - ORACLE_GENERAL_TRACE = "oracle" - PLAIN_TXT_TRACE = "txt" +``` +SPATIAL_SAMPLER TEMPORAL_SAMPLER SHARDS_SAMPLER INVALID_SAMPLER ``` -### SamplerType +## 配置对象 -```python -class SamplerType: - """采样策略。""" - SPATIAL_SAMPLER = "spatial" - TEMPORAL_SAMPLER = "temporal" -``` +### `ReaderInitParam` -### ReqOp +控制 trace 文件的解析方式,通过 `reader_init_params` 传给 `TraceReader`。 ```python -class ReqOp: - """请求操作类型。""" - READ = "read" - WRITE = "write" - DELETE = "delete" +ReaderInitParam( + binary_fmt_str: str = "", + ignore_obj_size: bool = False, + ignore_size_zero_req: bool = True, + obj_id_is_num: bool = True, + obj_id_is_num_set: bool = False, + cap_at_n_req: int = -1, + block_size: int = -1, + has_header: bool = False, + has_header_set: bool = False, + delimiter: str = ",", + trace_start_offset: int = 0, + sampler: Optional[Sampler] = None, +) ``` -## 数据结构 +| 属性 | 说明 | +|---|---| +| `ignore_obj_size` | 把每个对象都当作大小为 1,于是字节缺失率等于请求缺失率 | +| `ignore_size_zero_req` | 跳过对象大小为零的请求 | +| `obj_id_is_num` | 把对象 ID 解析为整数而非字符串 | +| `cap_at_n_req` | 读到该请求数后停止;`-1` 表示不限制 | +| `block_size` | 块级 trace 的块大小;`-1` 表示禁用 | +| `has_header` | CSV trace 是否带表头行 | +| `delimiter` | CSV trace 的字段分隔符 | +| `trace_start_offset` | 开始读取的字节偏移 | +| `binary_fmt_str` | `BIN_TRACE` 的 struct 格式串 | +| `sampler` | 读取时应用的可选 `Sampler` | -### Request +CSV 的字段位置在构造之后以属性方式设置,且**从 1 开始计数**:`time_field`、`obj_id_field`、`obj_size_field`、`op_field`、`ttl_field`、`cnt_field`、`tenant_field`、`next_access_vtime_field`、`n_feature_fields`。 -```python -class Request: - """表示缓存请求。""" - - def __init__(self): - self.obj_id: int = 0 - self.obj_size: int = 1 - self.timestamp: int = 0 - self.op: str = "read" -``` +### `CommonCacheParams` + +每个缓存都由这组参数构建而成。缓存类会替你构造它;你唯一会直接接触它的场合,是把它作为参数传给 `PluginCache` 的 init hook。 + +| 属性 | 类型 | +|---|---| +| `cache_size` | `int` | +| `default_ttl` | `int` | +| `hashpower` | `int` | +| `consider_obj_metadata` | `bool` | + +### `AnalysisOption` 与 `AnalysisParam` -### ReaderInitParam +`TraceAnalyzer` 的配置。各字段及其默认值见 [Trace 分析](examples/analysis.md#selecting-analyses)。 + +## 缓存 + +### `CacheBase` + +所有缓存的基类。完整的方法列表见[逐请求操作](examples/simulation.md#working-with-individual-requests)。 ```python -class ReaderInitParam: - """跟踪读取器的配置参数。""" - - 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 - - # 字段映射(从1开始索引) - 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 +process_trace(reader: ReaderProtocol, start_req: int = 0, max_req: int = -1) -> tuple[float, float] ``` -### Sampler +回放 trace 并返回 `(request_miss_ratio, byte_miss_ratio)`。使用 C 实现的 reader 时,整个循环在 C++ 中运行并释放 GIL;使用 Python reader 时,则回退为 Python 循环。 + +其他方法:`get`、`find`、`can_insert`、`insert`、`need_eviction`、`evict`、`remove`、`to_evict`、`get_occupied_byte`、`get_n_obj`、`set_cache_size`、`print_cache`。只读属性:`cache_size`、`cache_name`。 + +### 缓存算法 + +所有算法都接受公共参数 `cache_size`、`default_ttl=25920000`、`hashpower=24`、`consider_obj_metadata=False`、`admissioner=None`、`reader=None`,以及下表中的额外参数。各算法的原理见[缓存模拟](examples/simulation.md#caches)。 + +| 类 | 算法特有参数 | +|---|---| +| `LHD` | — | +| `LRU` | — | +| `FIFO` | — | +| `LFU` | — | +| `ARC` | — | +| `Clock` | `init_freq=0`、`n_bit_counter=1` | +| `Random` | — | +| `S3FIFO` | `small_size_ratio=0.1`、`ghost_size_ratio=0.9`、`move_to_main_threshold=2` | +| `Sieve` | — | +| `LIRS` | — | +| `TwoQ` | `a_in_size_ratio=0.25`、`a_out_size_ratio=0.5` | +| `SLRU` | — | +| `WTinyLFU` | `main_cache="SLRU"`、`window_size=0.01` | +| `LeCaR` | `update_weight=True`、`lru_weight=0.5` | +| `LFUDA` | — | +| `ClockPro` | `init_ref=0`、`init_ratio_cold=0.5` | +| `Cacheus` | — | +| `Belady` | — | +| `BeladySize` | `n_samples=128` | +| `LRUProb` | `prob=0.5` | +| `FlashProb` | `ram_size_ratio=0.05`、`disk_admit_prob=0.2`、`ram_cache="LRU"`、`disk_cache="FIFO"` | +| `Size` | — | +| `GDSF` | — | +| `Hyperbolic` | — | +| `ThreeLCache` | `objective="byte-miss-ratio"` —— 需要 `-DENABLE_3L_CACHE=ON` | +| `GLCache` | `segment_size=100`、`n_merge=2`、`type="learned"`、`rank_intvl=0.02`、`merge_consecutive_segs=True`、`train_source_y="online"`、`retrain_intvl=86400` —— 需要 `-DENABLE_GLCACHE=ON` | +| `LRB` | `objective="byte-miss-ratio"` —— 需要 `-DENABLE_LRB=ON` | + +`cache_size` 可以是 `int`(字节),也可以是 `(0, 1]` 区间内的 `float`(工作集的比例,此时需要提供 `reader`)。参见[按比例设置缓存大小](examples/simulation.md#cache-size-as-a-ratio)。 + +### `PluginCache` ```python -class Sampler: - """请求采样配置。""" - - def __init__(self, sample_ratio: float = 1.0, - type: str = "spatial"): - """初始化采样器。 - - 参数: - sample_ratio: 要采样的请求比例(0.0-1.0) - type: 采样类型("spatial" 或 "temporal") - """ - self.sample_ratio = sample_ratio - self.type = type +PluginCache( + cache_size: int | float, + cache_init_hook: Callable, + cache_hit_hook: Callable, + cache_miss_hook: Callable, + cache_eviction_hook: Callable, + cache_remove_hook: Callable, + cache_free_hook: Optional[Callable] = None, + cache_name: str = "PythonHookCache", + default_ttl: int = 25920000, + hashpower: int = 24, + consider_obj_metadata: bool = False, + admissioner: Optional[AdmissionerBase] = None, + reader: Optional[ReaderProtocol] = None, +) ``` -## 工具函数 +各 hook 的签名见[插件系统](examples/plugins.md#plugincache)。`set_hooks(...)` 可以替换已有实例上的 hook。 + +## 准入策略 + +所有准入器都派生自 `AdmissionerBase`,后者暴露 `admit(req)`、`update(req, cache_size)`、`clone()` 和 `free()`。把实例作为任意缓存的 `admissioner` 参数传入即可。 + +| 类 | 参数 | +|---|---| +| `BloomFilterAdmissioner` | — | +| `ProbAdmissioner` | `prob: float = None` | +| `SizeAdmissioner` | `size_threshold: int = None` | +| `SizeProbabilisticAdmissioner` | `exponent: float = None` | +| `AdaptSizeAdmissioner` | `max_iteration: int = None`、`reconf_interval: int = None` | +| `PluginAdmissioner` | `admissioner_name` 加五个 hook | -### 合成跟踪生成 +参数保持为 `None` 时会使用 C 库自身的默认值,这些默认值列在[准入策略](examples/simulation.md#admission-policies)中。 ```python -def create_zipf_requests(num_objects, num_requests, alpha, obj_size, seed=None): - """ - 创建 Zipf 分布的合成请求。 - - 参数: - num_objects (int): 唯一对象数量 - num_requests (int): 要生成的总请求数 - alpha (float): Zipf 偏斜参数(越高越偏斜) - obj_size (int): 每个对象的大小(字节) - seed (int, 可选): 随机种子,用于可重现性 - - 返回: - List[Request]: 生成的请求列表 - """ - -def create_uniform_requests(num_objects, num_requests, obj_size, seed=None): - """ - 创建均匀分布的合成请求。 - - 参数: - num_objects (int): 唯一对象数量 - num_requests (int): 要生成的总请求数 - obj_size (int): 每个对象的大小(字节) - seed (int, 可选): 随机种子,用于可重现性 - - 返回: - List[Request]: 生成的请求列表 - """ +PluginAdmissioner( + admissioner_name: str, + admissioner_init_hook: Callable, + admissioner_admit_hook: Callable, + admissioner_clone_hook: Callable, + admissioner_update_hook: Callable, + admissioner_free_hook: Callable, +) ``` -### 缓存算法 +## Reader + +### `ReaderProtocol` -可用的缓存算法及其工厂函数: +一个 `runtime_checkable` 的协议,描述任何 reader 必须提供的接口,因此自定义 reader 可以用在所有接受 `TraceReader` 的地方: ```python -# 基础算法 -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 - -# 高级算法 -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 - -# 实验性算法 -cache_3L(cache_size: int) -> Cache +get_num_of_req() -> int +read_one_req() -> Request +skip_n_req(n: int) -> int +reset() -> None +close() -> None +clone() -> ReaderProtocol +get_working_set_size() -> tuple[int, int] +__iter__() / __next__() / __len__() ``` -### 性能指标 +### `TraceReader` ```python -class CacheStats: - """缓存性能统计。""" - - 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: - """计算命中率。""" - total = self.hits + self.misses - return self.hits / total if total > 0 else 0.0 - - @property - def miss_ratio(self) -> float: - """计算缺失率。""" - return 1.0 - self.hit_ratio +TraceReader( + trace: str | Reader, + trace_type: TraceType = TraceType.UNKNOWN_TRACE, + reader_init_params: Optional[ReaderInitParam] = None, +) ``` -## 错误处理 +`trace` 可以是本地路径,也可以是 `s3://bucket/key` 形式的 URI;S3 对象会在首次使用时下载并缓存到本地。参见 [Trace Reader](examples/reader.md)。 + +- 可以直接对 reader 进行遍历和 `len()`。 +- 支持下标和切片:`reader[0]`、`reader[:100]`、`reader[-100:]`。切片返回的是一个基于克隆 reader 的迭代器,原 reader 的位置不受影响。 +- 定位相关方法:`read_one_req()`、`read_first_req(req)`、`read_last_req(req)`、`skip_n_req(n)`、`go_back_one_req()`、`read_one_req_above()`、`set_read_pos(pos)`、`reset()`、`close()`、`clone()`。 +- `get_working_set_size()` 返回 `(n_object, n_byte)`。 +- 只读属性包括 `n_read_req`、`n_total_req`、`n_req_left`、`trace_path`、`file_size`、`trace_type`、`trace_format`、`is_zstd_file`、`cloned`、`sampler`、`read_direction`、`lcs_ver`、`init_params`。`ignore_obj_size`、`ignore_size_zero_req` 和 `block_size` 可写。 -库使用标准的 Python 异常: +`read_one_req()` 在 trace 结束后调用会抛出 `RuntimeError`,而遍历则会正常结束。 -- `ValueError`: 无效参数或配置 -- `FileNotFoundError`: 跟踪文件未找到 -- `RuntimeError`: 底层 C++ 库的运行时错误 -- `MemoryError`: 内存不足条件 +### `SyntheticReader` -错误处理示例: +在内存中生成请求,无需 trace 文件。 ```python -try: - reader = lcs.TraceReader("nonexistent.csv", lcs.TraceType.CSV_TRACE) -except FileNotFoundError: - print("跟踪文件未找到") -except ValueError as e: - print(f"无效配置: {e}") +SyntheticReader( + num_of_req: int, + obj_size: int = 4000, + time_span: int = 604800, + start_obj_id: int = 0, + seed: Optional[int] = None, + alpha: float = 1.0, + dist: str = "zipf", + num_objects: Optional[int] = None, +) ``` -## 配置选项 +`dist` 取 `"zipf"` 或 `"uniform"`;`alpha` 仅对 Zipf 生效。`num_objects` 默认等于 `num_of_req`。参数非法时抛出 `ValueError`。 -### 读取器配置 +!!! note + `SyntheticReader` 是纯 Python 实现的 reader(`c_reader = False`)。`process_trace` 仍然可用,但会回退为 Python 循环,而 `TraceAnalyzer` 会直接拒绝它。 + +### Trace 生成器 ```python -reader_params = lcs.ReaderInitParam( - has_header=True, # CSV 有标题行 - delimiter=",", # 字段分隔符 - obj_id_is_num=True, # 对象 ID 是数字 - ignore_obj_size=False, # 不忽略对象大小 - ignore_size_zero_req=True, # 忽略零大小请求 - cap_at_n_req=1000000, # 限制请求数量 - block_size=4096, # 块大小(用于基于块的跟踪) - trace_start_offset=0, # 跳过初始请求 -) +create_zipf_requests(num_objects, num_requests, alpha=1.0, obj_size=4000, + time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] -# 字段映射(从1开始索引) -reader_params.time_field = 1 -reader_params.obj_id_field = 2 -reader_params.obj_size_field = 3 -reader_params.op_field = 4 +create_uniform_requests(num_objects, num_requests, obj_size=4000, + time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] ``` -### 采样配置 +两者返回的都是**迭代器**而非列表;如果需要重复回放,请用 `list(...)` 包一层。 + +## `TraceAnalyzer` ```python -sampler = lcs.Sampler( - sample_ratio=0.1, # 采样 10% 的请求 - type=lcs.SamplerType.SPATIAL_SAMPLER # 空间采样 +TraceAnalyzer( + reader: ReaderProtocol, + output_path: str, + analysis_param: Optional[AnalysisParam] = None, + analysis_option: Optional[AnalysisOption] = None, ) -reader_params.sampler = sampler ``` -## 线程安全 +方法:`run()`、`cleanup()`。要求使用 C 实现的 reader,否则抛出 `ReaderException`。参见 [Trace 分析](examples/analysis.md)。 -库为大多数用例提供线程安全操作: +## `Util` -- 单个缓存实例内的缓存操作是线程安全的 -- 可以并发使用多个读取器 -- 分析操作可以利用多线程 +用于 trace 转换和模拟的静态辅助方法。 -对于高并发场景,考虑为每个线程使用单独的缓存实例。 +```python +Util.convert_to_oracleGeneral(reader, ofilepath, output_txt=False, remove_size_change=False) +Util.convert_to_lcs(reader, ofilepath, output_txt=False, remove_size_change=False, lcs_ver=1) +Util.process_trace(cache, reader, start_req=0, max_req=-1) -> tuple[float, float] +``` -## 内存管理 +- `convert_to_oracleGeneral` 把 trace 改写为 oracleGeneral 格式,并计算 `Belady` 所需的下次访问字段。 +- `convert_to_lcs` 写出 LCS 格式;`lcs_ver` 用于选择版本(1–8)。 +- `Util.process_trace` 等价于 `cache.process_trace(...)`,但要求使用 C 实现的 reader,否则抛出 `ValueError`。 -库自动管理大多数操作的内存: +## 元信息 -- 缓存对象处理自己的内存分配 -- 跟踪读取器自动管理缓冲 -- 请求对象轻量且可重用 +`libcachesim.__version__` 是已安装的包版本;`libcachesim.__doc__` 是扩展模块的 docstring。 -对于大规模模拟,监控内存使用并考虑: +## 异常 -- 使用采样减少跟踪大小 -- 分块处理跟踪 -- 适当限制缓存大小 +绑定层使用标准的 Python 异常: -## 最佳实践 +| 异常 | 触发场景 | +|---|---| +| `ValueError` | 参数非法——S3 URI 格式错误、`cache_size` 为超出 `(0, 1]` 的 float 或缺少 `reader`、`dist` 取值不支持,或把非 C reader 传给了 `Util.process_trace` | +| `TypeError` | `reader_init_params` 不是 `ReaderInitParam`;用 `int` 或 `slice` 以外的类型给 reader 取下标 | +| `IndexError` | reader 下标越界,或定位过程中已到达 trace 末尾 | +| `RuntimeError` | trace 已结束后仍调用 `read_one_req()` | +| `ImportError` | 在未开启对应编译选项的构建中构造 `LRB`、`ThreeLCache` 或 `GLCache` | +| `ReaderException` | 把非 C reader 传给了 `TraceAnalyzer` | -1. **使用适当的缓存大小**: 根据模拟目标确定缓存大小 -2. **设置随机种子**: 用于合成跟踪的可重现结果 -3. **处理错误**: 始终将文件操作包装在 try-catch 块中 -4. **监控内存**: 对于大型跟踪,考虑采样或分块 -5. **使用线程**: 为分析任务利用多线程 -6. **验证跟踪**: 在模拟前检查跟踪格式和内容 +`ReaderException` 未在包级别重新导出;如果需要按类型捕获它,请从 `libcachesim.trace_analyzer` 导入。 diff --git a/docs/src/zh/developer.md b/docs/src/zh/developer.md new file mode 100644 index 0000000..8d41efb --- /dev/null +++ b/docs/src/zh/developer.md @@ -0,0 +1,184 @@ +# 开发者指南 + +本页面向的是*开发* libCacheSim Python 本身的人,而非它的使用者。如果你只是想使用这个库,请从[安装指南](getting_started/installation.md)开始。 + +## 仓库结构 + +``` +libCacheSim-python/ +├── libcachesim/ # Python 包 +│ ├── __init__.py # 对外 API(__all__) +│ ├── __init__.pyi # 编译扩展的类型存根 +│ ├── cache.py # 缓存包装类 +│ ├── admissioner.py # 准入策略包装类 +│ ├── trace_reader.py # TraceReader,支持 S3 +│ ├── synthetic_reader.py +│ ├── trace_analyzer.py +│ ├── protocols.py # ReaderProtocol +│ └── util.py # trace 转换辅助函数 +├── src/ # pybind11 绑定(C++) +│ ├── export_cache.cpp +│ ├── export_reader.cpp +│ ├── export_analyzer.cpp +│ ├── export_admissioner.cpp +│ └── libCacheSim/ # git 子模块:C 语言库 +├── tests/ +├── examples/ +├── scripts/ +└── docs/ +``` + +整体结构是:C 库负责实际计算,`src/*.cpp` 通过 pybind11 将其暴露出来,`libcachesim/*.py` 再把它封装成符合 Python 习惯的 API。 + +## 环境搭建 + +C 库是一个 git 子模块,因此检出时必须递归拉取: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +git submodule update --init --recursive +pip install -e ".[dev]" +``` + +`dev` 附加依赖会引入 `pytest`、`ruff`、`mypy` 和 `pre-commit`。 + +`scripts/install.sh` 会完成上述全部步骤并随后运行测试。加上 `--all` 可启用可选的学习型算法: + +```bash +bash scripts/install.sh --all +``` + +### 构建系统 + +构建通过 [scikit-build-core](https://scikit-build-core.readthedocs.io/) 完成,配置写在 `pyproject.toml` 中。它会先配置并构建内置的 C 库,再用 Ninja 编译绑定层。可选特性通过 `CMAKE_ARGS` 开关: + +```bash +CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" pip install -e . +``` + +注意 `pyproject.toml` 设置了 `build-dir = "build"`,因此增量重建会复用此前的产物。如果重建时读到了陈旧的中间产物,请删除 `build/` 和 `src/libCacheSim/build/`。 + +## 测试 + +```bash +python -m pytest tests/ +``` + +!!! important + `pyproject.toml` 中设置了 `addopts = [..., "-m", "not optional"]`,因此直接运行 `pytest` 会**跳过**可选学习型算法的测试。要运行这些测试,需要用对应的 CMake 选项构建,并显式指定 marker: + + ```bash + python -m pytest tests/ -m optional + ``` + +测试套件还设置了 `filterwarnings = ["error", ...]`,因此新出现的警告会直接导致构建失败。 + +有几个测试会从公开的 S3 存储桶下载 trace,因此首次运行需要网络;后续运行会使用本地缓存。 + +## 代码风格 + +仓库中并未提交 `pre-commit` 配置,因此请直接运行相关工具: + +```bash +# 检查并自动修复 Python 代码 +ruff check libcachesim/ tests/ examples/ +ruff format libcachesim/ + +# 类型检查 +mypy libcachesim/ + +# 格式化 C++(使用仓库中的 .clang-format) +clang-format -i src/*.cpp src/*.h +``` + +`ruff` 在 `pyproject.toml` 中配置,行宽为 120,启用了 `E`、`F`、`UP`、`B`、`SIM` 和 `G` 规则集。 + +## 新增一个缓存算法 + +若该算法在 C 库中已经存在,接入它需要五步: + +1. **绑定。** 在 `src/export_cache.cpp` 中,参照已有算法的写法暴露该算法的 `*_init` 函数。 +2. **封装。** 在 `libcachesim/cache.py` 中新增一个继承自 `CacheBase` 的类。用已有的 `_create_common_params(...)` 辅助函数来构造公共参数——这也是每个缓存能够免费获得按比例设置 `cache_size` 能力的原因——并把算法特有的设置以 `cache_specific_params` 字符串传入: + + ```python + class MyAlgo(CacheBase): + """My algorithm + + Special parameters: + my_param: what it controls (default: 0.5) + """ + + def __init__( + self, + cache_size: int | float, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, + my_param: float = 0.5, + admissioner: AdmissionerBase = None, + reader: ReaderProtocol = None, + ): + cache_specific_params = f"my-param={my_param}" + super().__init__( + _cache=MyAlgo_init( + _create_common_params( + cache_size, default_ttl, hashpower, consider_obj_metadata, reader + ), + cache_specific_params, + ), + admissioner=admissioner, + ) + ``` + + 注意 C 库的参数名使用连字符(`my-param`),而 Python 关键字参数使用下划线。 +3. **导出。** 把该类同时加入 `libcachesim/__init__.py` 的 import 块和 `__all__`,并在 `libcachesim/__init__.pyi` 中补上存根。 +4. **测试。** 在 `tests/test_cache.py` 中增加用例。如果该算法依赖可选的编译选项,请标记 `@pytest.mark.optional`。 +5. **写文档。** 在 `docs/src/en/examples/simulation.md` 中新增一节,并在 `docs/src/en/api.md` 的表格中增加一行。 + +如果该算法依赖第三方库,请像 `ThreeLCache` 和 `GLCache` 那样保护 import——用 `try`/`except ImportError` 捕获后重新抛出,并在消息中给出用户需要的 `CMAKE_ARGS` 写法。 + +## 文档 + +站点使用 [MkDocs Material](https://squidfunk.github.io/mkdocs-material/) 加 `mkdocs-static-i18n` 构建。源文件位于 `docs/src//`,各语言目录必须彼此镜像:`en/examples/reader.md` 这个页面的译文必须位于 `zh/examples/reader.md`。放在其他路径的文件根本不会被渲染。缺失的译文会回退到英文,因此部分翻译也是可以接受的。 + +本地构建与预览: + +```bash +bash scripts/build_docs.sh --serve # http://127.0.0.1:8000 +``` + +或者直接运行: + +```bash +pip install -r docs/requirements.txt +cd docs && mkdocs build --clean --strict +``` + +提 PR 之前请务必用 `--strict` 构建一次——CI 跑的就是这个命令,它会把失效的站内链接变成构建失败。 + +页面之间请优先使用相对链接(`../faq.md`),而不是指向已发布站点的绝对 URL,这样在本地构建和语言回退时链接仍然有效。 + +翻译中文页面时,如果某个标题会被其他页面以锚点链接引用,请用 `attr_list` 显式固定锚点 ID,例如 `## 按比例设置缓存大小 {#cache-size-as-a-ratio}`,这样跨页链接在两种语言下都能正常工作。 + +## 持续集成 + +| 工作流 | 触发条件 | 作用 | +|---|---|---| +| `.github/workflows/build.yml` | `src/`、`libcachesim/`、`tests/` 下的改动 | 在 Ubuntu 和 macOS(Intel 与 Apple Silicon)上针对 Python 3.10–3.13 构建并测试,同时单独构建文档 | +| `.github/workflows/docs.yml` | `docs/` 下的改动 | 以 `--strict` 构建,并在 `main` 分支上部署到 GitHub Pages | +| `.github/workflows/pypi-release.yml` | 发布 release,或手动触发 | 用 cibuildwheel 构建 wheel 并发布到 PyPI | + +注意 `build.yml` 只在代码路径变动时触发,而 `docs.yml` 只在 `docs/` 变动时触发,因此仅改文档的 PR 不会运行测试套件,反之亦然。 + +## 发布 + +发布流程由创建 GitHub release 触发,进而运行 `pypi-release.yml`。 + +wheel 由 [cibuildwheel](https://cibuildwheel.pypa.io/) 按 `pyproject.toml` 中的配置构建,会为所有受支持的 CPython 版本构建 manylinux 和 macOS wheel,并启用**全部三个**可选算法,同时通过导入 wheel 并分别运行默认测试和 `optional` 测试来做校验。 + +`scripts/sync_version.py` 负责让 `pyproject.toml` 中的版本号与子模块中的 `src/libCacheSim/version.txt` 保持一致。 + +## 参与贡献 + +Bug 报告和功能需求请提交到 [GitHub issues](https://github.com/cacheMon/libCacheSim-python/issues/new/choose)。如果改动涉及的是模拟内核本身而非绑定层,正确的仓库是 [1a1a11a/libCacheSim](https://github.com/1a1a11a/libCacheSim)。 diff --git a/docs/src/zh/examples.md b/docs/src/zh/examples.md deleted file mode 100644 index 0e85828..0000000 --- a/docs/src/zh/examples.md +++ /dev/null @@ -1,488 +0,0 @@ -# 示例和教程 - -本页提供使用 libCacheSim Python 绑定的实际示例和深入教程。 - -## 基础示例 - -### 简单缓存模拟 - -最基本的缓存模拟示例: - -```python -import libcachesim as lcs - -# 创建一个1MB大小的LRU缓存 -cache = lcs.LRU(cache_size=1024*1024) - -# 模拟一些请求 -requests = [ - (1, 100), # 对象1,大小100字节 - (2, 200), # 对象2,大小200字节 - (1, 100), # 对象1,再次访问(命中) - (3, 150), # 对象3,大小150字节 -] - -for obj_id, size in requests: - hit = cache.get(obj_id, size) - print(f"对象 {obj_id}: {'命中' if hit else '缺失'}") - -# 获取统计信息 -print(f"命中率: {cache.get_hit_ratio():.2%}") -``` - -### 跟踪文件处理 - -从CSV文件读取和处理跟踪: - -```python -import libcachesim as lcs - -# 配置跟踪读取器 -reader_params = lcs.ReaderInitParam() -reader_params.has_header = True -reader_params.delimiter = "," -reader_params.time_field = 1 -reader_params.obj_id_field = 2 -reader_params.obj_size_field = 3 - -# 创建跟踪读取器 -reader = lcs.TraceReader("workload.csv", lcs.TraceType.CSV_TRACE, reader_params) - -# 创建缓存 -cache = lcs.LRU(cache_size=1024*1024) - -# 处理跟踪 -request_count = 0 -for request in reader: - hit = cache.get(request.obj_id, request.obj_size) - request_count += 1 - - if request_count % 10000 == 0: - print(f"处理了 {request_count} 个请求,命中率: {cache.get_hit_ratio():.2%}") - -print(f"最终命中率: {cache.get_hit_ratio():.2%}") -``` - -## 合成工作负载生成 - -### Zipf分布请求 - -生成具有Zipf分布的合成工作负载: - -```python -import libcachesim as lcs - -# 创建Zipf分布的合成读取器 -reader = lcs.SyntheticReader( - num_objects=10000, - num_requests=100000, - distribution="zipf", - alpha=1.0, # Zipf偏斜参数 - obj_size=4096, - seed=42 # 为了可重现性 -) - -# 创建缓存 -cache = lcs.LRU(cache_size=10*1024*1024) # 10MB - -# 运行模拟 -for request in reader: - cache.get(request.obj_id, request.obj_size) - -print(f"Zipf工作负载 (α=1.0) 命中率: {cache.get_hit_ratio():.2%}") - -# 尝试不同的偏斜参数 -for alpha in [0.5, 1.0, 1.5, 2.0]: - reader = lcs.SyntheticReader( - num_objects=10000, - num_requests=50000, - distribution="zipf", - alpha=alpha, - obj_size=4096, - seed=42 - ) - - cache = lcs.LRU(cache_size=5*1024*1024) - for request in reader: - cache.get(request.obj_id, request.obj_size) - - print(f"α={alpha}: 命中率 {cache.get_hit_ratio():.2%}") -``` - -### 均匀分布请求 - -```python -import libcachesim as lcs - -# 创建均匀分布的合成读取器 -reader = lcs.SyntheticReader( - num_objects=5000, - num_requests=50000, - distribution="uniform", - obj_size=4096, - seed=42 -) - -cache = lcs.LRU(cache_size=5*1024*1024) -for request in reader: - cache.get(request.obj_id, request.obj_size) - -print(f"均匀工作负载命中率: {cache.get_hit_ratio():.2%}") -``` - -## 缓存算法比较 - -### 多算法评估 - -比较不同缓存算法的性能: - -```python -import libcachesim as lcs - -# 创建合成工作负载 -reader = lcs.SyntheticReader( - num_objects=10000, - num_requests=100000, - distribution="zipf", - alpha=1.2, - obj_size=4096, - seed=42 -) - -# 保存请求以便重用 -requests = list(reader) - -# 测试的算法 -algorithms = { - 'LRU': lcs.LRU, - 'LFU': lcs.LFU, - 'FIFO': lcs.FIFO, - 'ARC': lcs.ARC, - 'S3FIFO': lcs.S3FIFO, - 'Sieve': lcs.Sieve, -} - -cache_size = 10*1024*1024 # 10MB - -results = {} -for name, algorithm in algorithms.items(): - cache = algorithm(cache_size) - - for request in requests: - cache.get(request.obj_id, request.obj_size) - - results[name] = cache.get_hit_ratio() - print(f"{name:8}: {cache.get_hit_ratio():.2%}") - -# 找到最佳算法 -best_algo = max(results, key=results.get) -print(f"\n最佳算法: {best_algo} ({results[best_algo]:.2%})") -``` - -## 跟踪采样 - -### 空间采样 - -使用采样减少大型跟踪的大小: - -```python -import libcachesim as lcs - -# 设置采样参数 -sampler = lcs.Sampler( - sample_ratio=0.1, # 采样10%的请求 - type=lcs.SamplerType.SPATIAL_SAMPLER -) - -reader_params = lcs.ReaderInitParam() -reader_params.has_header = True -reader_params.sampler = sampler - -# 读取采样跟踪 -reader = lcs.TraceReader("large_trace.csv", lcs.TraceType.CSV_TRACE, reader_params) - -cache = lcs.LRU(cache_size=1024*1024) -request_count = 0 - -for request in reader: - cache.get(request.obj_id, request.obj_size) - request_count += 1 - -print(f"处理了 {request_count} 个采样请求") -print(f"采样命中率: {cache.get_hit_ratio():.2%}") -``` - -### 时间采样 - -```python -import libcachesim as lcs - -# 时间采样配置 -sampler = lcs.Sampler( - sample_ratio=0.05, # 采样5% - type=lcs.SamplerType.TEMPORAL_SAMPLER -) - -reader_params = lcs.ReaderInitParam() -reader_params.sampler = sampler - -reader = lcs.TraceReader("timestamped_trace.csv", lcs.TraceType.CSV_TRACE, reader_params) - -# 运行模拟... -``` - -## 跟踪分析 - -### 基本跟踪统计 - -分析跟踪特征: - -```python -import libcachesim as lcs - -# 创建跟踪分析器 -analyzer = lcs.TraceAnalyzer("workload.csv", lcs.TraceType.CSV_TRACE) - -# 分析基本统计 -print("跟踪分析:") -print(f"总请求数: {analyzer.get_num_requests():,}") -print(f"唯一对象数: {analyzer.get_num_objects():,}") -print(f"平均对象大小: {analyzer.get_average_obj_size():.2f} 字节") -print(f"总数据大小: {analyzer.get_total_size():,} 字节") - -# 分析重用距离 -reuse_distances = analyzer.get_reuse_distance() -print(f"平均重用距离: {sum(reuse_distances)/len(reuse_distances):.2f}") -``` - -### 流行度分析 - -```python -import libcachesim as lcs -import matplotlib.pyplot as plt - -# 创建分析器 -analyzer = lcs.TraceAnalyzer("workload.csv", lcs.TraceType.CSV_TRACE) - -# 获取对象流行度 -popularity = analyzer.get_popularity() - -# 绘制流行度分布 -plt.figure(figsize=(10, 6)) -plt.loglog(range(1, len(popularity)+1), sorted(popularity, reverse=True)) -plt.xlabel('对象排名') -plt.ylabel('访问频率') -plt.title('对象流行度分布') -plt.grid(True) -plt.show() -``` - -## 高级场景 - -### 缓存层次结构 - -模拟多级缓存层次结构: - -```python -import libcachesim as lcs - -class CacheHierarchy: - def __init__(self, l1_size, l2_size): - self.l1_cache = lcs.LRU(l1_size) # L1缓存 - self.l2_cache = lcs.LRU(l2_size) # L2缓存 - self.l1_hits = 0 - self.l2_hits = 0 - self.misses = 0 - - def get(self, obj_id, obj_size): - # 首先检查L1 - if self.l1_cache.get(obj_id, obj_size): - self.l1_hits += 1 - return True - - # 然后检查L2 - if self.l2_cache.get(obj_id, obj_size): - self.l2_hits += 1 - # 将对象提升到L1 - self.l1_cache.get(obj_id, obj_size) - return True - - # 完全缺失 - self.misses += 1 - # 将对象加载到两个级别 - self.l1_cache.get(obj_id, obj_size) - self.l2_cache.get(obj_id, obj_size) - return False - - def get_stats(self): - total = self.l1_hits + self.l2_hits + self.misses - return { - 'l1_hit_ratio': self.l1_hits / total, - 'l2_hit_ratio': self.l2_hits / total, - 'overall_hit_ratio': (self.l1_hits + self.l2_hits) / total - } - -# 使用缓存层次结构 -hierarchy = CacheHierarchy(l1_size=1024*1024, l2_size=10*1024*1024) - -reader = lcs.SyntheticReader( - num_objects=50000, - num_requests=100000, - distribution="zipf", - alpha=1.0, - obj_size=4096, - seed=42 -) - -for request in reader: - hierarchy.get(request.obj_id, request.obj_size) - -stats = hierarchy.get_stats() -print(f"L1命中率: {stats['l1_hit_ratio']:.2%}") -print(f"L2命中率: {stats['l2_hit_ratio']:.2%}") -print(f"总命中率: {stats['overall_hit_ratio']:.2%}") -``` - -### 缓存预热 - -在评估前预热缓存: - -```python -import libcachesim as lcs - -reader = lcs.SyntheticReader( - num_objects=10000, - num_requests=200000, - distribution="zipf", - alpha=1.0, - obj_size=4096, - seed=42 -) - -cache = lcs.LRU(cache_size=5*1024*1024) - -# 分为预热和评估阶段 -warmup_requests = 50000 -eval_requests = 0 - -for i, request in enumerate(reader): - hit = cache.get(request.obj_id, request.obj_size) - - if i < warmup_requests: - # 预热阶段 - 不计算统计 - continue - else: - # 评估阶段 - eval_requests += 1 - -print(f"预热后命中率: {cache.get_hit_ratio():.2%}") -print(f"评估请求数: {eval_requests}") -``` - -### 动态缓存大小 - -随时间变化缓存大小: - -```python -import libcachesim as lcs - -reader = lcs.SyntheticReader( - num_objects=10000, - num_requests=100000, - distribution="zipf", - alpha=1.0, - obj_size=4096, - seed=42 -) - -# 从小缓存开始 -initial_size = 1024*1024 # 1MB -max_size = 10*1024*1024 # 10MB -growth_interval = 10000 # 每10000个请求增长 - -cache = lcs.LRU(initial_size) -current_size = initial_size - -for i, request in enumerate(reader): - # 定期增加缓存大小 - if i > 0 and i % growth_interval == 0 and current_size < max_size: - current_size = min(current_size * 2, max_size) - # 注意:这里需要创建新缓存,因为现有缓存大小无法动态更改 - new_cache = lcs.LRU(current_size) - cache = new_cache - print(f"在请求 {i} 处将缓存大小增加到 {current_size/1024/1024:.1f}MB") - - cache.get(request.obj_id, request.obj_size) - -print(f"最终命中率: {cache.get_hit_ratio():.2%}") -``` - -## 性能优化技巧 - -### 批量处理 - -```python -import libcachesim as lcs - -# 处理大型跟踪时批量处理请求 -def process_trace_in_batches(filename, cache, batch_size=10000): - reader = lcs.TraceReader(filename, lcs.TraceType.CSV_TRACE) - - batch = [] - total_processed = 0 - - for request in reader: - batch.append(request) - - if len(batch) >= batch_size: - # 处理批次 - for req in batch: - cache.get(req.obj_id, req.obj_size) - - total_processed += len(batch) - print(f"处理了 {total_processed} 个请求") - batch = [] - - # 处理剩余请求 - for req in batch: - cache.get(req.obj_id, req.obj_size) - - return total_processed + len(batch) - -# 使用 -cache = lcs.LRU(cache_size=10*1024*1024) -total = process_trace_in_batches("large_trace.csv", cache) -print(f"总共处理了 {total} 个请求") -``` - -### 内存高效的请求处理 - -```python -import libcachesim as lcs - -def memory_efficient_simulation(filename, cache_size): - """内存高效的缓存模拟。""" - - reader_params = lcs.ReaderInitParam() - reader_params.cap_at_n_req = 1000000 # 限制内存中的请求数 - - reader = lcs.TraceReader(filename, lcs.TraceType.CSV_TRACE, reader_params) - cache = lcs.LRU(cache_size) - - request_count = 0 - for request in reader: - cache.get(request.obj_id, request.obj_size) - request_count += 1 - - # 定期报告进度 - if request_count % 100000 == 0: - print(f"进度: {request_count:,} 请求,命中率: {cache.get_hit_ratio():.2%}") - - return cache.get_hit_ratio() - -# 使用 -hit_ratio = memory_efficient_simulation("workload.csv", 10*1024*1024) -print(f"最终命中率: {hit_ratio:.2%}") -``` - -这些示例展示了libCacheSim Python绑定的各种使用场景,从基础缓存模拟到高级性能分析和优化技术。根据您的具体需求调整这些示例。 diff --git a/docs/src/zh/examples/analysis.md b/docs/src/zh/examples/analysis.md new file mode 100644 index 0000000..077c0aa --- /dev/null +++ b/docs/src/zh/examples/analysis.md @@ -0,0 +1,133 @@ +# Trace 分析 + +除了模拟缓存之外,libCacheSim 还能直接刻画负载特征:请求速率、对象大小分布、复用距离、流行度等。这些都通过 `TraceAnalyzer` 完成,它是底层 libCacheSim lib 中分析器的一层轻量封装。 + +## 基本用法 + +`TraceAnalyzer` 接受一个 reader、一个输出路径前缀,以及两个可选的配置对象: + +```python +import libcachesim as lcs + +# 第 1 步:打开一条 trace(详见 Trace Reader 页面) +URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +reader = lcs.TraceReader( + trace=URI, + trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params=lcs.ReaderInitParam(ignore_obj_size=False), +) + +# 第 2 步:运行分析 +analyzer = lcs.TraceAnalyzer(reader, "example_analysis") +analyzer.run() +``` + +构造参数如下: + +- `reader: ReaderProtocol`——待分析的 trace。 +- `output_path: str`——生成的结果文件的前缀。 +- `analysis_option: AnalysisOption`(可选)——要运行哪些分析。默认为 `AnalysisOption()`。 +- `analysis_param: AnalysisParam`(可选)——这些分析的调节参数。默认为 `AnalysisParam()`。 + +!!! important + 分析器完全运行在 C++ 后端,因此只接受由 C 实现的 reader——实际上就是 [`TraceReader`](reader.md)。传入 `SyntheticReader` 会抛出 `ReaderException: Only C/C++ reader is supported`。若要分析合成负载,请先用 `Util.convert_to_oracleGeneral` 将其写出,再用 `TraceReader` 重新打开。 + +## 选择分析项 {#selecting-analyses} + +`AnalysisOption` 的每个字段对应一项分析,其中五项默认开启: + +| 选项 | 默认值 | 含义 | +|---|---|---| +| `req_rate` | `True` | 请求速率与对象速率随时间的变化 | +| `access_pattern` | `True` | 单个对象随时间的访问模式 | +| `size` | `True` | 对象大小分布,分别按请求和按对象统计 | +| `reuse` | `True` | 复用时间 / 复用距离分布 | +| `popularity` | `True` | 对象流行度分布(Zipf 拟合) | +| `ttl` | `False` | TTL 分布(仅对带 TTL 的 trace 有意义) | +| `popularity_decay` | `False` | 对象流行度如何随时间衰减 | +| `lifetime` | `False` | 对象生命周期分布 | +| `create_future_reuse_ccdf` | `False` | 实验性——未来复用的 CCDF | +| `prob_at_age` | `False` | 实验性——访问概率随年龄的变化 | +| `size_change` | `False` | 对象大小在多次访问间的变化情况 | + +各项分析彼此独立,因此在大 trace 上关掉不需要的分析可以显著加快运行速度: + +```python +analysis_option = lcs.AnalysisOption( + req_rate=True, # 保留基本的请求速率分析 + access_pattern=False, + size=True, # 保留大小分析 + reuse=False, + popularity=False, + ttl=False, + popularity_decay=False, + lifetime=False, + create_future_reuse_ccdf=False, + prob_at_age=False, + size_change=False, +) + +analyzer = lcs.TraceAnalyzer(reader, "example_analysis", analysis_option=analysis_option) +analyzer.run() +``` + +## 调节分析行为 + +`AnalysisParam` 控制已启用分析的具体行为: + +| 参数 | 默认值 | 含义 | +|---|---|---| +| `access_pattern_sample_ratio_inv` | `10` | 访问模式分析的采样率倒数——取值为 `n` 时大约保留 `1/n` 的数据 | +| `track_n_popular` | `10` | 统计并报告请求数的最流行对象个数 | +| `track_n_hit` | `5` | 追踪多少个 "X-hit wonder" 分桶,即恰好被访问 1 次、2 次……直到 `track_n_hit` 次的对象数量 | +| `time_window` | `60` | 时间序列输出所用分桶的宽度,单位为秒 | +| `warmup_time` | `0` | 开始统计前跳过的 trace 秒数 | + +```python +analysis_param = lcs.AnalysisParam( + track_n_popular=4, + track_n_hit=4, + time_window=300, +) + +analyzer = lcs.TraceAnalyzer( + reader, "example_analysis", + analysis_option=analysis_option, + analysis_param=analysis_param, +) +analyzer.run() +``` + +!!! warning + 有两个约束很容易踩坑: + + - `warmup_time` 必须是 `time_window` 的整数倍,否则分析器会直接报错,因为流行度衰减的计算依赖这一关系。 + - 流行度分析和复用分析需要足够大的工作集才能给出有意义的结果。在很小的 trace 上——只有寥寥几个不同对象时——请把 `track_n_popular` 和 `track_n_hit` 设为不超过对象总数,或者干脆关闭 `popularity` 和 `reuse`。 + +## 分析结果 + +`run()` 会写出纯文本的结果文件,它们都以 `output_path` 为前缀。每项启用的分析至少产出一个文件——大小分析写出 `example_analysis.size`,某些分析还会额外输出按时间窗口划分的变体,例如 `example_analysis.sizeWindow_w60_req`。 + +```python +with open("example_analysis.size") as f: + print(f.read()) +``` + +本次运行的汇总信息——trace 路径、请求数与对象数、强制缺失率、平均对象大小、平均访问频率、时间跨度,以及 X-hit wonder 和流行度直方图——会写入**当前工作目录**下名为 `stat` 的文件。注意该路径是固定的,并非由 `output_path` 推导而来,而且分析器是以*追加*方式写入,因此多次运行的结果会累积在同一个文件里。 + +用完之后,可以调用 `cleanup()` 释放分析器的内部状态: + +```python +analyzer.cleanup() +``` + +## 工作集大小 + +对于最常用的那个统计量——trace 一共触及了多少数据——你根本不需要分析器。`TraceReader` 直接就能给出: + +```python +n_obj, n_byte = reader.get_working_set_size() +print(f"{n_obj} unique objects, {n_byte} bytes") +``` + +按比例设置的 `cache_size` 也正是以此为基准计算的,参见[缓存模拟](simulation.md#cache-size-as-a-ratio)。 diff --git a/docs/src/zh/examples/plugins.md b/docs/src/zh/examples/plugins.md new file mode 100644 index 0000000..a3ea8e1 --- /dev/null +++ b/docs/src/zh/examples/plugins.md @@ -0,0 +1,57 @@ +# 插件系统 + +## PluginCache + +我们允许用户通过 libCacheSim 的插件系统添加任意自定义缓存。 + +借助用户自定义的 Python hook 函数, + +```c++ + py::function cache_init_hook; + py::function cache_hit_hook; + py::function cache_miss_hook; + py::function cache_eviction_hook; + py::function cache_remove_hook; + py::function cache_free_hook; +``` + +我们可以完全从 Python 一侧模拟并决定缓存的淘汰行为。 + +这些 hook 函数的签名要求如下。 +```python +def cache_init_hook(ccparams: CommonCacheParams) -> CustomizedCacheData: ... +def cache_hit_hook(data: CustomizedCacheData, req: Request) -> None: ... +def cache_miss_hook(data: CustomizedCacheData, req: Request) -> None: ... +def cache_eviction_hook(data: CustomizedCacheData, req: Request) -> int | str: ... +def cache_remove_hook(data: CustomizedCacheData, obj_id: int | str) ->: ... +def cache_free_hook(data: CustomizedCacheData) ->: ... +``` + +- **注意:** `CustomizedCacheData` 并不是本库提供的类型。它只是表示用户自行决定从 `cache_init_hook` 返回、并作为 `data` 传给其他 hook 函数的那个对象。 + +## PluginAdmissioner + +我们允许用户通过 libCacheSim 的插件系统定义自己的准入策略,并将其与已有的缓存实现(如 `LRU`、`S3FIFO`)配合使用。 + +借助用户自定义的 Python hook 函数: + +```c++ + py::function admissioner_init_hook; + py::function admissioner_admit_hook; + py::function admissioner_update_hook; + py::function admissioner_clone_hook; + py::function admissioner_free_hook; +``` + +我们可以在 Python 中方便地完全掌控哪些对象被准入底层缓存。 + +这些 hook 函数的签名要求如下。 +```python +def admissioner_init_hook() -> CustomizedAdmissionerData: ... +def admissioner_admit_hook(data: CustomizedAdmissionerData, req: Request) -> bool: ... +def admissioner_update_hook(data: CustomizedAdmissionerData, req: Request, cache_size: int) -> None: ... +def admissioner_clone_hook(data: CustomizedAdmissionerData) -> AdmissionerBase: ... +def admissioner_free_hook(data: CustomizedAdmissionerData) -> None: ... +``` + +- **注意:** `CustomizedAdmissionerData` 并不是本库提供的类型。它只是表示用户自行决定从 `admissioner_init_hook` 返回、并作为 `data` 传给其他 hook 函数的那个对象。 diff --git a/docs/src/zh/examples/reader.md b/docs/src/zh/examples/reader.md new file mode 100644 index 0000000..14ed599 --- /dev/null +++ b/docs/src/zh/examples/reader.md @@ -0,0 +1,56 @@ +# Trace Reader + +我们提供了统一的 trace reader,用于打开不同格式的 trace 文件并读取其中的请求。 + +## 基本用法 + +`TraceReader` 类是这一功能的核心。创建 `TraceReader` 实例时,就会打开一个 trace 文件以便读取请求。 + +`TraceReader` 接受三个参数: + +- `trace: str | TraceReader`:trace 路径或另一个 trace 实例。trace 路径可以是本机上的文件路径(例如 ~/data/trace.oracleGeneral.zst),也可以是 S3 URI(例如 s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst)。 +- `trace_type: TraceType`(可选):若不指定,将根据文件名推断。 +- `reader_init_params: ReaderInitParam`(可选):若不指定,将使用默认参数初始化 reader。 + +下面是通过 S3 URI 加载一条 trace 的例子。 + +```python +import libcachesim as lcs + +# 打开一条托管在 S3 上的 trace(更多 trace 见 https://github.com/cacheMon/cache_dataset) +URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +reader = lcs.TraceReader( + trace = URI, + trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) +) +``` + +然后就可以遍历整条 trace。 + +```python +for req in reader: + print(req.obj_id, req.obj_size) +``` + +## Reader 切片 + +`TraceReader` 支持切片和按下标访问。 + +```python +# 读取前 100 条请求 +for req in reader[:100]: + print(req.obj_id, req.obj_size) +``` + +```python +# 读取前 100 条之后的 100 条请求 +for req in reader[100:200]: + print(req.obj_id, req.obj_size) +``` + +```python +# 读取最后 100 条请求 +for req in reader[-100:]: + print(req.obj_id, req.obj_size) +``` diff --git a/docs/src/zh/examples/simulation.md b/docs/src/zh/examples/simulation.md new file mode 100644 index 0000000..5c6a202 --- /dev/null +++ b/docs/src/zh/examples/simulation.md @@ -0,0 +1,346 @@ +# 缓存模拟 + +## 基本用法 + +缓存类是缓存模拟的核心。创建一个缓存实例(例如 `LRU`、`S3FIFO`)时,我们可以配置缓存大小以及该算法特有的参数,比如晋升阈值。 + +```py +import libcachesim as lcs + +# 初始化缓存 +cache = lcs.S3FIFO( + cache_size=1024 * 1024, + # 算法特有参数 + small_size_ratio=0.2, + ghost_size_ratio=0.8, + move_to_main_threshold=2, +) +``` + +准入策略是可选的——如果不提供,缓存会直接按替换策略接纳所有对象。通过 `admissioner` 参数,可以在缓存前面放置一个准入器(例如 `BloomFilterAdmissioner`)。 + +```py +import libcachesim as lcs + +# 初始化准入器 +admissioner = lcs.BloomFilterAdmissioner() + +# 第 2 步:初始化缓存 +cache = lcs.S3FIFO( + cache_size=1024 * 1024, + # 算法特有参数 + small_size_ratio=0.2, + ghost_size_ratio=0.8, + move_to_main_threshold=2, + # 可选地提供准入器 + admissioner=admissioner, +) +``` + +然后就可以借助 trace reader 用真实世界的负载运行缓存模拟(关于 `TraceReader` 的更多用法见 [Trace Reader](reader.md)): + +```py +# 高效处理整条 trace(C++ 后端) +req_miss_ratio, byte_miss_ratio = cache.process_trace(reader) +print(f"Request miss ratio: {req_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") +``` + +`process_trace` 还接受另外两个参数,用于把回放限制在 trace 的一部分上: + +```py +# 跳过前 10000 条请求,然后处理接下来的 1000 条 +req_miss_ratio, byte_miss_ratio = cache.process_trace(reader, start_req=10_000, max_req=1_000) +``` + +- `start_req: int` —— 第一条待处理请求的下标(默认:`0`) +- `max_req: int` —— 最多处理多少条请求;`-1` 表示整条 trace(默认:`-1`) + +!!! note + `process_trace` 会在回放前把 reader 倒回起点,所以你不需要自己调用 `reset()`。但*缓存*本身会跨多次调用保留状态——每测一种配置就新建一个缓存,而不要复用已经被上一轮预热过的缓存。 + +## 按比例设置缓存大小 {#cache-size-as-a-ratio} + +`cache_size` 既接受绝对字节数(`int`),也接受 trace 工作集的一个比例(`float`)。float 必须落在 `(0, 1]` 区间内,并且需要提供 `reader` 参数,因为要用它来调用 `reader.get_working_set_size()`: + +```py +# trace 总工作集大小(字节)的 10% +cache = lcs.S3FIFO( + cache_size=0.1, + reader=reader, # cache_size 为 float 时必须提供 +) +``` + +传入 float 却不给 `reader`,或者 float 超出 `(0, 1]`,都会抛出 `ValueError`。注意 `1024` 和 `1024.0` 因此含义截然不同——前者是 1 KiB,后者会被拒绝。 + +## 逐请求操作 {#working-with-individual-requests} + +`process_trace` 把整个回放过程放在 C++ 后端执行,是速度最快的方式。当你需要逐条请求进行观察或干预时,`CacheBase` 也暴露了底层操作: + +```py +for req in reader: + hit = cache.get(req) # 查找,缺失时插入(必要时触发淘汰) + if not hit: + print(f"miss on {req.obj_id}, cache now holds {cache.get_n_obj()} objects") +``` + +| 方法 | 说明 | +|---|---| +| `get(req)` | 完整的请求路径:查找 `req`,缺失时插入,必要时淘汰。命中返回 `True`。 | +| `find(req, update_cache=True)` | 查找对象,但缺失时不插入。设 `update_cache=False` 可做无副作用的探测。 | +| `can_insert(req)` | 该对象是否会被准入。 | +| `insert(req)` | 直接插入对象,不检查空间。 | +| `need_eviction(req)` | 插入 `req` 是否需要先淘汰。 | +| `to_evict(req)` | 返回下一个将被淘汰的对象,但并不真的淘汰它。 | +| `evict(req)` | 按策略淘汰一个对象。 | +| `remove(obj_id)` | 移除指定对象。若该对象不在缓存中则返回 `False`。 | +| `get_occupied_byte()` | 当前已占用的字节数。 | +| `get_n_obj()` | 当前缓存中的对象个数。 | +| `set_cache_size(new_size)` | 原地调整缓存大小。 | +| `print_cache()` | 返回描述当前缓存状态的字符串,调试时很有用。 | + +`cache_size` 和 `cache_name` 属性是只读的。 + +## 缓存算法 {#caches} + +下面这些缓存类都继承自 `CacheBase` 并共享同一套接口。除非另有说明,所有缓存类都接受以下公共参数: + +- `cache_size: int | float` —— 缓存大小(字节),或工作集的一个比例(见[上文](#cache-size-as-a-ratio)) +- `default_ttl: int`(可选)—— 默认 TTL,单位为秒(默认:`25920000`,即 300 天) +- `hashpower: int`(可选)—— 初始哈希表大小的以 2 为底的对数(默认:`24`) +- `consider_obj_metadata: bool`(可选)—— 每个对象的缓存元数据是否计入缓存大小(默认:`False`) +- `admissioner: AdmissionerBase`(可选)—— 置于缓存之前的准入策略(默认:`None`) +- `reader: ReaderProtocol`(可选)—— 仅当 `cache_size` 为比例时才需要(默认:`None`) + +### LHD +**Least Hit Density(最小命中密度)** 根据每个对象的单位空间期望命中数(命中密度)进行淘汰。 + +- *除公共参数外无额外参数* + +### LRU +**Least Recently Used(最近最少使用)** 淘汰最长时间未被访问的对象。 + +- *除公共参数外无额外参数* + +### LRUK +**LRU-K** 淘汰后向 K 距离(backward K-distance)最大的对象,也就是第 K 次最近访问时间最早的那个。访问次数不足 K 次的对象后向 K 距离视为无穷大,会按 FIFO 顺序被优先淘汰,因此仅被访问一次的对象不足以在缓存中站稳脚跟。 + +- `k: int` —— 每个对象跟踪的最近访问次数(默认:`2`) + +### FIFO +**First-In, First-Out(先进先出)** 按进入顺序淘汰对象,不考虑访问频率和时间局部性。 + +- *除公共参数外无额外参数* + +### LFU +**Least Frequently Used(最不经常使用)** 淘汰访问频率最低的对象。 + +- *除公共参数外无额外参数* + +### ARC +**Adaptive Replacement Cache(自适应替换缓存)** 是一种在时间局部性和访问频率之间取得平衡的混合算法。 + +- *除公共参数外无额外参数* + +### Clock +**Clock** 是 `LRU` 的一种低复杂度近似。 + +- `init_freq: int` —— 新对象的初始频率计数值(默认:`0`) +- `n_bit_counter: int` —— 频率计数器使用的位数(默认:`1`) + +### Random +**Random** 随机淘汰对象。 + +- *除公共参数外无额外参数* + +### S3FIFO +**Simple, Scalable FIFO** 把缓存拆分为两部分:一个用于新准入对象的小 FIFO 队列,以及一个用于已证明较热对象的主 FIFO 队列,同时用一个 ghost 队列记录最近被淘汰对象的标识。只被访问一次的对象(one-hit wonder)会很快被挤出小队列,而不会污染主队列。 + +- `small_size_ratio: float` —— 分配给小队列的缓存比例(默认:`0.1`) +- `ghost_size_ratio: float` —— ghost 队列大小占缓存的比例(默认:`0.9`) +- `move_to_main_threshold: int` —— 对象在小队列中被访问多少次后晋升到主队列(默认:`2`) + +### Sieve +**Sieve** 在一个 FIFO 队列上移动指针,淘汰第一个 visited 位未被置位的对象,同时清除途经对象的该位。它在保持 FIFO 简洁性的同时达到接近 LRU 的缺失率,且命中时不做任何晋升操作。 + +- *除公共参数外无额外参数* + +### LIRS +**Low Inter-reference Recency Set** 按对象倒数第二次访问(而非最后一次访问)的近期程度来排序,从而能把真正的热点对象与扫描过程中只被碰过一次的对象区分开。 + +- *除公共参数外无额外参数* + +### TwoQ +**2Q** 把新对象先放入一个 FIFO 队列(`Ain`),只有当对象的标识还在 ghost 队列(`Aout`)中时又被再次访问,才会晋升进入 LRU 主队列。 + +- `a_in_size_ratio: float` —— `Ain` 队列大小占缓存的比例(默认:`0.25`) +- `a_out_size_ratio: float` —— `Aout` ghost 队列大小占缓存的比例(默认:`0.5`) + +### SLRU +**Segmented LRU(分段 LRU)** 把缓存划分为若干有序的 LRU 分段;对象每命中一次就晋升一段,并随着新对象到来逐步向淘汰端降级。 + +- *除公共参数外无额外参数* + +### MQ +**Multi-Queue(多队列)** 把对象分布在一组按访问频率排序的 LRU 队列中:对象被重复访问时逐级晋升,在其生命期(lifetime)内未被访问则逐级降级。淘汰总是从最低的非空队列尾部取出,同时用一个 FIFO 幽灵队列(`Qout`)记录被淘汰对象的频率,使得很快再次被访问的对象能够回到原来的队列。 + +- `n_queue: int` —— 队列层数,取值需在 `[1, 64]` 内(默认:`8`) +- `lifetime: int` —— 对象未被访问多少个请求后降级一层(默认:`10000`) +- `qout_size_ratio: float` —— `Qout` 幽灵队列大小相对缓存大小的倍数,取值需在 `(0, 64]` 内(默认:`4.0`) + +### WTinyLFU +**Window TinyLFU** 在一个较大的主缓存前面放置一个小的 LRU 窗口,并用频率草图(frequency sketch)来判断离开窗口的对象是否值得挤掉主缓存中的淘汰候选者。 + +- `main_cache: str` —— 主缓存所用的淘汰算法(默认:`"SLRU"`) +- `window_size: float` —— LRU 窗口大小占**总缓存大小**的比例,取值需在 `[0, 1)` 内,主缓存获得剩余部分(默认:`0.01`) + +### LeCaR +**Learning Cache Replacement** 同时维护一个 LRU 候选者和一个 LFU 候选者,并用基于后悔最小化(regret minimisation)更新的权重在二者之间做选择,从而能随着负载在偏时间局部性与偏频率之间切换而自适应调整。 + +- `update_weight: bool` —— 回放过程中是否持续学习权重(默认:`True`) +- `lru_weight: float` —— 选择 LRU 候选者的初始概率;LFU 权重为 `1 - lru_weight`(默认:`0.5`) + +### LFUDA +**LFU with Dynamic Aging(带动态老化的 LFU)** 在 `LFU` 基础上,每次访问时给对象优先级加上一个全局年龄值,这样很久以前流行的对象最终会老化淘汰,而不会长期占住缓存。 + +- *除公共参数外无额外参数* + +### ClockPro +**CLOCK-Pro** 用 CLOCK 指针近似 `LIRS`,同时追踪热页和冷页,并为最近被淘汰的冷页设置一个试用期。 + +- `init_ref: int` —— 新准入对象的初始引用计数(默认:`0`) +- `init_ratio_cold: float` —— 缓存中初始被划为冷页的比例(默认:`0.5`) + +### Clock2QPlus +**Clock-2Q+** 是 2Q 的变体,在 Clock 主缓存前面放置一个小的 FIFO 试用队列。幽灵队列记录最近被淘汰的对象标识,自适应的关联窗口(correlation window)则用于调节对象需要在 FIFO 队列中存活多久才值得晋升。 + +- `fifo_size_ratio: float` —— FIFO 队列大小占缓存的比例(默认:`0.1`) +- `ghost_size_ratio: float` —— 幽灵队列大小占缓存的比例(默认:`0.9`) +- `move_to_main_threshold: int` —— 对象晋升到主缓存前所需的命中次数(默认:`1`) +- `corr_window_ratio: float` —— 关联窗口初始大小占 FIFO 队列的比例(默认:`0.5`) + +### Cacheus +**Cacheus** 在 `LeCaR` 的基础上增加了对学习率的轻量自适应以及扫描/抖动检测,使其在 `LeCaR` 表现不佳的负载上退化得更平缓。 + +- *除公共参数外无额外参数* + +### Belady +**Belady's MIN** 是最优的离线策略:它淘汰下一次访问时间最远的对象。该策略无法在线实现,其存在意义是作为可达缺失率的下界。 + +- *除公共参数外无额外参数* + +!!! important + `Belady` 和 `BeladySize` 会读取 `req.next_access_vtime`,只有 oracle trace 才带有这一字段。请使用 `ORACLE_GENERAL_TRACE` 格式的 trace(如本页各示例所示);在普通 trace 上该未来访问字段缺失,结果没有意义。 + +### BeladySize +**Size-aware Belady** 把 `Belady` 扩展到对象大小可变的场景,在一批候选对象的采样中综合考虑下次访问时间和对象大小来选择淘汰目标。 + +- `n_samples: int` —— 选择淘汰对象时采样的对象个数(默认:`128`) + +### LRUProb +**LRU with Probabilistic Promotion(概率晋升的 LRU)** 行为与 `LRU` 类似,但对象只以 `prob` 的概率被移到队首。取值越低,行为越接近 `FIFO`,晋升开销也越小。 + +- `prob: float` —— 命中时晋升对象的概率(默认:`0.5`) + +### FlashProb +**FlashProb** 建模 RAM + 闪存的两级缓存,只以一定概率把从 RAM 淘汰的对象写入闪存层,从而限制闪存设备上的写放大。 + +- `ram_size_ratio: float` —— RAM 层大小占总缓存的比例(默认:`0.05`) +- `disk_admit_prob: float` —— 对象被准入磁盘层的概率(默认:`0.2`) +- `ram_cache: str` —— RAM 层所用的淘汰算法(默认:`"LRU"`) +- `disk_cache: str` —— 磁盘层所用的淘汰算法(默认:`"FIFO"`) + +### Size +**Size** 优先淘汰最大的对象,从而最大化保留的对象数量。在对象大小差异很大的负载上适合作为基线。 + +- *除公共参数外无额外参数* + +### GDSF +**GreedyDual-Size with Frequency** 按频率除以大小对对象排序,并叠加一个全局老化因子,从而偏好体积小且访问频繁的对象。 + +- *除公共参数外无额外参数* + +### Hyperbolic +**Hyperbolic** 每次淘汰时采样若干对象,淘汰其中访问次数除以在缓存中驻留时间最小的那个,从而在不维护全局结构的情况下近似出一个优先级排序。 + +- *除公共参数外无额外参数* + +### ThreeLCache +**3LCache** 是一种学习型策略,它预测每个对象的保留价值,并据此把对象组织在三个层级中。 + +- `objective: str` —— 学习模型优化的目标指标(默认:`"byte-miss-ratio"`) + +!!! warning + 需要以 `-DENABLE_3L_CACHE=ON` 构建。参见[安装指南](../getting_started/installation.md#optional-eviction-algorithms)。在未开启该选项的构建中构造它会抛出 `ImportError`。 + +### GLCache +**Group-Learned Cache** 把对象分组为 segment,学习预测每个 segment 的未来价值,并通过合并价值最低的 segment 来完成淘汰,而不是逐个对象做决策。 + +- `segment_size: int` —— 每个 segment 包含的对象数(默认:`100`) +- `n_merge: int` —— 单次淘汰中合并的 segment 数(默认:`2`) +- `type: str` —— 缓存类型,例如学习型变体或某个基线(默认:`"learned"`) +- `rank_intvl: float` —— segment 重新排序的频率,以缓存的比例表示(默认:`0.02`) +- `merge_consecutive_segs: bool` —— 合并是否限定在连续的 segment 之间(默认:`True`) +- `train_source_y: str` —— 训练标签的来源(默认:`"online"`) +- `retrain_intvl: int` —— 模型重训练的间隔秒数(默认:`86400`) + +!!! warning + 需要以 `-DENABLE_GLCACHE=ON` 构建。参见[安装指南](../getting_started/installation.md#optional-eviction-algorithms)。在未开启该选项的构建中构造它会抛出 `ImportError`。 + +### LRB +**Learning Relaxed Belady** 训练一个模型在线近似 Belady 的决策,淘汰那些被预测为下次访问较远的对象。 + +- `objective: str` —— 学习模型优化的目标指标(默认:`"byte-miss-ratio"`) + +!!! warning + 需要以 `-DENABLE_LRB=ON` 构建。参见[安装指南](../getting_started/installation.md#optional-eviction-algorithms)。在未开启该选项的构建中构造它会抛出 `ImportError`。 + +### PluginCache +**PluginCache** 让你无需编译,仅通过 hook 函数就能用纯 Python 实现淘汰策略。它在[插件系统](plugins.md)中单独介绍。 + +## 准入策略 {#admission-policies} + +### BloomFilterAdmissioner +使用布隆过滤器,根据对象被看到的次数来决定是否准入。 + +- *无参数* + +### ProbAdmissioner +以固定概率准入对象。 + +- `prob: float`(可选)—— 准入对象的概率(默认:`0.5`) + +### SizeAdmissioner +仅在对象大小低于指定阈值时才准入。 + +- `size_threshold: int`(可选)—— 允许准入的最大对象大小(字节)(默认:`9_223_372_036_854_775_807`,即 `INT64_MAX`) + +### SizeProbabilisticAdmissioner +以随对象增大而递减的概率准入对象,从而偏好小对象。 + +- `exponent: float`(可选)—— 控制过滤大对象力度的指数(默认:`1e-6`) + +### AdaptSizeAdmissioner +实现 **AdaptSize**,一种基于反馈、周期性调整大小阈值的策略。 + +- `max_iteration: int`(可选)—— 参数调优的最大迭代次数(默认:`15`) +- `reconf_interval: int`(可选)—— 重新评估阈值的间隔(以请求数计)(默认:`30_000`) + +### PluginAdmissioner +让你通过 hook 函数用 Python 实现准入策略。参见[插件系统](plugins.md#pluginadmissioner)。 + +## 对比不同算法 + +由于所有缓存都暴露相同的接口,遍历多个算法非常简单。`process_trace` 会在开始前把 reader 倒回起点,因此同一个 reader 可以直接交给每一轮运行,无需显式调用 `reset()`: + +```py +import libcachesim as lcs + +URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" +reader = lcs.TraceReader(trace=URI, trace_type=lcs.TraceType.ORACLE_GENERAL_TRACE) + +CACHE_SIZE = 1024 * 1024 +for cls in (lcs.LRU, lcs.FIFO, lcs.ARC, lcs.S3FIFO, lcs.Sieve): + cache = cls(cache_size=CACHE_SIZE) + req_miss_ratio, byte_miss_ratio = cache.process_trace(reader) + print(f"{cache.cache_name:>10}: req {req_miss_ratio:.4f} byte {byte_miss_ratio:.4f}") +``` diff --git a/docs/src/zh/faq.md b/docs/src/zh/faq.md new file mode 100644 index 0000000..b8135a7 --- /dev/null +++ b/docs/src/zh/faq.md @@ -0,0 +1,21 @@ +# 常见问题 + +1. pip install 失败时如何解决? + + 请参阅[安装指南](getting_started/installation.md)。 + +2. 构建时出现类似 "cannot find Python package" 的错误提示。 + + 原因是构建 Python 绑定需要 Python 的开发头文件和库文件。 + + 如果你拥有管理员权限,可以使用系统的包管理器安装所需的软件包。例如: + + * **Debian/Ubuntu**:`sudo apt install python3-dev` + * **RHEL/CentOS/Fedora**:`sudo yum install python3-devel` + * **macOS**:通常用 Homebrew 安装 Python(`brew install python`)即可。 + + 另外,如果你把 Python 安装在了自定义位置,则需要设置环境变量来帮助构建系统找到它。请记得将下面命令中的占位符替换为你的实际路径。 + + ```shell + export CMAKE_ARGS="-DPython3_ROOT_DIR=${Python3_ROOT_DIR} -DPython3_INCLUDE_DIR=${Python3_INCLUDE_DIR} -DPython3_EXECUTABLE=${Python3_EXECUTABLE}" + ``` diff --git a/docs/src/zh/getting_started/installation.md b/docs/src/zh/getting_started/installation.md new file mode 100644 index 0000000..d3c6df0 --- /dev/null +++ b/docs/src/zh/getting_started/installation.md @@ -0,0 +1,102 @@ +# 安装 + +## 环境要求 + +| | | +|---|---| +| **操作系统** | Linux / macOS | +| **Python** | 3.10 -- 3.13 | +| **架构** | x86_64 / aarch64 | + +不支持 Windows。 + +## 从 PyPI 安装 + +我们已将预编译的 wheel 发布到 [PyPI](https://pypi.org/project/libcachesim/),因此大多数情况下无需编译器: + +```bash +pip install libcachesim +``` + +推荐使用 [uv](https://docs.astral.sh/uv/) 创建和管理环境: + +```bash +uv venv --python 3.12 --seed +source .venv/bin/activate +uv pip install libcachesim +``` + +验证安装结果: + +```bash +python -c "import libcachesim; print(libcachesim.__version__)" +``` + +## 可选的淘汰算法 {#optional-eviction-algorithms} + +有三个算法依赖第三方机器学习库,因此由 CMake 选项控制,且默认全部为 `OFF`: + +| 算法 | CMake 选项 | 依赖 | +|---|---|---| +| [`LRB`](../examples/simulation.md#lrb) | `ENABLE_LRB` | LightGBM | +| [`ThreeLCache`](../examples/simulation.md#threelcache) | `ENABLE_3L_CACHE` | LightGBM | +| [`GLCache`](../examples/simulation.md#glcache) | `ENABLE_GLCACHE` | XGBoost | + +!!! note + 发布到 PyPI 的 wheel 已启用全部三个算法。只有当没有匹配你平台的 wheel、pip 回退到从源码构建,或者你自己从源码检出进行构建时,才需要执行下面的步骤。 + +首先安装第三方依赖: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +bash scripts/install_deps.sh + +# 如果你无法安装系统软件包(例如没有 sudo 权限) +bash scripts/install_deps_user.sh +``` + +然后通过 `CMAKE_ARGS` 传入选项重新安装。加上 `--no-cache-dir` 可以强制重新构建,而不是复用缓存的 wheel: + +```bash +# 启用单个算法 +CMAKE_ARGS="-DENABLE_LRB=ON" pip install libcachesim --no-cache-dir + +# 或者三个全部启用 +CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" \ + pip install libcachesim --no-cache-dir +``` + +!!! important + 由于这些选项默认为 `OFF`,普通的源码构建会**静默地略过**这三个算法——此时构造 `LRB`、`ThreeLCache` 或 `GLCache` 会在运行时失败。反过来,如果在未安装依赖的情况下把选项打开,CMake 会在配置阶段直接报错(`LIGHTGBM_PATH not found`,或提示缺少 `xgboost` 包)。请先运行依赖安装脚本。 + +## 从源码安装 + +C 语言库 [libCacheSim](https://github.com/1a1a11a/libCacheSim) 以 git 子模块的形式引入,因此检出时必须递归拉取: + +```bash +git clone https://github.com/cacheMon/libCacheSim-python.git +cd libCacheSim-python +git submodule update --init --recursive +pip install . +``` + +`scripts/install.sh` 封装了整个流程——它会更新子模块、以可编辑模式安装本包、检查导入是否正常,并运行测试套件: + +```bash +bash scripts/install.sh + +# 同上,但启用全部可选算法 +bash scripts/install.sh --all +``` + +构建扩展需要支持 C++17 的编译器、CMake ≥ 3.15 以及 Ninja。构建过程由 [scikit-build-core](https://scikit-build-core.readthedocs.io/) 驱动,它会先配置并构建内置的 C 库,再编译 [pybind11](https://pybind11.readthedocs.io/) 绑定。 + +## 疑难排查 + +有两类失败足够常见,已在[常见问题](../faq.md)中单列条目: + +- `pip install` 找不到合适的 wheel,并且源码构建报错。 +- 构建时提示 `cannot find Python package`——缺少 Python 的开发头文件,或者 Python 安装在非标准位置。 + +其他问题请[提交 issue](https://github.com/cacheMon/libCacheSim-python/issues/new/choose)。 diff --git a/docs/src/zh/getting_started/quickstart.md b/docs/src/zh/getting_started/quickstart.md new file mode 100644 index 0000000..e3c49c6 --- /dev/null +++ b/docs/src/zh/getting_started/quickstart.md @@ -0,0 +1,194 @@ +# 快速开始 + +本指南将帮助你快速上手 libCacheSim。 + +## 前置条件 + +- 操作系统:Linux / macOS +- Python:3.10 -- 3.13 + +## 安装 + +你可以直接使用 [pip](https://pypi.org/project/libcachesim/) 安装 libCacheSim。 + +我们推荐使用 [uv](https://docs.astral.sh/uv/)——一个非常快的 Python 环境管理器——来创建和管理 Python 环境。请参照其[官方文档](https://docs.astral.sh/uv/#getting-started)安装 `uv`。装好 `uv` 之后,用下面的命令创建新环境并安装 libCacheSim: + +```bash +uv venv --python 3.12 --seed +source .venv/bin/activate +uv pip install libcachesim +``` + +如需从源码构建,或需要启用 LRB、ThreeLCache 和 GLCache 这三个在源码构建中默认被排除的淘汰算法,请参阅[安装指南](installation.md)。 + +## 缓存模拟 + +装好 libcachesim 之后,你就可以针对某个淘汰算法和缓存 trace 开始模拟了。示例脚本如下: + +??? code + ```python + import libcachesim as lcs + + # 第 1 步:打开一条托管在 S3 上的 trace(更多 trace 见 https://github.com/cacheMon/cache_dataset) + URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" + reader = lcs.TraceReader( + trace = URI, + trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) + ) + + # 第 2 步:初始化缓存 + cache = lcs.S3FIFO( + cache_size=1024*1024, + # 算法特有参数 + small_size_ratio=0.2, + ghost_size_ratio=0.8, + move_to_main_threshold=2, + ) + + # 第 3 步:高效处理整条 trace(C++ 后端) + req_miss_ratio, byte_miss_ratio = cache.process_trace(reader) + print(f"Request miss ratio: {req_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") + + # 第 3.1 步:只处理前 1000 条请求 + cache = lcs.S3FIFO( + cache_size=1024 * 1024, + # 算法特有参数 + small_size_ratio=0.2, + ghost_size_ratio=0.8, + move_to_main_threshold=2, + ) + req_miss_ratio, byte_miss_ratio = cache.process_trace(reader, start_req=0, max_req=1000) + print(f"Request miss ratio: {req_miss_ratio:.4f}, Byte miss ratio: {byte_miss_ratio:.4f}") + ``` + +上面的例子展示了用 `libcachesim` 做缓存模拟的基本流程: + +1. 用 `TraceReader` 打开并高效处理 trace 文件。 +2. 初始化一个缓存对象(这里是 `S3FIFO`),并指定缓存大小(例如 1MB)。 +3. 用 `process_trace` 在整条 trace 上运行模拟,得到对象缺失率和字节缺失率。 +4. 也可以通过 `start_req` 和 `max_req` 只处理 trace 的一部分,做局部模拟。 + +这套流程适用于大多数缓存算法和 trace 类型,既容易上手,也便于自定义实验。 + +### 按 trace 比例设置缓存大小 + +在比较规模差异很大的多条 trace 时,使用绝对字节数会很不方便。此时可以把 `cache_size` 设为 `(0, 1]` 区间内的 `float`,它会被解释为 trace 工作集的一个比例,但这要求同时把 `reader` 传给缓存: + +```python +cache = lcs.S3FIFO( + cache_size=0.1, # trace 工作集大小(字节)的 10% + reader=reader, # cache_size 为 float 时必须提供 +) +``` + +`int` 始终表示绝对字节数,所以 `1024` 表示 1 KiB,而 `1024.0` 超出取值范围,会抛出 `ValueError`。 + +## Trace 分析 + +下面这个例子演示了 `TraceAnalyzer` 的用法。 + +??? code + ```python + import libcachesim as lcs + + # 第 1 步:从 S3 存储桶获取一条 trace + URI = "s3://cache-datasets/cache_dataset_oracleGeneral/2007_msr/msr_hm_0.oracleGeneral.zst" + reader = lcs.TraceReader( + trace = URI, + trace_type = lcs.TraceType.ORACLE_GENERAL_TRACE, + reader_init_params = lcs.ReaderInitParam(ignore_obj_size=False) + ) + + analysis_option = lcs.AnalysisOption( + req_rate=True, # 保留基本的请求速率分析 + access_pattern=False, # 关闭访问模式分析 + size=True, # 保留大小分析 + reuse=False, # 小数据集上关闭复用分析 + popularity=False, # 小数据集(少于 200 个对象)上关闭流行度分析 + ttl=False, # 关闭 TTL 分析 + popularity_decay=False, # 关闭流行度衰减分析 + lifetime=False, # 关闭生命周期分析 + create_future_reuse_ccdf=False, # 关闭实验性功能 + prob_at_age=False, # 关闭实验性功能 + size_change=False, # 关闭大小变化分析 + ) + + analysis_param = lcs.AnalysisParam() + + analyzer = lcs.TraceAnalyzer( + reader, "example_analysis", analysis_option=analysis_option, analysis_param=analysis_param + ) + + analyzer.run() + ``` + +上面的代码演示了如何用 `libcachesim` 进行 trace 分析,流程如下: + +1. 用 `TraceReader` 打开 trace 文件,指定 trace 类型和所需的 reader 初始化参数。以 `s3://` 开头的 URI 会自动从 S3 存储桶下载 trace 文件。 +2. 用 `AnalysisOption` 配置分析项,开启或关闭特定分析(如请求速率、对象大小等)。 +3. 可选地用 `AnalysisParam` 设置额外的分析参数。 +4. 用 reader、输出目录以及选定的选项和参数创建 `TraceAnalyzer` 对象。 +5. 调用 `analyzer.run()` 运行分析。 + +运行结束后,你就可以查看分析结果,例如汇总统计(`stat`)或详细结果(如 `example_analysis.size`)。 + +## 插件系统 + +libCacheSim 还允许用户开发自己的缓存淘汰算法,并通过插件系统进行测试。 + +下面是通过插件系统实现 `LRU` 的例子。 + +??? code + ```python + from collections import OrderedDict + from typing import Any + + from libcachesim import PluginCache, LRU, CommonCacheParams, Request, SyntheticReader + + 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 = 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}") + ``` + +只要为缓存的初始化、命中、缺失、淘汰、移除和清理定义好自定义 hook 函数,用户就能轻松地对自己的缓存淘汰算法做原型验证和测试。 + +## 下一步 + +- [Trace Reader](../examples/reader.md)——打开本地和 S3 上的 trace、切片与遍历 +- [缓存模拟](../examples/simulation.md)——所有淘汰算法与准入策略及其参数 +- [Trace 分析](../examples/analysis.md)——用 `TraceAnalyzer` 刻画负载特征 +- [插件系统](../examples/plugins.md)——自定义缓存与准入策略的 hook 签名 +- [API 参考](../api.md)——完整的对外接口 diff --git a/docs/src/zh/index.md b/docs/src/zh/index.md index 997399a..d8763ec 100644 --- a/docs/src/zh/index.md +++ b/docs/src/zh/index.md @@ -1,68 +1,35 @@ -# libCacheSim Python 绑定 +# 欢迎使用 libCacheSim Python -欢迎使用 libCacheSim Python 绑定!这是一个高性能的缓存模拟库,提供了 Python 接口。 +!!! note + 为方便起见,下文将 *libCacheSim Python 包*(本仓库)简称为 *libCacheSim*,将其底层的 *C 语言库* 称为 *libCacheSim lib*。 -## 概述 +
+ ![](../assets/logos/logo.jpg){ align="center" alt="libCacheSim Light" class="logo-light" width="60%" } +
-libCacheSim 是一个高性能的缓存模拟框架,支持各种缓存算法和跟踪格式。Python 绑定为缓存模拟、分析和研究提供了易于使用的接口。 +

+一个用于构建和运行缓存模拟的高性能库 + +

-## 主要特性 +

+ +Star +Watch +Fork +

-- **高性能**: 基于优化的 C++ libCacheSim 库构建 -- **多种缓存算法**: 支持 LRU、LFU、FIFO、ARC、Clock、S3FIFO、Sieve 等多种算法 -- **跟踪支持**: 读取各种跟踪格式(CSV、二进制、OracleGeneral 等) -- **合成跟踪**: 生成 Zipf 和均匀分布的合成工作负载 -- **分析工具**: 内置跟踪分析和缓存性能评估 -- **易于集成**: 简单的 Python API,适用于研究和生产环境 +libCacheSim 是 [libCacheSim lib](https://github.com/1a1a11a/libCacheSim) 的 Python 绑定,简单易用,可用于构建和运行缓存模拟。 -## 快速示例 +得益于[底层的 libCacheSim lib](https://github.com/1a1a11a/libCacheSim),libCacheSim 速度很快: -```python -import libcachesim as lcs +- 高性能——真实 trace 回放可达每秒 2000 万条以上请求。 +- 高内存效率——内存占用小且可预测。 +- 开箱即用的并行能力——利用多核 CPU 加速 trace 分析与缓存模拟。 -# 创建缓存 -cache = lcs.LRU(cache_size=1024*1024) # 1MB 缓存 +libCacheSim 同时灵活易用: -# 生成合成跟踪 -reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0 -) - -# 模拟缓存行为 -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:.4f}") -``` - -## 安装 - -```bash -pip install libcachesim -``` - -或从源码安装: - -```bash -git clone https://github.com/cacheMon/libCacheSim-python.git -cd libCacheSim-python -pip install -e . -``` - -## 快速开始 - -查看我们的[快速开始指南](quickstart.md)开始使用 libCacheSim Python 绑定,或浏览 [API 参考](api.md)获取详细文档。 - -## 贡献 - -我们欢迎贡献!请查看我们的 [GitHub 仓库](https://github.com/cacheMon/libCacheSim-python)了解更多信息。 - -## 许可证 - -本项目采用 GPL-3.0 许可证。 +- 与[开源缓存数据集](https://github.com/cacheMon/cache_dataset)无缝集成,该数据集在 S3 上托管了数千条 trace。 +- 基于[底层 libCacheSim lib](https://github.com/1a1a11a/libCacheSim) 的高吞吐模拟。 +- 可细粒度控制缓存请求及其他内部数据。 +- 无需任何编译即可开发自定义的插件缓存。 diff --git a/docs/src/zh/quickstart.md b/docs/src/zh/quickstart.md deleted file mode 100644 index fbdc7f6..0000000 --- a/docs/src/zh/quickstart.md +++ /dev/null @@ -1,183 +0,0 @@ -# 快速开始指南 - -本指南将帮助您开始使用 libCacheSim Python 绑定。 - -## 安装 - -### 从 PyPI 安装(推荐) - -```bash -pip install libcachesim -``` - -### 从源码安装 - -```bash -git clone https://github.com/cacheMon/libCacheSim-python.git -cd libCacheSim-python -git submodule update --init --recursive -pip install -e . -``` - -## 基本用法 - -### 1. 创建缓存 - -```python -import libcachesim as lcs - -# 创建不同类型的缓存 -lru_cache = lcs.LRU(cache_size=1024*1024) # 1MB LRU 缓存 -lfu_cache = lcs.LFU(cache_size=1024*1024) # 1MB LFU 缓存 -fifo_cache = lcs.FIFO(cache_size=1024*1024) # 1MB FIFO 缓存 -``` - -### 2. 使用合成跟踪 - -```python -# 生成 Zipf 分布的请求 -reader = lcs.SyntheticReader( - num_of_req=10000, - obj_size=1024, - dist="zipf", - alpha=1.0, - num_objects=1000, - seed=42 -) - -# 模拟缓存行为 -cache = lcs.LRU(cache_size=50*1024) -hit_count = 0 - -for req in reader: - if cache.get(req): - hit_count += 1 - -print(f"命中率: {hit_count/reader.get_num_of_req():.4f}") -``` - -### 3. 读取真实跟踪 - -```python -# 读取 CSV 跟踪 -reader = lcs.TraceReader( - trace="path/to/trace.csv", - trace_type=lcs.TraceType.CSV_TRACE, - has_header=True, - delimiter=",", - obj_id_is_num=True -) - -# 处理请求 -cache = lcs.LRU(cache_size=1024*1024) -for req in reader: - result = cache.get(req) - # 处理结果... -``` - -### 4. 缓存性能分析 - -```python -# 运行综合分析 -analyzer = lcs.TraceAnalyzer(reader, "output_prefix") -analyzer.run() - -# 这会生成各种分析文件: -# - 命中率曲线 -# - 访问模式分析 -# - 时间局部性分析 -# - 等等... -``` - -## 可用的缓存算法 - -libCacheSim 支持众多缓存算法: - -### 基础算法 -- **LRU**: 最近最少使用 -- **LFU**: 最不经常使用 -- **FIFO**: 先进先出 -- **Clock**: 时钟算法 -- **Random**: 随机替换 - -### 高级算法 -- **ARC**: 自适应替换缓存 -- **S3FIFO**: 简单、快速、公平的 FIFO -- **Sieve**: Sieve 驱逐算法 -- **TinyLFU**: 带准入控制的 Tiny LFU -- **TwoQ**: 双队列算法 -- **LRB**: 学习松弛 Belady - -### 实验性算法 -- **3LCache**: 三级缓存 -- **等等...** - -## 跟踪格式 - -支持的跟踪格式包括: - -- **CSV**: 逗号分隔值 -- **Binary**: 自定义二进制格式 -- **OracleGeneral**: Oracle 通用格式 -- **Vscsi**: VMware vSCSI 格式 -- **等等...** - -## 高级功能 - -### 自定义缓存策略 - -您可以使用 Python 钩子实现自定义缓存策略: - -```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() -``` - -### 跟踪采样 - -```python -# 空间采样 10% 的请求 -reader = lcs.TraceReader( - trace="large_trace.csv", - trace_type=lcs.TraceType.CSV_TRACE, - sampling_ratio=0.1, - sampling_type=lcs.SamplerType.SPATIAL_SAMPLER -) -``` - -### 多线程分析 - -```python -# 使用多线程进行分析 -analyzer = lcs.TraceAnalyzer(reader, "output", n_threads=4) -analyzer.run() -``` - -## 下一步 - -- 探索 [API 参考](api.md) 获取详细文档 -- 查看[使用示例](examples.md)了解更复杂的用例 -- 访问我们的 [GitHub 仓库](https://github.com/cacheMon/libCacheSim-python) 获取源码和问题报告 diff --git a/libcachesim/__init__.py b/libcachesim/__init__.py index e2e1c20..52fe7af 100644 --- a/libcachesim/__init__.py +++ b/libcachesim/__init__.py @@ -26,16 +26,19 @@ ARC, Clock, Random, + LRUK, # Advanced algorithms S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, LeCaR, LFUDA, ClockPro, + Clock2QPlus, Cacheus, # Optimal algorithms Belady, @@ -92,16 +95,19 @@ "ARC", "Clock", "Random", + "LRUK", # Advanced cache algorithms "S3FIFO", "Sieve", "LIRS", "TwoQ", "SLRU", + "MQ", "WTinyLFU", "LeCaR", "LFUDA", "ClockPro", + "Clock2QPlus", "Cacheus", # Optimal algorithms "Belady", diff --git a/libcachesim/__init__.pyi b/libcachesim/__init__.pyi index 508c2d3..51b581c 100644 --- a/libcachesim/__init__.pyi +++ b/libcachesim/__init__.pyi @@ -18,7 +18,7 @@ class Request: def __init__( self, obj_size: int = 1, - op: ReqOp = ReqOp.READ, + op: ReqOp = ReqOp.OP_NOP, valid: bool = True, obj_id: int = 0, clock_time: int = 0, @@ -140,7 +140,7 @@ class Cache: class CacheBase: """Base class for all cache implementations""" - def __init__(self, _cache: Cache): ... + def __init__(self, _cache: Cache, admissioner: Optional["AdmissionerBase"] = None): ... def get(self, req: Request) -> bool: ... def find(self, req: Request, update_cache: bool = True) -> CacheObject: ... def can_insert(self, req: Request) -> bool: ... @@ -151,6 +151,7 @@ class CacheBase: def to_evict(self, req: Request) -> CacheObject: ... def get_occupied_byte(self) -> int: ... def get_n_obj(self) -> int: ... + def set_cache_size(self, new_size: int) -> None: ... def print_cache(self) -> str: ... def process_trace(self, reader: ReaderProtocol, start_req: int = 0, max_req: int = -1) -> tuple[float, float]: ... @property @@ -194,6 +195,11 @@ class Random(CacheBase): self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None ): ... +class LRUK(CacheBase): + def __init__( + self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, k: int = 2, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None + ): ... + # Advanced algorithms class S3FIFO(CacheBase): def __init__( @@ -220,6 +226,11 @@ class SLRU(CacheBase): self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None ): ... +class MQ(CacheBase): + def __init__( + self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, n_queue: int = 8, lifetime: int = 10000, qout_size_ratio: float = 4.0, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None + ): ... + class WTinyLFU(CacheBase): def __init__( self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, main_cache: str = "SLRU", window_size: float = 0.01, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None @@ -240,6 +251,11 @@ class ClockPro(CacheBase): self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, init_ref: int = 0, init_ratio_cold: float = 0.5, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None ): ... +class Clock2QPlus(CacheBase): + def __init__( + self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, fifo_size_ratio: float = 0.1, ghost_size_ratio: float = 0.9, move_to_main_threshold: int = 1, corr_window_ratio: float = 0.5, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None + ): ... + class Cacheus(CacheBase): def __init__( self, cache_size: int | float, default_ttl: int = 25920000, hashpower: int = 24, consider_obj_metadata: bool = False, admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None @@ -323,7 +339,12 @@ class PluginCache(CacheBase): # Readers class TraceReader(ReaderProtocol): c_reader: bool - def __init__(self, trace: str, trace_type: TraceType = TraceType.UNKNOWN_TRACE, **kwargs): ... + def __init__( + self, + trace: str, + trace_type: TraceType = TraceType.UNKNOWN_TRACE, + reader_init_params: Optional[ReaderInitParam] = None, + ): ... class SyntheticReader(ReaderProtocol): c_reader: bool @@ -360,7 +381,13 @@ def create_uniform_requests( # Analyzer class TraceAnalyzer: - def __init__(self, analyzer, reader: ReaderProtocol, output_path: str, analysis_param, analysis_option): ... + def __init__( + self, + reader: ReaderProtocol, + output_path: str, + analysis_param: Optional[AnalysisParam] = None, + analysis_option: Optional[AnalysisOption] = None, + ): ... def run(self) -> None: ... def cleanup(self) -> None: ... diff --git a/libcachesim/cache.py b/libcachesim/cache.py index 95367ef..eecb90a 100644 --- a/libcachesim/cache.py +++ b/libcachesim/cache.py @@ -17,6 +17,8 @@ LIRS_init, TwoQ_init, SLRU_init, + LRU_K_init, + MQ_init, # Advanced algorithms S3FIFO_init, Sieve_init, @@ -24,6 +26,7 @@ LeCaR_init, LFUDA_init, ClockPro_init, + Clock2QPlus_init, Cacheus_init, # Optimal algorithms Belady_init, @@ -231,6 +234,36 @@ def __init__( ) +class LRUK(CacheBase): + """LRU-K: evict the object with the largest backward K-distance + + Objects accessed fewer than K times have an infinite backward K-distance + and are evicted first in FIFO order. + + Special parameters: + k: the number of recent accesses to track (default: 2) + """ + + def __init__( + self, + cache_size: int | float, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, + k: int = 2, + admissioner: AdmissionerBase = None, + reader: ReaderProtocol = None, + ): + cache_specific_params = f"k={k}" + super().__init__( + _cache=LRU_K_init( + _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader), + cache_specific_params, + ), + admissioner=admissioner + ) + + class FIFO(CacheBase): """First In First Out cache (no special parameters)""" @@ -447,12 +480,51 @@ def __init__( ) +class MQ(CacheBase): + """Multi-Queue replacement algorithm + + Objects are held in a hierarchy of LRU queues ordered by access frequency; + an object that is not re-accessed within its lifetime is demoted to the + next lower queue. Evicted objects keep their frequency in a FIFO ghost + queue (Qout). + + Special parameters: + n_queue: number of queues in the hierarchy, must be in [1, 64] (default: 8) + lifetime: number of requests an object may stay in its queue without being + accessed before it is demoted (default: 10000) + qout_size_ratio: size of the Qout ghost queue relative to the cache size, + must be in (0, 64] (default: 4.0) + """ + + def __init__( + self, + cache_size: int | float, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, + n_queue: int = 8, + lifetime: int = 10000, + qout_size_ratio: float = 4.0, + admissioner: AdmissionerBase = None, + reader: ReaderProtocol = None, + ): + cache_specific_params = f"n-queue={n_queue}, lifetime={lifetime}, Qout-size-ratio={qout_size_ratio}" + super().__init__( + _cache=MQ_init( + _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader), + cache_specific_params, + ), + admissioner=admissioner + ) + + class WTinyLFU(CacheBase): """Window TinyLFU Special parameters: main_cache: the type of the main cache (default: "SLRU") - window_size: ratio of the window size to the main cache size (default: 0.01) + window_size: ratio of the window LRU size to the total cache size, must be + in [0, 1); the main cache receives the remainder (default: 0.01) """ def __init__( @@ -552,6 +624,45 @@ def __init__( ) +class Clock2QPlus(CacheBase): + """Clock-2Q+ replacement algorithm + + A 2Q variant that uses a small FIFO probationary queue in front of a Clock + main cache, with a ghost queue and an adaptive correlation window. + + Special parameters: + fifo_size_ratio: ratio of the FIFO queue size to the total cache size (default: 0.1) + ghost_size_ratio: ratio of the ghost queue size to the total cache size (default: 0.9) + move_to_main_threshold: number of hits before an object is promoted to the main cache (default: 1) + corr_window_ratio: initial ratio of the correlation window to the FIFO queue size (default: 0.5) + """ + + def __init__( + self, + cache_size: int | float, + default_ttl: int = 86400 * 300, + hashpower: int = 24, + consider_obj_metadata: bool = False, + fifo_size_ratio: float = 0.1, + ghost_size_ratio: float = 0.9, + move_to_main_threshold: int = 1, + corr_window_ratio: float = 0.5, + admissioner: AdmissionerBase = None, + reader: ReaderProtocol = None, + ): + cache_specific_params = ( + f"fifo-size-ratio={fifo_size_ratio}, ghost-size-ratio={ghost_size_ratio}, " + f"move-to-main-threshold={move_to_main_threshold}, corr-window-ratio={corr_window_ratio}" + ) + super().__init__( + _cache=Clock2QPlus_init( + _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader), + cache_specific_params, + ), + admissioner=admissioner + ) + + class Cacheus(CacheBase): """Cacheus algorithm (no special parameters)""" @@ -606,7 +717,8 @@ def __init__( admissioner: AdmissionerBase = None, reader: ReaderProtocol = None, ): - cache_specific_params = f"n-samples={n_samples}" + # NOTE: the C parser accepts "n-sample" (singular) + cache_specific_params = f"n-sample={n_samples}" super().__init__( _cache=BeladySize_init( _create_common_params(cache_size, default_ttl, hashpower, consider_obj_metadata, reader), diff --git a/pyproject.toml b/pyproject.toml index 68dd3e1..4d3ae9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ build-dir = "build" [tool.scikit-build.cmake] build-type = "Release" args = ["-G", "Ninja"] -define = { CMAKE_OSX_DEPLOYMENT_TARGET = "15.0" } +define = { CMAKE_OSX_DEPLOYMENT_TARGET = "11.0" } version = ">=3.15" source-dir = "." @@ -83,7 +83,7 @@ skip = ["*-win32", "*-manylinux_i686", "*-musllinux*", "pp*"] # Set the environment variable for the wheel build step. # NOTE(haocheng): we enable all the optional features for the wheel build. # And MACOSX_DEPLOYMENT_TARGET should be kept in sync with [tool.scikit-build.cmake].define.CMAKE_OSX_DEPLOYMENT_TARGET -environment = { LCS_BUILD_DIR = "{project}/src/libCacheSim/build", MACOSX_DEPLOYMENT_TARGET = "15.0", CMAKE_ARGS = "-DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON -DENABLE_LRB=ON" } +environment = { LCS_BUILD_DIR = "{project}/src/libCacheSim/build", MACOSX_DEPLOYMENT_TARGET = "11.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\")'; cp -r {project}/tests .; python -m pytest tests/ -v -m 'not optional'; python -m pytest tests/ -v -m 'optional'" diff --git a/src/export_cache.cpp b/src/export_cache.cpp index a9f3afb..e962b99 100644 --- a/src/export_cache.cpp +++ b/src/export_cache.cpp @@ -482,6 +482,7 @@ void export_cache(py::module& m) { make_cache_wrapper("CAR_init")(m); make_cache_wrapper("Cacheus_init")(m); make_cache_wrapper("Clock_init")(m); + make_cache_wrapper("Clock2QPlus_init")(m); make_cache_wrapper("ClockPro_init")(m); make_cache_wrapper("FIFO_init")(m); make_cache_wrapper("FIFO_Merge_init")(m); @@ -495,7 +496,9 @@ void export_cache(py::module& m) { make_cache_wrapper("LFUDA_init")(m); make_cache_wrapper("LIRS_init")(m); make_cache_wrapper("LRU_init")(m); + make_cache_wrapper("LRU_K_init")(m); make_cache_wrapper("LRU_Prob_init")(m); + make_cache_wrapper("MQ_init")(m); make_cache_wrapper("nop_init")(m); make_cache_wrapper("QDLP_init")(m); diff --git a/src/libCacheSim b/src/libCacheSim index dbf8423..373aee9 160000 --- a/src/libCacheSim +++ b/src/libCacheSim @@ -1 +1 @@ -Subproject commit dbf84235759f64f5e0353a77c49d187ed0dd3608 +Subproject commit 373aee94ee29afc4b9fdf90a9ad2cffa3c879a1b diff --git a/tests/test_cache.py b/tests/test_cache.py index 2a477d2..fd0ad6e 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -16,13 +16,16 @@ ARC, Clock, Random, + LRUK, # Advanced algorithms S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, + Clock2QPlus, # Request and other utilities Request, ReqOp, @@ -79,15 +82,18 @@ class TestCacheBasicFunctionality: ARC, Clock, Random, + LRUK, S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, LeCaR, LFUDA, ClockPro, + Clock2QPlus, Cacheus, LRUProb, FlashProb, @@ -111,7 +117,8 @@ def test_cache_initialization(self, cache_class): pytest.skip(f"Cache {cache_class.__name__} failed to initialize: {e}") @pytest.mark.parametrize( - "cache_class", [LHD, LRU, FIFO, LFU, ARC, Clock, Random, S3FIFO, Sieve, LIRS, TwoQ, SLRU, WTinyLFU] + "cache_class", + [LHD, LRU, FIFO, LFU, ARC, Clock, Random, LRUK, S3FIFO, Sieve, LIRS, TwoQ, SLRU, MQ, WTinyLFU, Clock2QPlus], ) def test_basic_get_and_insert(self, cache_class): """Test basic get and insert operations""" @@ -154,15 +161,18 @@ def test_basic_get_and_insert(self, cache_class): ARC, Clock, Random, + LRUK, S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, LeCaR, LFUDA, ClockPro, + Clock2QPlus, Cacheus, LRUProb, FlashProb, @@ -206,15 +216,18 @@ def test_cache_eviction(self, cache_class): ARC, Clock, Random, + LRUK, S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, LeCaR, LFUDA, ClockPro, + Clock2QPlus, Cacheus, LRUProb, FlashProb, @@ -254,15 +267,18 @@ def test_cache_find_method(self, cache_class): ARC, Clock, Random, + LRUK, S3FIFO, Sieve, LIRS, TwoQ, SLRU, + MQ, WTinyLFU, LeCaR, LFUDA, ClockPro, + Clock2QPlus, Cacheus, LRUProb, FlashProb, @@ -551,4 +567,75 @@ def test_fractional_cache_size(self): # with obj size trace = SyntheticReader(num_of_req=10000, obj_size=100, dist="uniform", num_objects=1000, seed=42) cache = LRU(0.5, reader=trace) - assert cache.cache_size == 0.5 * trace.get_working_set_size()[1] \ No newline at end of file + assert cache.cache_size == 0.5 * trace.get_working_set_size()[1] + +class TestNewAlgorithmParameters: + """Test cache-specific parameters of the recently added algorithms + + NOTE: out-of-range parameters are rejected by libCacheSim's ERROR() macro, + which aborts the process instead of raising, so they cannot be tested here. + """ + + @pytest.mark.parametrize("k", [1, 2, 4]) + def test_lruk_k_parameter(self, k): + """LRU-K should accept different values of k and stay functional""" + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + cache = LRUK(1024 * 10, k=k) + miss_ratio, _ = cache.process_trace(reader) + assert 0.0 <= miss_ratio <= 1.0 + + @pytest.mark.parametrize("n_queue", [1, 4, 8]) + def test_mq_n_queue_parameter(self, n_queue): + """MQ should honor n_queue, which is reflected in the cache name""" + cache = MQ(1024 * 10, n_queue=n_queue) + assert cache.cache_name == f"MQ-{n_queue}" + + def test_mq_lifetime_and_qout_parameters(self): + """MQ should accept a custom lifetime and ghost queue ratio""" + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + cache = MQ(1024 * 10, n_queue=4, lifetime=100, qout_size_ratio=2.0) + miss_ratio, _ = cache.process_trace(reader) + assert 0.0 <= miss_ratio <= 1.0 + + def test_clock2qplus_parameters(self): + """Clock2QPlus should honor its ratio/threshold parameters""" + cache = Clock2QPlus( + 1024 * 10, + fifo_size_ratio=0.2, + ghost_size_ratio=0.8, + move_to_main_threshold=2, + corr_window_ratio=0.3, + ) + # cache_name is "Clock2QPlus---" + assert cache.cache_name == "Clock2QPlus-0.2000-2-0.30" + + @pytest.mark.parametrize("n_samples", [32, 128]) + def test_beladysize_n_samples(self, n_samples): + """BeladySize must accept n_samples + + Regression test: the binding used to emit the key "n-samples" while the + C parser only accepts "n-sample", which aborted the process on every + BeladySize construction. + """ + from libcachesim import BeladySize + + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + cache = BeladySize(1024 * 10, n_samples=n_samples) + miss_ratio, _ = cache.process_trace(reader) + assert 0.0 <= miss_ratio <= 1.0 + + def test_belady_process_trace(self): + """Belady should produce a valid miss ratio on a synthetic trace""" + from libcachesim import Belady + + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + cache = Belady(1024 * 10) + miss_ratio, _ = cache.process_trace(reader) + assert 0.0 <= miss_ratio <= 1.0 + + def test_clock2qplus_process_trace(self): + """Clock2QPlus should produce a valid miss ratio on a synthetic trace""" + reader = SyntheticReader(num_of_req=1000, obj_size=100, alpha=1.0, dist="zipf", num_objects=100, seed=42) + cache = Clock2QPlus(1024 * 10) + miss_ratio, _ = cache.process_trace(reader) + assert 0.0 <= miss_ratio <= 1.0 From d6e67e7a75e529ce352896c9ffa63bafab6b659a Mon Sep 17 00:00:00 2001 From: haochengxia Date: Mon, 24 Aug 2026 18:09:18 -0600 Subject: [PATCH 2/4] fix outdated doc --- .gitignore | 3 +- docs/src/en/api.md | 12 ++++-- docs/src/en/examples/analysis.md | 28 +++++++++++++- docs/src/en/getting_started/installation.md | 43 +++++++++++++++++---- docs/src/zh/api.md | 6 +-- docs/src/zh/examples/analysis.md | 24 +++++++++++- docs/src/zh/getting_started/installation.md | 26 +++++++++++-- libcachesim/__init__.py | 2 + libcachesim/__init__.pyi | 9 ++--- libcachesim/cache.py | 4 +- 10 files changed, 128 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 0388f44..3e9df00 100644 --- a/.gitignore +++ b/.gitignore @@ -233,4 +233,5 @@ sftp-config.json CMakeFiles/* *.pyc !scripts/smart_build.py -uv.lock \ No newline at end of file +uv.lock +.claude/ \ No newline at end of file diff --git a/docs/src/en/api.md b/docs/src/en/api.md index 6c5e97a..f8f4380 100644 --- a/docs/src/en/api.md +++ b/docs/src/en/api.md @@ -40,8 +40,9 @@ Request( ### `CacheObject` -Returned by `Cache.find`, `insert`, `evict`, and `to_evict`. Exposes read-only `obj_id` and -`obj_size`. +Returned by `Cache.find`, `insert`, and `to_evict`. Exposes read-only `obj_id` and +`obj_size`. Note that `evict` returns `None` — it delegates to the cache's `void` eviction +callback, so use `to_evict` if you need to inspect the victim before it is removed. ## Enumerations @@ -210,7 +211,7 @@ PluginCache( ``` Hook signatures are documented in [Plugin System](examples/plugins.md#plugincache). -`set_hooks(...)` replaces the hooks on an existing instance. +Hooks are fixed at construction time; build a new `PluginCache` to change them. ## Admission policies @@ -320,7 +321,10 @@ create_uniform_requests(num_objects, num_requests, obj_size=4000, time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] ``` -Both return an **iterator**, not a list; wrap in `list(...)` if you need to replay them twice. +Both return a **re-iterable** generator object, not a list: each `for` loop over it starts a +fresh pass, and with a fixed `seed` every pass yields the same sequence. There is no need to +wrap them in `list(...)` to replay them, and for large workloads you should not — that +materialises every `Request` at once. ## `TraceAnalyzer` diff --git a/docs/src/en/examples/analysis.md b/docs/src/en/examples/analysis.md index 18563f9..6acd8d0 100644 --- a/docs/src/en/examples/analysis.md +++ b/docs/src/en/examples/analysis.md @@ -36,8 +36,32 @@ The constructor arguments are: !!! important The analyzer runs entirely in the C++ backend, so it only accepts a C-backed reader — in practice, [`TraceReader`](reader.md). Passing a `SyntheticReader` raises - `ReaderException: Only C/C++ reader is supported`. To analyse a synthetic workload, write it - out first with `Util.convert_to_oracleGeneral` and reopen it with `TraceReader`. + `ReaderException: Only C/C++ reader is supported`. `Util.convert_to_oracleGeneral` cannot + bridge the gap either — it takes a native reader, so handing it a `SyntheticReader` raises + `TypeError`. To analyse a synthetic workload, write its requests out as a trace file and + reopen that with `TraceReader`: + + ```py + import libcachesim as lcs + + synthetic = lcs.SyntheticReader(num_of_req=10000, obj_size=100, dist="zipf", + alpha=1.0, num_objects=1000, seed=42) + + with open("synthetic.csv", "w") as f: + for req in synthetic: + if not req.valid: + break + f.write(f"{req.clock_time},{req.obj_id},{req.obj_size}\n") + + init_params = lcs.ReaderInitParam(has_header=False, delimiter=",", obj_id_is_num=True) + init_params.time_field = 1 + init_params.obj_id_field = 2 + init_params.obj_size_field = 3 + + reader = lcs.TraceReader("synthetic.csv", lcs.TraceType.CSV_TRACE, init_params) + analyzer = lcs.TraceAnalyzer(reader, "synthetic_analysis") + analyzer.run() + ``` ## Selecting analyses diff --git a/docs/src/en/getting_started/installation.md b/docs/src/en/getting_started/installation.md index e8a7280..dd827f7 100644 --- a/docs/src/en/getting_started/installation.md +++ b/docs/src/en/getting_started/installation.md @@ -60,16 +60,26 @@ bash scripts/install_deps.sh bash scripts/install_deps_user.sh ``` -Then reinstall, passing the options through `CMAKE_ARGS`. Add `--no-cache-dir` to force a -rebuild rather than reusing a cached wheel: +Then reinstall, passing the options through `CMAKE_ARGS`. The flags only take effect during a +*source* build, so the install has to be forced to run one: `--no-cache-dir` on its own is not +enough, because pip may report `Requirement already satisfied` or install a prebuilt wheel, and +in either case `CMAKE_ARGS` is silently ignored. Build from the checkout instead: ```bash # Enable one algorithm -CMAKE_ARGS="-DENABLE_LRB=ON" pip install libcachesim --no-cache-dir +CMAKE_ARGS="-DENABLE_LRB=ON" pip install --force-reinstall . # Or enable all three CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" \ - pip install libcachesim --no-cache-dir + pip install --force-reinstall . +``` + +To build from PyPI rather than a checkout, also disable wheels so that a source build actually +happens: + +```bash +CMAKE_ARGS="-DENABLE_LRB=ON" \ + pip install --force-reinstall --no-binary libcachesim libcachesim ``` !!! important @@ -101,10 +111,27 @@ bash scripts/install.sh bash scripts/install.sh --all ``` -Building the extension requires a C++17 compiler, CMake ≥ 3.15, and Ninja. The build is driven -by [scikit-build-core](https://scikit-build-core.readthedocs.io/), which configures and builds -the bundled C library before compiling the [pybind11](https://pybind11.readthedocs.io/) -bindings. +Building the extension requires a C++17 compiler, CMake ≥ 3.15, and Ninja, plus three native +dependencies that CMake looks for at configure time: **pkg-config**, **GLib 2.0** and +**Zstandard**. All three are mandatory — `CMakeLists.txt` declares them with +`find_package(PkgConfig REQUIRED)`, `pkg_check_modules(GLib REQUIRED glib-2.0)` and +`find_package(ZSTD REQUIRED)` — and a missing one aborts configuration before any code is +compiled, with an error such as `No package 'glib-2.0' found`. + +The dependency scripts above install them on the platforms they cover (`install_deps.sh` targets +yum-based distributions and macOS). On Debian/Ubuntu, use the submodule's script or install them +directly: + +```bash +bash src/libCacheSim/scripts/install_dependency.sh + +# or, the minimum needed to configure the build +sudo apt install -y pkg-config libglib2.0-dev libzstd-dev +``` + +The build itself is driven by [scikit-build-core](https://scikit-build-core.readthedocs.io/), +which configures and builds the bundled C library before compiling the +[pybind11](https://pybind11.readthedocs.io/) bindings. ## Troubleshooting diff --git a/docs/src/zh/api.md b/docs/src/zh/api.md index 30dc559..09ea68b 100644 --- a/docs/src/zh/api.md +++ b/docs/src/zh/api.md @@ -38,7 +38,7 @@ Request( ### `CacheObject` -由 `Cache.find`、`insert`、`evict` 和 `to_evict` 返回,暴露只读的 `obj_id` 和 `obj_size`。 +由 `Cache.find`、`insert` 和 `to_evict` 返回,暴露只读的 `obj_id` 和 `obj_size`。注意 `evict` 返回的是 `None`——它转发到缓存的 `void` 淘汰回调;若需要在对象被移除前查看淘汰候选,请使用 `to_evict`。 ## 枚举类型 @@ -192,7 +192,7 @@ PluginCache( ) ``` -各 hook 的签名见[插件系统](examples/plugins.md#plugincache)。`set_hooks(...)` 可以替换已有实例上的 hook。 +各 hook 的签名见[插件系统](examples/plugins.md#plugincache)。hook 在构造时固定,如需更换请新建一个 `PluginCache`。 ## 准入策略 @@ -289,7 +289,7 @@ create_uniform_requests(num_objects, num_requests, obj_size=4000, time_span=604800, start_obj_id=0, seed=None) -> Iterator[Request] ``` -两者返回的都是**迭代器**而非列表;如果需要重复回放,请用 `list(...)` 包一层。 +两者返回的都是**可重复迭代**的生成器对象而非列表:每次 `for` 循环都会重新开始一轮,且在固定 `seed` 下每轮产生的序列完全相同。因此无需用 `list(...)` 包一层来重复回放;对于大规模负载更不应这样做,否则会一次性把所有 `Request` materialize 到内存中。 ## `TraceAnalyzer` diff --git a/docs/src/zh/examples/analysis.md b/docs/src/zh/examples/analysis.md index 077c0aa..d805848 100644 --- a/docs/src/zh/examples/analysis.md +++ b/docs/src/zh/examples/analysis.md @@ -30,7 +30,29 @@ analyzer.run() - `analysis_param: AnalysisParam`(可选)——这些分析的调节参数。默认为 `AnalysisParam()`。 !!! important - 分析器完全运行在 C++ 后端,因此只接受由 C 实现的 reader——实际上就是 [`TraceReader`](reader.md)。传入 `SyntheticReader` 会抛出 `ReaderException: Only C/C++ reader is supported`。若要分析合成负载,请先用 `Util.convert_to_oracleGeneral` 将其写出,再用 `TraceReader` 重新打开。 + 分析器完全运行在 C++ 后端,因此只接受由 C 实现的 reader——实际上就是 [`TraceReader`](reader.md)。传入 `SyntheticReader` 会抛出 `ReaderException: Only C/C++ reader is supported`。`Util.convert_to_oracleGeneral` 也无法作为桥梁——它接受的是原生 reader,传入 `SyntheticReader` 会抛出 `TypeError`。若要分析合成负载,请先把它的请求写成 trace 文件,再用 `TraceReader` 打开: + + ```py + import libcachesim as lcs + + synthetic = lcs.SyntheticReader(num_of_req=10000, obj_size=100, dist="zipf", + alpha=1.0, num_objects=1000, seed=42) + + with open("synthetic.csv", "w") as f: + for req in synthetic: + if not req.valid: + break + f.write(f"{req.clock_time},{req.obj_id},{req.obj_size}\n") + + init_params = lcs.ReaderInitParam(has_header=False, delimiter=",", obj_id_is_num=True) + init_params.time_field = 1 + init_params.obj_id_field = 2 + init_params.obj_size_field = 3 + + reader = lcs.TraceReader("synthetic.csv", lcs.TraceType.CSV_TRACE, init_params) + analyzer = lcs.TraceAnalyzer(reader, "synthetic_analysis") + analyzer.run() + ``` ## 选择分析项 {#selecting-analyses} diff --git a/docs/src/zh/getting_started/installation.md b/docs/src/zh/getting_started/installation.md index d3c6df0..ed0c8cb 100644 --- a/docs/src/zh/getting_started/installation.md +++ b/docs/src/zh/getting_started/installation.md @@ -56,15 +56,22 @@ bash scripts/install_deps.sh bash scripts/install_deps_user.sh ``` -然后通过 `CMAKE_ARGS` 传入选项重新安装。加上 `--no-cache-dir` 可以强制重新构建,而不是复用缓存的 wheel: +然后通过 `CMAKE_ARGS` 传入选项重新安装。这些选项只在**源码构建**时生效,因此必须强制走源码构建:仅加 `--no-cache-dir` 是不够的,pip 仍可能提示 `Requirement already satisfied` 或直接安装预编译 wheel,这两种情况下 `CMAKE_ARGS` 都会被静默忽略。请改为从当前 checkout 构建: ```bash # 启用单个算法 -CMAKE_ARGS="-DENABLE_LRB=ON" pip install libcachesim --no-cache-dir +CMAKE_ARGS="-DENABLE_LRB=ON" pip install --force-reinstall . # 或者三个全部启用 CMAKE_ARGS="-DENABLE_LRB=ON -DENABLE_3L_CACHE=ON -DENABLE_GLCACHE=ON" \ - pip install libcachesim --no-cache-dir + pip install --force-reinstall . +``` + +若要从 PyPI 而不是 checkout 构建,还需要禁用 wheel,确保真正执行源码构建: + +```bash +CMAKE_ARGS="-DENABLE_LRB=ON" \ + pip install --force-reinstall --no-binary libcachesim libcachesim ``` !!! important @@ -90,7 +97,18 @@ bash scripts/install.sh bash scripts/install.sh --all ``` -构建扩展需要支持 C++17 的编译器、CMake ≥ 3.15 以及 Ninja。构建过程由 [scikit-build-core](https://scikit-build-core.readthedocs.io/) 驱动,它会先配置并构建内置的 C 库,再编译 [pybind11](https://pybind11.readthedocs.io/) 绑定。 +构建扩展需要支持 C++17 的编译器、CMake ≥ 3.15 以及 Ninja,另外还需要三个在 configure 阶段查找的原生依赖:**pkg-config**、**GLib 2.0** 和 **Zstandard**。这三者都是必需的——`CMakeLists.txt` 中分别以 `find_package(PkgConfig REQUIRED)`、`pkg_check_modules(GLib REQUIRED glib-2.0)` 和 `find_package(ZSTD REQUIRED)` 声明——缺少任意一个都会在编译任何代码之前中断配置,并报出类似 `No package 'glib-2.0' found` 的错误。 + +上文的依赖脚本会在其支持的平台上安装它们(`install_deps.sh` 面向基于 yum 的发行版和 macOS)。在 Debian/Ubuntu 上,请使用子模块提供的脚本,或直接安装: + +```bash +bash src/libCacheSim/scripts/install_dependency.sh + +# 或者只装配置构建所需的最小集合 +sudo apt install -y pkg-config libglib2.0-dev libzstd-dev +``` + +构建本身由 [scikit-build-core](https://scikit-build-core.readthedocs.io/) 驱动,它会先配置并构建内置的 C 库,再编译 [pybind11](https://pybind11.readthedocs.io/) 绑定。 ## 疑难排查 diff --git a/libcachesim/__init__.py b/libcachesim/__init__.py index 52fe7af..1cd27ba 100644 --- a/libcachesim/__init__.py +++ b/libcachesim/__init__.py @@ -4,6 +4,7 @@ from .libcachesim_python import ( Cache, + CacheObject, Request, ReqOp, ReaderInitParam, @@ -77,6 +78,7 @@ __all__ = [ # Core classes "Cache", + "CacheObject", "Request", "ReqOp", "ReaderInitParam", diff --git a/libcachesim/__init__.pyi b/libcachesim/__init__.pyi index 51b581c..51719fe 100644 --- a/libcachesim/__init__.pyi +++ b/libcachesim/__init__.pyi @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Optional, Callable, Any from collections.abc import Iterator -from .libcachesim_python import ReqOp, TraceType, SamplerType +from .libcachesim_python import Reader, ReqOp, TraceType, SamplerType from .protocols import ReaderProtocol class Request: @@ -131,7 +131,7 @@ class Cache: def can_insert(self, req: Request) -> bool: ... def insert(self, req: Request) -> CacheObject: ... def need_eviction(self, req: Request) -> bool: ... - def evict(self, req: Request) -> CacheObject: ... + def evict(self, req: Request) -> None: ... def remove(self, obj_id: int) -> bool: ... def to_evict(self, req: Request) -> CacheObject: ... def get_occupied_byte(self) -> int: ... @@ -146,7 +146,7 @@ class CacheBase: def can_insert(self, req: Request) -> bool: ... def insert(self, req: Request) -> CacheObject: ... def need_eviction(self, req: Request) -> bool: ... - def evict(self, req: Request) -> CacheObject: ... + def evict(self, req: Request) -> None: ... def remove(self, obj_id: int) -> bool: ... def to_evict(self, req: Request) -> CacheObject: ... def get_occupied_byte(self) -> int: ... @@ -334,14 +334,13 @@ class PluginCache(CacheBase): admissioner: Optional["AdmissionerBase"] = None, reader: Optional[ReaderProtocol] = None, ): ... - def set_hooks(self, init_hook, hit_hook, miss_hook, eviction_hook, remove_hook, free_hook=None): ... # Readers class TraceReader(ReaderProtocol): c_reader: bool def __init__( self, - trace: str, + trace: Reader | str, trace_type: TraceType = TraceType.UNKNOWN_TRACE, reader_init_params: Optional[ReaderInitParam] = None, ): ... diff --git a/libcachesim/cache.py b/libcachesim/cache.py index eecb90a..a8e27c9 100644 --- a/libcachesim/cache.py +++ b/libcachesim/cache.py @@ -77,7 +77,9 @@ def insert(self, req: Request) -> CacheObject: def need_eviction(self, req: Request) -> bool: return self._cache.need_eviction(req) - def evict(self, req: Request) -> CacheObject: + def evict(self, req: Request) -> None: + # The C eviction callback returns void, so this is always None; use + # to_evict() to inspect the victim before it is removed. return self._cache.evict(req) def remove(self, obj_id: int) -> bool: From f094f395eaffddaa05629e725717a753c398eaaa Mon Sep 17 00:00:00 2001 From: haochengxia Date: Mon, 24 Aug 2026 18:18:50 -0600 Subject: [PATCH 3/4] fix doc claim --- docs/src/en/developer.md | 9 +++++++-- docs/src/en/getting_started/installation.md | 12 +++++++++++- docs/src/zh/developer.md | 5 ++++- docs/src/zh/getting_started/installation.md | 10 +++++++++- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/docs/src/en/developer.md b/docs/src/en/developer.md index e494d07..2007f3b 100644 --- a/docs/src/en/developer.md +++ b/docs/src/en/developer.md @@ -45,10 +45,15 @@ pip install -e ".[dev]" The `dev` extra brings in `pytest`, `ruff`, `mypy`, and `pre-commit`. -`scripts/install.sh` does all of the above and then runs the tests. Pass `--all` to enable the -optional learned algorithms: +`scripts/install.sh` automates most of this: it updates the submodule, installs the package +editable, verifies the import, and runs the tests. Note that it installs plain `-e .` plus +`pytest`, **not** the full `dev` extra — so `ruff`, `mypy` and `pre-commit` still need the +command above if you want them. Pass `--all` to enable the optional learned algorithms: ```bash +# --all only flips the CMake options on; install LightGBM/XGBoost first or +# configuration fails with LIGHTGBM_PATH not found +bash scripts/install_deps.sh # or install_deps_user.sh without sudo bash scripts/install.sh --all ``` diff --git a/docs/src/en/getting_started/installation.md b/docs/src/en/getting_started/installation.md index dd827f7..dc77de0 100644 --- a/docs/src/en/getting_started/installation.md +++ b/docs/src/en/getting_started/installation.md @@ -102,7 +102,7 @@ pip install . ``` `scripts/install.sh` wraps the whole flow — it updates the submodule, installs the package in -editable mode, checks that the import works, and runs the test suite: +editable mode (`pip install -e .`), checks that the import works, and runs the test suite: ```bash bash scripts/install.sh @@ -111,6 +111,16 @@ bash scripts/install.sh bash scripts/install.sh --all ``` +!!! warning "`--all` needs the learned-algorithm dependencies first" + `--all` only turns the three CMake options on; it does **not** install LightGBM and XGBoost. + On a machine that does not already have them, the build then fails during configuration with + an error such as `LIGHTGBM_PATH not found`. Run the dependency script first: + + ```bash + bash scripts/install_deps.sh # or install_deps_user.sh without sudo + bash scripts/install.sh --all + ``` + Building the extension requires a C++17 compiler, CMake ≥ 3.15, and Ninja, plus three native dependencies that CMake looks for at configure time: **pkg-config**, **GLib 2.0** and **Zstandard**. All three are mandatory — `CMakeLists.txt` declares them with diff --git a/docs/src/zh/developer.md b/docs/src/zh/developer.md index 8d41efb..1ab3a03 100644 --- a/docs/src/zh/developer.md +++ b/docs/src/zh/developer.md @@ -43,9 +43,12 @@ pip install -e ".[dev]" `dev` 附加依赖会引入 `pytest`、`ruff`、`mypy` 和 `pre-commit`。 -`scripts/install.sh` 会完成上述全部步骤并随后运行测试。加上 `--all` 可启用可选的学习型算法: +`scripts/install.sh` 自动化了其中大部分工作:更新子模块、以可编辑模式安装、验证导入,并运行测试。注意它安装的是普通的 `-e .` 外加 `pytest`,**并非**完整的 `dev` 附加依赖——如果需要 `ruff`、`mypy` 和 `pre-commit`,仍要执行上面的命令。加上 `--all` 可启用可选的学习型算法: ```bash +# --all 只是打开 CMake 选项;请先安装 LightGBM/XGBoost, +# 否则配置阶段会因 LIGHTGBM_PATH not found 而失败 +bash scripts/install_deps.sh # 无 sudo 权限时用 install_deps_user.sh bash scripts/install.sh --all ``` diff --git a/docs/src/zh/getting_started/installation.md b/docs/src/zh/getting_started/installation.md index ed0c8cb..da220a0 100644 --- a/docs/src/zh/getting_started/installation.md +++ b/docs/src/zh/getting_started/installation.md @@ -88,7 +88,7 @@ git submodule update --init --recursive pip install . ``` -`scripts/install.sh` 封装了整个流程——它会更新子模块、以可编辑模式安装本包、检查导入是否正常,并运行测试套件: +`scripts/install.sh` 封装了整个流程——它会更新子模块、以可编辑模式安装本包(`pip install -e .`)、检查导入是否正常,并运行测试套件: ```bash bash scripts/install.sh @@ -97,6 +97,14 @@ bash scripts/install.sh bash scripts/install.sh --all ``` +!!! warning "`--all` 需要先安装学习型算法的依赖" + `--all` 只是打开三个 CMake 选项,并**不会**安装 LightGBM 和 XGBoost。在尚未装好它们的机器上,构建会在配置阶段失败,报出类似 `LIGHTGBM_PATH not found` 的错误。请先运行依赖脚本: + + ```bash + bash scripts/install_deps.sh # 无 sudo 权限时用 install_deps_user.sh + bash scripts/install.sh --all + ``` + 构建扩展需要支持 C++17 的编译器、CMake ≥ 3.15 以及 Ninja,另外还需要三个在 configure 阶段查找的原生依赖:**pkg-config**、**GLib 2.0** 和 **Zstandard**。这三者都是必需的——`CMakeLists.txt` 中分别以 `find_package(PkgConfig REQUIRED)`、`pkg_check_modules(GLib REQUIRED glib-2.0)` 和 `find_package(ZSTD REQUIRED)` 声明——缺少任意一个都会在编译任何代码之前中断配置,并报出类似 `No package 'glib-2.0' found` 的错误。 上文的依赖脚本会在其支持的平台上安装它们(`install_deps.sh` 面向基于 yum 的发行版和 macOS)。在 Debian/Ubuntu 上,请使用子模块提供的脚本,或直接安装: From 4fd9d57fb083349a7cd0f462fd854bf51b5ec550 Mon Sep 17 00:00:00 2001 From: haochengxia Date: Tue, 25 Aug 2026 00:14:20 -0600 Subject: [PATCH 4/4] sync version --- CMakeLists.txt | 9 ++- docs/src/en/getting_started/installation.md | 19 ++++-- docs/src/zh/getting_started/installation.md | 15 ++++- pyproject.toml | 2 +- scripts/install_deps.sh | 69 ++++++++++++++++----- scripts/sync_version.py | 12 +++- 6 files changed, 99 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 297b526..5c06757 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -274,7 +274,14 @@ set_target_properties(libcachesim_python PROPERTIES INSTALL_RPATH "$ORIGIN" ) -target_compile_definitions(libcachesim_python PRIVATE VERSION_INFO=${PROJECT_VERSION}) +if(DEFINED SKBUILD_PROJECT_VERSION AND NOT SKBUILD_PROJECT_VERSION STREQUAL "") + set(LCS_PYTHON_VERSION ${SKBUILD_PROJECT_VERSION}) +else() + set(LCS_PYTHON_VERSION "dev") +endif() +message(STATUS "libcachesim_python version: ${LCS_PYTHON_VERSION}") + +target_compile_definitions(libcachesim_python PRIVATE VERSION_INFO=${LCS_PYTHON_VERSION}) # ============================================================================= # Platform-Specific Configuration diff --git a/docs/src/en/getting_started/installation.md b/docs/src/en/getting_started/installation.md index dc77de0..9958bb9 100644 --- a/docs/src/en/getting_started/installation.md +++ b/docs/src/en/getting_started/installation.md @@ -128,17 +128,26 @@ dependencies that CMake looks for at configure time: **pkg-config**, **GLib 2.0* `find_package(ZSTD REQUIRED)` — and a missing one aborts configuration before any code is compiled, with an error such as `No package 'glib-2.0' found`. -The dependency scripts above install them on the platforms they cover (`install_deps.sh` targets -yum-based distributions and macOS). On Debian/Ubuntu, use the submodule's script or install them -directly: +`scripts/install_deps.sh` installs them on Debian/Ubuntu, CentOS/RHEL and macOS, picking the +package manager to use automatically. It calls `sudo` only when not already running as root: ```bash -bash src/libCacheSim/scripts/install_dependency.sh +bash scripts/install_deps.sh +``` + +Or, on Debian/Ubuntu or macOS, install just the three by hand: -# or, the minimum needed to configure the build +```bash +# Debian/Ubuntu sudo apt install -y pkg-config libglib2.0-dev libzstd-dev + +# macOS +brew install pkgconf glib zstd ``` +On CentOS/RHEL prefer the script: it builds Zstandard from source, because the version in the +base repositories is often too old. + The build itself is driven by [scikit-build-core](https://scikit-build-core.readthedocs.io/), which configures and builds the bundled C library before compiling the [pybind11](https://pybind11.readthedocs.io/) bindings. diff --git a/docs/src/zh/getting_started/installation.md b/docs/src/zh/getting_started/installation.md index da220a0..f5aa296 100644 --- a/docs/src/zh/getting_started/installation.md +++ b/docs/src/zh/getting_started/installation.md @@ -107,15 +107,24 @@ bash scripts/install.sh --all 构建扩展需要支持 C++17 的编译器、CMake ≥ 3.15 以及 Ninja,另外还需要三个在 configure 阶段查找的原生依赖:**pkg-config**、**GLib 2.0** 和 **Zstandard**。这三者都是必需的——`CMakeLists.txt` 中分别以 `find_package(PkgConfig REQUIRED)`、`pkg_check_modules(GLib REQUIRED glib-2.0)` 和 `find_package(ZSTD REQUIRED)` 声明——缺少任意一个都会在编译任何代码之前中断配置,并报出类似 `No package 'glib-2.0' found` 的错误。 -上文的依赖脚本会在其支持的平台上安装它们(`install_deps.sh` 面向基于 yum 的发行版和 macOS)。在 Debian/Ubuntu 上,请使用子模块提供的脚本,或直接安装: +`scripts/install_deps.sh` 会在 Debian/Ubuntu、CentOS/RHEL 和 macOS 上安装它们,并自动选择对应的包管理器;只有在非 root 身份运行时才会调用 `sudo`: ```bash -bash src/libCacheSim/scripts/install_dependency.sh +bash scripts/install_deps.sh +``` + +如果不想运行该脚本,在 Debian/Ubuntu 或 macOS 上也可以手动只安装这三个依赖: -# 或者只装配置构建所需的最小集合 +```bash +# Debian/Ubuntu sudo apt install -y pkg-config libglib2.0-dev libzstd-dev + +# macOS +brew install pkgconf glib zstd ``` +在 CentOS/RHEL 上建议直接使用脚本:它会从源码构建 Zstandard,因为基础仓库中的版本通常过旧。 + 构建本身由 [scikit-build-core](https://scikit-build-core.readthedocs.io/) 驱动,它会先配置并构建内置的 C 库,再编译 [pybind11](https://pybind11.readthedocs.io/) 绑定。 ## 疑难排查 diff --git a/pyproject.toml b/pyproject.toml index 4d3ae9a..6f6dc36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "scikit_build_core.build" [project] name = "libcachesim" -version = "0.3.3.post4" +version = "0.3.5" description="Python bindings for libCacheSim" readme = "README.md" requires-python = ">=3.10" diff --git a/scripts/install_deps.sh b/scripts/install_deps.sh index 5deedf5..90b1827 100755 --- a/scripts/install_deps.sh +++ b/scripts/install_deps.sh @@ -21,6 +21,22 @@ log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2 } +# Elevation helper. +# +# This script is both a developer convenience and cibuildwheel's `before-all` +# (see [tool.cibuildwheel] in pyproject.toml), where it runs as root inside the +# manylinux container with no sudo available. Running as root leaves SUDO empty, +# so every command below is byte-for-byte what it was before this helper existed. +SUDO="" +if [[ ${EUID} -ne 0 ]]; then + if command -v sudo >/dev/null 2>&1; then + SUDO="sudo" + else + log_error "This script needs root privileges, but neither root nor sudo is available." + exit 1 + fi +fi + # Install and configure g++ version install_gcc() { log_info "Installing and configuring g++ version..." @@ -96,7 +112,7 @@ install_zstd() { pushd _build >/dev/null cmake -G Ninja .. ninja - ninja install + ${SUDO} ninja install popd >/dev/null popd >/dev/null popd >/dev/null @@ -114,7 +130,7 @@ install_xgboost() { pushd build >/dev/null cmake -G Ninja .. ninja - ninja install + ${SUDO} ninja install popd >/dev/null popd >/dev/null popd >/dev/null @@ -132,16 +148,45 @@ install_lightgbm() { pushd build >/dev/null cmake -G Ninja .. ninja - ninja install + ${SUDO} 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..." - +# Detect OS and install dependencies. +# +# Dispatch on the available package manager rather than on $OSTYPE alone, so that +# this covers the same platforms as upstream's +# src/libCacheSim/scripts/install_dependency.sh (Debian/Ubuntu/WSL, macOS, +# CentOS/RHEL). macOS is matched first; the manylinux images used for the release +# wheels ship yum/dnf but no apt-get, so they still take the yum branch below. +if [[ "$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 + +elif command -v apt-get >/dev/null 2>&1; then + log_info "Detected Debian/Ubuntu system, installing dependencies via apt..." + + ${SUDO} apt-get update + # CMakeLists.txt requires pkg-config, GLib 2.0 and Zstandard; the rest mirrors + # upstream's setup_ubuntu. Unlike the yum branch there is no need to build CMake + # or Zstd from source, since apt ships cmake >= 3.15 and libzstd-dev. + ${SUDO} apt-get install -y --no-install-recommends \ + build-essential pkg-config git wget cmake ninja-build \ + libglib2.0-dev libzstd-dev \ + libgoogle-perftools-dev google-perftools xxhash libunwind-dev + + # Needed by the optional learned algorithms (ENABLE_GLCACHE / ENABLE_3L_CACHE / + # ENABLE_LRB); no distro packages provide the CMake config files these need. + install_xgboost + install_lightgbm + +elif command -v yum >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then + log_info "Detected CentOS/RHEL system, installing dependencies via yum..." + # Enable EPEL repository yum install -y epel-release @@ -167,15 +212,9 @@ if [[ "$OSTYPE" == "linux-gnu"* ]]; then 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" + log_error "Unsupported operating system: $OSTYPE (no brew, apt-get, yum or dnf found)" exit 1 fi diff --git a/scripts/sync_version.py b/scripts/sync_version.py index 61dcb98..3d5dde2 100644 --- a/scripts/sync_version.py +++ b/scripts/sync_version.py @@ -13,6 +13,13 @@ from pathlib import Path +# Matches the [project] version line. The trailing [^"]* accepts PEP 440 suffixes such +# as ".post4", "rc1" or ".dev0" -- an earlier pattern required exactly N.N.N, so a +# version like "0.3.3.post4" matched nothing and this script silently stopped syncing. +# Anchoring on "dev" or a leading digit keeps it from matching pins like ">=3.15". +VERSION_LINE_RE = r"version = \"(dev|[0-9]+\.[0-9]+\.[0-9]+[^\"]*)\"" + + def get_project_root(): """Get the project root directory.""" script_dir = Path(__file__).parent @@ -52,7 +59,7 @@ def update_pyproject_toml(version): pyproject_data = f.read() # Update the version line in pyproject.toml, make it can match any version in version.txt, like "0.3.1" or "dev" - match = re.search(r"version = \"(dev|[0-9]+\.[0-9]+\.[0-9]+)\"", pyproject_data) + match = re.search(VERSION_LINE_RE, pyproject_data) if not match: print("Error: Could not find a valid version line in pyproject.toml", file=sys.stderr) return False @@ -65,7 +72,8 @@ def update_pyproject_toml(version): print(f"Warning: pyproject version {current_version} is newer than version.txt {version}, skipping update") return False # replace the version line with the new version - pyproject_data = re.sub(r"version = \"(dev|[0-9]+\.[0-9]+\.[0-9]+)\"", f'version = "{version}"', pyproject_data) + # count=1: only the [project] version, never the tool version pins further down. + pyproject_data = re.sub(VERSION_LINE_RE, f'version = "{version}"', pyproject_data, count=1) # Write back to file with proper formatting with open(pyproject_toml_path, "w") as f: