Compare commits
2 Commits
c3464d692e
...
b7e3a0da86
| Author | SHA1 | Date | |
|---|---|---|---|
| b7e3a0da86 | |||
| 3fd72705fc |
+9
-1
@@ -2,11 +2,19 @@
|
|||||||
/dist/
|
/dist/
|
||||||
/bin/
|
/bin/
|
||||||
steamcache2
|
steamcache2
|
||||||
|
|
||||||
|
# Downloaded SteamPrefill client simulator (auto-managed by make client)
|
||||||
|
/bin/steam-prefill/*
|
||||||
|
!/bin/steam-prefill/.gitkeep
|
||||||
/plans/
|
/plans/
|
||||||
|
|
||||||
#disk cache
|
#validation artifacts
|
||||||
|
/validate-disk/
|
||||||
/disk/
|
/disk/
|
||||||
|
|
||||||
|
#logs
|
||||||
|
*.log
|
||||||
|
|
||||||
#config file
|
#config file
|
||||||
/config.yaml
|
/config.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -33,15 +33,85 @@ bench: deps ## Run all benchmarks (MemoryFS + DiskFS variants, including all evi
|
|||||||
@go test -bench=. -benchmem -run=^$ -benchtime=1s ./vfs/disk
|
@go test -bench=. -benchmem -run=^$ -benchtime=1s ./vfs/disk
|
||||||
@echo "Bench done."
|
@echo "Bench done."
|
||||||
|
|
||||||
|
setcap: build ## Explicitly set cap_net_bind_service on the (just-built) binary for port 80 use outside validate targets
|
||||||
|
@echo "Setting cap_net_bind_service on the binary so it can listen on port 80 as your normal user..."
|
||||||
|
@sudo setcap 'cap_net_bind_service=+ep' dist/default_linux_amd64_v1/steamcache2
|
||||||
|
@echo "Done. You should now be able to run 'make run-validation' as your normal user (no root)."
|
||||||
|
|
||||||
|
validate run-validation: build ## Start steamcache2 on :80 with small test caches (foreground)
|
||||||
|
@echo "=== Starting steamcache2 in validation mode ==="
|
||||||
|
@echo "Port 80 + small memory/disk caches (for exercising disk tier, GC, etc.)"
|
||||||
|
@echo "Press Ctrl-C to stop the server."
|
||||||
|
@echo ""
|
||||||
|
@BINARY=dist/default_linux_amd64_v1/steamcache2; \
|
||||||
|
if [ "$$(id -u)" -ne 0 ] && ! getcap "$$BINARY" 2>/dev/null | grep -q cap_net_bind_service; then \
|
||||||
|
echo "Setting cap_net_bind_service on the freshly built binary (sudo may prompt)..."; \
|
||||||
|
sudo setcap 'cap_net_bind_service=+ep' "$$BINARY" || { \
|
||||||
|
echo "ERROR: setcap failed (or was cancelled)."; \
|
||||||
|
echo "You can run 'make setcap' manually, then retry 'make validate'."; \
|
||||||
|
exit 1; \
|
||||||
|
}; \
|
||||||
|
fi; \
|
||||||
|
if [ "$$(id -u)" -ne 0 ] && ! getcap "$$BINARY" 2>/dev/null | grep -q cap_net_bind_service; then \
|
||||||
|
echo "ERROR: Port 80 still requires the capability after setcap attempt."; \
|
||||||
|
echo "Run 'make setcap' and retry."; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
exec "$$BINARY" --config docs/examples/validate-config.yaml --log-level info
|
||||||
|
|
||||||
|
validate-check: ## Quick post-benchmark sanity check against a running steamcache2 (default port 80, override with PORT=xxxx)
|
||||||
|
@echo "=== steamcache2 Full Function Validation Report ==="
|
||||||
|
@PORT="$${PORT:-80}"; \
|
||||||
|
echo "Server: http://localhost:$$PORT"; \
|
||||||
|
curl -s --max-time 5 "http://localhost:$$PORT/metrics" || echo "(could not reach /metrics on port $$PORT - is the server running?)"; \
|
||||||
|
echo ""; \
|
||||||
|
echo "Tip: also inspect recent server logs for errors, coalesced hits, and disk activity."
|
||||||
|
|
||||||
|
prefill: ## Download latest SteamPrefill into bin/steam-prefill/SteamPrefill (gitignored)
|
||||||
|
@./scripts/download-prefill.sh
|
||||||
|
|
||||||
|
validate-kill: ## Kill leftover steamcache2 processes (safer, checks process name)
|
||||||
|
@echo "Looking for steamcache2 processes on common validation ports (80 is primary)..."
|
||||||
|
@for port in 80 8040 8080; do \
|
||||||
|
pids=""; \
|
||||||
|
if command -v ss >/dev/null 2>&1; then \
|
||||||
|
pids=$$(ss -tlnp 2>/dev/null | grep ":$${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | sort -u); \
|
||||||
|
fi; \
|
||||||
|
if [ -z "$$pids" ] && command -v lsof >/dev/null 2>&1; then \
|
||||||
|
pids=$$(lsof -ti :$${port} 2>/dev/null | sort -u); \
|
||||||
|
fi; \
|
||||||
|
for pid in $$pids; do \
|
||||||
|
proc=$$(ps -p $$pid -o comm= 2>/dev/null || true); \
|
||||||
|
cmd=$$(ps -p $$pid -o cmd= 2>/dev/null || true); \
|
||||||
|
if echo "$$proc $$cmd" | grep -qi "steamcache"; then \
|
||||||
|
echo " Killing steamcache2 (port $$port, PID $$pid, $$proc)"; \
|
||||||
|
kill -TERM $$pid 2>/dev/null || true; \
|
||||||
|
sleep 0.2; \
|
||||||
|
kill -0 $$pid 2>/dev/null && kill -9 $$pid 2>/dev/null || true; \
|
||||||
|
else \
|
||||||
|
echo " Skipping PID $$pid on port $$port (not steamcache2: $$proc)"; \
|
||||||
|
fi; \
|
||||||
|
done; \
|
||||||
|
done
|
||||||
|
@echo "Validation server cleanup complete."
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
help: ## Show this help message
|
help: ## Show this help message
|
||||||
@echo steamcache2 Makefile
|
@echo "steamcache2 Makefile"
|
||||||
@echo Available targets:
|
@echo "Available targets:"
|
||||||
@echo run Run the application (cross-platform via go run)
|
@echo " run Run the application (cross-platform via go run)"
|
||||||
@echo run-debug Run the application with debug logging (cross-platform)
|
@echo " run-debug Run the application with debug logging (cross-platform)"
|
||||||
@echo build Build the application (goreleaser snapshot)
|
@echo " build Build the application (goreleaser snapshot)"
|
||||||
@echo test Run all tests
|
@echo " test Run all tests"
|
||||||
@echo test-race Run all tests with the race detector
|
@echo " test-race Run all tests with the race detector"
|
||||||
@echo lint Run golangci-lint + review label check
|
@echo " lint Run golangci-lint + review label check"
|
||||||
@echo check-review-labels Fail on temporary review labels (P*, T*, I*, R*, etc.)
|
@echo " check-review-labels Fail on temporary review labels (P*, T*, I*, R*, etc.)"
|
||||||
@echo deps Download dependencies
|
@echo " deps Download dependencies"
|
||||||
@echo clean Remove build/test artifacts
|
@echo " clean Remove build/test artifacts"
|
||||||
|
@echo " bench Run low-level VFS microbenchmarks"
|
||||||
|
@echo " validate / run-validation Start server on :80 (builds, auto-setcaps fresh binary, then runs as normal user)"
|
||||||
|
@echo " setcap Explicitly set cap on current build (for port 80 use outside validate)"
|
||||||
|
@echo " validate-check Quick /metrics report after running a workload"
|
||||||
|
@echo " validate-kill Kill leftover steamcache2 processes (safer)"
|
||||||
|
@echo " prefill Download latest SteamPrefill into bin/steam-prefill/ (for use with run-validation)"
|
||||||
@@ -61,6 +61,113 @@ Run `make help` to see the full list of available commands.
|
|||||||
|
|
||||||
This is the preferred approach for day-to-day development. Avoid running raw `go test`, `go run`, or `golangci-lint` commands directly for routine tasks.
|
This is the preferred approach for day-to-day development. Avoid running raw `go test`, `go run`, or `golangci-lint` commands directly for routine tasks.
|
||||||
|
|
||||||
|
### Validating Full Functionality with external tools
|
||||||
|
|
||||||
|
steamcache2 provides a convenient small-cache configuration and helper targets so you can easily validate behavior using external tools such as [SteamPrefill (tpill90/steam-lancache-prefill)](https://github.com/tpill90/steam-lancache-prefill).
|
||||||
|
|
||||||
|
This gives you:
|
||||||
|
- Real Steam manifest + chunk traffic (no reinventing the wheel)
|
||||||
|
- Excellent `benchmark setup` / `benchmark run` workflow with warmup, randomization, and mixed chunk sizes
|
||||||
|
- The ability to validate a **just-built binary** end-to-end (caching, coalescing, Range support, memory+disk tiers, GC/eviction, metrics, special endpoints, startup validation, etc.)
|
||||||
|
|
||||||
|
#### Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build the binary (this also runs short tests)
|
||||||
|
make build
|
||||||
|
|
||||||
|
# 2. Start a validation-oriented instance (small caches so disk tier + GC get exercised)
|
||||||
|
# Uses port 80 by default; the script will automatically set the needed
|
||||||
|
# capability on the binary via sudo setcap if it is missing.
|
||||||
|
./scripts/validate-with-prefill.sh
|
||||||
|
|
||||||
|
# 3. In another terminal (or on another machine), create a workload once if you haven't already,
|
||||||
|
# then drive it through your local steamcache2.
|
||||||
|
# Note: when using a non-80 port you may need to give SteamPrefill the full address.
|
||||||
|
./scripts/validate-with-prefill.sh # (shows the exact commands with the correct port)
|
||||||
|
```
|
||||||
|
|
||||||
|
When the benchmark finishes, press Ctrl-C in the first terminal to cleanly stop the server.
|
||||||
|
|
||||||
|
#### Simple validation server (recommended for manual testing)
|
||||||
|
|
||||||
|
For easy validation with external tools (SteamPrefill, etc.), use:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make run-validation
|
||||||
|
# or
|
||||||
|
make validate
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts `steamcache2` on port 80 using a deliberately small memory + disk configuration (good for exercising the disk tier, GC, coalescing, promotions, etc.).
|
||||||
|
|
||||||
|
`make run-validation` (and `make validate`) will automatically ensure the `cap_net_bind_service` capability is set on the binary it just built (one sudo prompt the first time after each rebuild). This keeps the server running as your normal user so the disk cache directory stays owned by you.
|
||||||
|
|
||||||
|
If you want the capability on the binary for other workflows (e.g. `make run`, or running the binary directly on port 80), use the explicit target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make setcap
|
||||||
|
```
|
||||||
|
|
||||||
|
When the server is running, point your external SteamPrefill (or other load generator) at it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./SteamPrefill benchmark run ...
|
||||||
|
```
|
||||||
|
|
||||||
|
When finished, you can get a quick metrics summary with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make validate-check
|
||||||
|
```
|
||||||
|
|
||||||
|
This is the recommended simple workflow. No automatic downloading or running of external tools.
|
||||||
|
|
||||||
|
#### Inspecting the Result
|
||||||
|
|
||||||
|
After a benchmark run you can ask for a quick report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make validate-check
|
||||||
|
# or manually:
|
||||||
|
curl -s http://localhost/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Look for:
|
||||||
|
- High cache hit rate after the warmup pass
|
||||||
|
- Non-zero `coalesced` and `disk` activity
|
||||||
|
- Zero unexpected errors
|
||||||
|
|
||||||
|
#### The Validation Config
|
||||||
|
|
||||||
|
The script uses [docs/examples/validate-config.yaml](docs/examples/validate-config.yaml). It enables both memory and disk tiers at modest sizes (128 MB / 512 MB) with conservative concurrency. Edit or copy it if you need larger caches for bigger workloads.
|
||||||
|
|
||||||
|
#### What Gets Validated
|
||||||
|
|
||||||
|
Running a realistic SteamPrefill benchmark workload through a built steamcache2 exercises the complete public surface that matters for production use:
|
||||||
|
- Steam User-Agent detection and depot/manifest/chunk URL patterns
|
||||||
|
- Full MISS → cache write → HIT (and HIT-COALESCED) paths
|
||||||
|
- Range request handling from cached full responses
|
||||||
|
- Request coalescing under concurrent load
|
||||||
|
- Memory tier + disk tier interaction (including async disk attach)
|
||||||
|
- Garbage collection and eviction under pressure
|
||||||
|
- Metrics and special endpoints (`/`, `/lancache-heartbeat`, `/metrics`)
|
||||||
|
- Per-client and global rate limiting (with trusted proxy handling)
|
||||||
|
- Startup configuration validation and upstream behavior
|
||||||
|
- Clean shutdown hygiene
|
||||||
|
|
||||||
|
This is the closest practical equivalent to "run the thing real clients will run and make sure nothing is broken."
|
||||||
|
|
||||||
|
#### Troubleshooting
|
||||||
|
|
||||||
|
- **Low hit rate on first run**: Normal. The first `benchmark run` is the warmup that populates the cache.
|
||||||
|
- **Want to test real disk I/O (not RAM cache)**: Make sure your workload size (shown by `benchmark setup`) is larger than the total RAM on the machine running steamcache2.
|
||||||
|
- **Server won't start or bind on port 80 as non-root**: `make run-validation` and `make validate` automatically run `setcap` on the binary they just built. If it still fails, run `make setcap` explicitly and retry. The server always runs as your normal user (no root) so the disk cache directory ownership stays correct.
|
||||||
|
- **SteamPrefill not found**: Install it yourself from its GitHub releases. Then use `make validate` to start the server with small caches and point SteamPrefill at it manually.
|
||||||
|
- **SteamPrefill won't use server as cache properly**: SteamPrefill has some bad autodetectiong functions sometimes it works when the server is resolvable from localhost or 127.0.0.1 other times you have to fully override the dns for the proper dns name lancache.steamcontent.com to point to 127.0.0.1 i don't recommend doing it unless your okay with having to undo and redo it depending on if your running the server or not its a pain.
|
||||||
|
|
||||||
|
See also the SteamPrefill documentation for `benchmark setup` and `benchmark run` options.
|
||||||
|
|
||||||
### Command Line Flags
|
### Command Line Flags
|
||||||
|
|
||||||
While most configuration is done via the YAML file, some runtime options are still available as command-line flags:
|
While most configuration is done via the YAML file, some runtime options are still available as command-line flags:
|
||||||
@@ -154,11 +261,11 @@ SteamCache2 supports different garbage collection algorithms for memory and disk
|
|||||||
**Available GC Algorithms:**
|
**Available GC Algorithms:**
|
||||||
|
|
||||||
- **`lru`** (default): Least Recently Used - evicts oldest accessed files
|
- **`lru`** (default): Least Recently Used - evicts oldest accessed files
|
||||||
- **`lfu`**: Least Frequently Used (P1 real impl) - evicts by lowest AccessCount (tiebreak older ATime); uses existing FileInfo counters
|
- **`lfu`**: Least Frequently Used - evicts by lowest AccessCount (tiebreak older ATime); uses existing FileInfo counters
|
||||||
- **`fifo`**: First In, First Out - evicts oldest created files (predictable)
|
- **`fifo`**: First In, First Out - evicts oldest created files (predictable and terrible all in one) don't ever use it
|
||||||
- **`largest`**: Size-based - evicts largest files first (maximizes file count)
|
- **`largest`**: Size-based - evicts largest files first (maximizes small file count) if used on memory greatly improves access time
|
||||||
- **`smallest`**: Size-based - evicts smallest files first (maximizes cache hit rate)
|
- **`smallest`**: Size-based - evicts smallest files first (maximizes large file count) probably best used for disk since there kinda slow with small files
|
||||||
- **`hybrid`**: Recency + frequency hybrid (P1 meaningful) - evicts by lowest time-decayed score (GetTimeDecayedScore combining ATime + AccessCount)
|
- **`hybrid`**: Recency + frequency hybrid - evicts by lowest time-decayed score (GetTimeDecayedScore combining ATime + AccessCount)
|
||||||
|
|
||||||
**Recommended Algorithms by Cache Type:**
|
**Recommended Algorithms by Cache Type:**
|
||||||
|
|
||||||
@@ -166,18 +273,19 @@ SteamCache2 supports different garbage collection algorithms for memory and disk
|
|||||||
- **`lru`** - Best overall performance, good balance of speed and hit rate
|
- **`lru`** - Best overall performance, good balance of speed and hit rate
|
||||||
- **`lfu`** - Excellent for gaming cafes where popular games stay cached
|
- **`lfu`** - Excellent for gaming cafes where popular games stay cached
|
||||||
- **`hybrid`** - Optimal for mixed workloads with varying file sizes
|
- **`hybrid`** - Optimal for mixed workloads with varying file sizes
|
||||||
|
- **`largest`** - Crazy good for access times since disks are slow with lots of tiny files
|
||||||
|
|
||||||
**For Disk Cache (Slow, Large Size):**
|
**For Disk Cache (Slow, Large Size):**
|
||||||
- **`hybrid`** - Recommended for optimal performance, balances speed and storage efficiency
|
- **`hybrid`** - Recommended for optimal performance, balances speed and storage efficiency
|
||||||
- **`largest`** - Good for maximizing number of cached files
|
- **`smallest`** - Good for maximizing linear reads which is the only place spinning disks have performance don't expect too much though steam kinda uses small files
|
||||||
- **`lru`** - Reliable default with good performance
|
- **`lru`** - Reliable default with good performance
|
||||||
|
|
||||||
**Use Cases:**
|
**Use Cases:**
|
||||||
- **Gaming Cafes**: Use `lfu` for memory, `hybrid` for disk
|
- **Gaming Cafes**: Use `largest` for memory, `hybrid` for disk
|
||||||
- **LAN Events**: Use `lfu` for memory, `hybrid` for disk
|
- **LAN Events**: Use `largest` for memory, `hybrid` for disk
|
||||||
- **Home Use**: Use `lru` for memory, `hybrid` for disk
|
- **Home Use**: Use `largest` for memory, `hybrid` for disk
|
||||||
- **Testing**: Use `fifo` for predictable behavior
|
- **Testing**: Use `fifo` for nothing its pointless
|
||||||
- **Large File Storage**: Use `largest` for disk to maximize file count
|
- **Large File Storage**: Use `smallest` for disk get rid of the slow tiny files first
|
||||||
|
|
||||||
### DNS Configuration
|
### DNS Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# validate-config.yaml
|
||||||
|
#
|
||||||
|
# Small dual-tier configuration intended for full-function validation of a
|
||||||
|
# built steamcache2 binary using realistic Steam client workloads driven by
|
||||||
|
# the external SteamPrefill (https://github.com/tpill90/steam-lancache-prefill)
|
||||||
|
# "benchmark" commands.
|
||||||
|
#
|
||||||
|
# Why these values?
|
||||||
|
# - Both tiers enabled. Memory is sized large enough to survive the disk attach
|
||||||
|
# window in mixed mode (see steamcache.go: the goroutine that blocks on d.Size()
|
||||||
|
# before SetSlow). With a realistic SteamPrefill benchmark (high rate of unique
|
||||||
|
# ~1MB chunks) the old tiny 128MB mem + 512MB disk caused almost all early
|
||||||
|
# content to live only in memory, get evicted by its GC, and never reach disk.
|
||||||
|
# Result: "never hitting", hit_rate 0, memory_size 0, despite files appearing
|
||||||
|
# on disk for late-arriving chunks. Larger mem + disk makes the validation
|
||||||
|
# actually exercise hits, promotions, disk tier, and GC as intended.
|
||||||
|
# - Conservative concurrency limits suitable for a developer laptop.
|
||||||
|
# - trusted_proxies set for 127.0.0.0/8 so that an external benchmark tool
|
||||||
|
# can simulate multiple distinct clients via X-Forwarded-For if desired.
|
||||||
|
# - upstream left empty: the server will use the incoming Host header
|
||||||
|
# (exactly what happens when you point SteamPrefill at your Lancache IP).
|
||||||
|
#
|
||||||
|
# Usage (typical dev workflow):
|
||||||
|
# make build
|
||||||
|
# ./scripts/validate-with-prefill.sh
|
||||||
|
# # In another terminal:
|
||||||
|
# SteamPrefill benchmark run -c 20 ...
|
||||||
|
#
|
||||||
|
# After the benchmark run, inspect with:
|
||||||
|
# curl -s http://localhost/metrics
|
||||||
|
#
|
||||||
|
# Tweak sizes upward if you want to run very large workloads while still
|
||||||
|
# exercising the disk tier (workload >> RAM is ideal for real disk testing).
|
||||||
|
|
||||||
|
listen_address: :80
|
||||||
|
|
||||||
|
max_concurrent_requests: 1000
|
||||||
|
max_requests_per_client: 10
|
||||||
|
|
||||||
|
max_object_size: "0" # unlimited for validation (real Steam files can be large)
|
||||||
|
trusted_proxies: ["127.0.0.0/8"]
|
||||||
|
|
||||||
|
cache:
|
||||||
|
memory:
|
||||||
|
size: 1GB
|
||||||
|
gc_algorithm: largest
|
||||||
|
disk:
|
||||||
|
size: 2GB
|
||||||
|
path: ./validate-disk # ephemeral; clean between runs if you want a fresh test
|
||||||
|
gc_algorithm: hybrid # recommended for disk in the project README
|
||||||
|
|
||||||
|
# Empty upstream = use Host header from the client (SteamPrefill / real Steam clients).
|
||||||
|
# This matches the common "DNS points lancache.steamcontent.com at the cache" setup.
|
||||||
|
upstream: ""
|
||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
github.com/spf13/cobra v1.8.1
|
github.com/spf13/cobra v1.8.1
|
||||||
golang.org/x/sync v0.16.0
|
golang.org/x/sync v0.16.0
|
||||||
|
golang.org/x/sys v0.12.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,5 +17,4 @@ require (
|
|||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
golang.org/x/sys v0.12.0 // indirect
|
|
||||||
)
|
)
|
||||||
|
|||||||
Executable
+155
@@ -0,0 +1,155 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# download-prefill.sh
|
||||||
|
#
|
||||||
|
# Downloads the latest (or specific) release of SteamPrefill
|
||||||
|
# (https://github.com/tpill90/steam-lancache-prefill) into
|
||||||
|
# bin/steam-prefill/SteamPrefill
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./scripts/download-prefill.sh
|
||||||
|
#
|
||||||
|
# Environment:
|
||||||
|
# PREFILL_VERSION - Pin a specific version tag (e.g. v3.4.2)
|
||||||
|
# PREFILL_FORCE - Set to any non-empty value to re-download even if present
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DEST_DIR="bin/steam-prefill"
|
||||||
|
TARGET="$DEST_DIR/SteamPrefill"
|
||||||
|
|
||||||
|
mkdir -p "$DEST_DIR"
|
||||||
|
|
||||||
|
if [[ -x "$TARGET" && -z "${PREFILL_FORCE:-}" ]]; then
|
||||||
|
echo "SteamPrefill already present at $TARGET"
|
||||||
|
echo "Run with PREFILL_FORCE=1 to re-download."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION="${PREFILL_VERSION:-}"
|
||||||
|
|
||||||
|
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64|amd64) ARCH_NAME="x64" ;;
|
||||||
|
aarch64|arm64) ARCH_NAME="arm64" ;;
|
||||||
|
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$OS" in
|
||||||
|
linux) OS_NAME="linux" ;;
|
||||||
|
darwin) OS_NAME="osx" ;;
|
||||||
|
*) echo "Unsupported OS: $OS"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Resolving SteamPrefill version..."
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" ]]; then
|
||||||
|
# Follow the /latest redirect to discover the current tag
|
||||||
|
LATEST_URL=$(curl -sIL -o /dev/null -w '%{url_effective}' \
|
||||||
|
"https://github.com/tpill90/steam-lancache-prefill/releases/latest" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$LATEST_URL" =~ /tag/([^/?#]+) ]]; then
|
||||||
|
VERSION="${BASH_REMATCH[1]}"
|
||||||
|
else
|
||||||
|
echo "Failed to resolve latest version from GitHub redirect."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading SteamPrefill $VERSION for ${OS_NAME}-${ARCH_NAME}..."
|
||||||
|
|
||||||
|
rm -f "$TARGET" "$TARGET.tmp" 2>/dev/null || true
|
||||||
|
|
||||||
|
DOWNLOADED=0
|
||||||
|
|
||||||
|
# Preferred: query the GitHub API for the exact asset list (most reliable)
|
||||||
|
API_URL="https://api.github.com/repos/tpill90/steam-lancache-prefill/releases/tags/${VERSION}"
|
||||||
|
ASSET_URL=""
|
||||||
|
|
||||||
|
if command -v jq >/dev/null 2>&1; then
|
||||||
|
echo " Querying GitHub API for assets..."
|
||||||
|
ASSET_NAME=$(curl -fsSL "$API_URL" 2>/dev/null | jq -r --arg os "$OS_NAME" --arg arch "$ARCH_NAME" '
|
||||||
|
.assets[]
|
||||||
|
| select(.name | ascii_downcase | contains($os))
|
||||||
|
| select(.name | ascii_downcase | contains($arch))
|
||||||
|
| .name
|
||||||
|
' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$ASSET_NAME" ]]; then
|
||||||
|
ASSET_URL="https://github.com/tpill90/steam-lancache-prefill/releases/download/${VERSION}/${ASSET_NAME}"
|
||||||
|
echo " Found asset via API: $ASSET_NAME"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback: try common name patterns if API or jq not available
|
||||||
|
if [[ -z "$ASSET_URL" ]]; then
|
||||||
|
echo " Trying common asset name patterns..."
|
||||||
|
CANDIDATES=(
|
||||||
|
"SteamPrefill-${VERSION}-${OS_NAME}-${ARCH_NAME}.zip"
|
||||||
|
"SteamPrefill-${VERSION}-${OS_NAME}-${ARCH_NAME}"
|
||||||
|
"SteamPrefill-${OS_NAME}-${ARCH_NAME}.zip"
|
||||||
|
"SteamPrefill-${OS_NAME}-${ARCH_NAME}"
|
||||||
|
"SteamPrefill-linux-${ARCH_NAME}.zip"
|
||||||
|
"SteamPrefill-linux-${ARCH_NAME}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for name in "${CANDIDATES[@]}"; do
|
||||||
|
URL="https://github.com/tpill90/steam-lancache-prefill/releases/download/${VERSION}/${name}"
|
||||||
|
echo " Trying $name ..."
|
||||||
|
if curl -fI -s --retry 2 "$URL" >/dev/null 2>&1; then
|
||||||
|
ASSET_URL="$URL"
|
||||||
|
ASSET_NAME="$name"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$ASSET_URL" ]]; then
|
||||||
|
echo "Downloading $ASSET_NAME ..."
|
||||||
|
if curl -fL --retry 3 --retry-delay 2 -A "Mozilla/5.0 (compatible; SteamPrefill-Downloader)" \
|
||||||
|
--progress-bar -o "$TARGET.tmp" "$ASSET_URL"; then
|
||||||
|
|
||||||
|
echo "Download complete."
|
||||||
|
|
||||||
|
if [[ "$ASSET_NAME" == *.zip ]]; then
|
||||||
|
echo "Extracting..."
|
||||||
|
if ! command -v unzip >/dev/null 2>&1; then
|
||||||
|
echo "Error: unzip is required for this release."
|
||||||
|
rm -f "$TARGET.tmp"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
unzip -o -q "$TARGET.tmp" -d "$DEST_DIR"
|
||||||
|
FOUND=$(find "$DEST_DIR" -type f -name "SteamPrefill" | head -1)
|
||||||
|
if [[ -n "$FOUND" ]]; then
|
||||||
|
mv "$FOUND" "$TARGET"
|
||||||
|
fi
|
||||||
|
rm -f "$TARGET.tmp"
|
||||||
|
find "$DEST_DIR" -mindepth 1 -maxdepth 1 -type d -name "SteamPrefill*" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
else
|
||||||
|
mv "$TARGET.tmp" "$TARGET"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod +x "$TARGET"
|
||||||
|
DOWNLOADED=1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $DOWNLOADED -eq 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "Failed to download a matching asset for $VERSION."
|
||||||
|
echo "You can try pinning a different version:"
|
||||||
|
echo " PREFILL_VERSION=vX.Y.Z ./scripts/download-prefill.sh"
|
||||||
|
echo ""
|
||||||
|
echo "Or download manually from:"
|
||||||
|
echo " https://github.com/tpill90/steam-lancache-prefill/releases/tag/${VERSION}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Installed SteamPrefill $VERSION → $TARGET"
|
||||||
|
echo ""
|
||||||
|
echo "You can now run it directly, for example:"
|
||||||
|
echo " ./bin/steam-prefill/SteamPrefill --help"
|
||||||
|
echo " ./bin/steam-prefill/SteamPrefill benchmark run ..."
|
||||||
Executable
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# validate-with-prefill.sh
|
||||||
|
#
|
||||||
|
# Thin glue script to make it trivial for developers to validate complete
|
||||||
|
# steamcache2 functionality using the external SteamPrefill (lancacheprefill)
|
||||||
|
# tool as the realistic client simulator.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# 1. make build
|
||||||
|
# 2. ./scripts/validate-with-prefill.sh
|
||||||
|
# (Automatically kills any leftover steamcache2 on the target port first.)
|
||||||
|
# 3. In another terminal (or on another machine), run the printed
|
||||||
|
# SteamPrefill benchmark commands (the script tells you the exact address/port).
|
||||||
|
# 4. After the benchmark finishes, run the suggested metrics check.
|
||||||
|
# 5. Ctrl-C here to cleanly stop the steamcache2 instance.
|
||||||
|
#
|
||||||
|
# This script + the accompanying validate-config.yaml + README docs are the
|
||||||
|
# entire "couple little scripts to hook steamcache2 and lancacheprefill together"
|
||||||
|
# implementation. No Go code, no new dependencies, stays outside go test / bench.
|
||||||
|
#
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# --- Locate the built steamcache2 binary (produced by "make build") ---
|
||||||
|
BINARY=""
|
||||||
|
for candidate in \
|
||||||
|
"dist/default_linux_amd64_v1/steamcache2" \
|
||||||
|
"dist/steamcache2" \
|
||||||
|
"./steamcache2" \
|
||||||
|
"steamcache2"
|
||||||
|
do
|
||||||
|
if [[ -x "$candidate" ]]; then
|
||||||
|
BINARY="$candidate"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z "$BINARY" ]]; then
|
||||||
|
echo "ERROR: Could not find a built steamcache2 binary."
|
||||||
|
echo "Run 'make build' first (or place the binary in one of the searched locations)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using steamcache2 binary: $BINARY"
|
||||||
|
|
||||||
|
# --- Validation config (small dual-tier so disk + GC get real exercise) ---
|
||||||
|
# Source of truth lives in docs/examples/ (safe from "make clean").
|
||||||
|
# The script will also accept an explicit path via STEAMCACHE2_VALIDATE_CONFIG.
|
||||||
|
VALIDATE_CONFIG="${STEAMCACHE2_VALIDATE_CONFIG:-docs/examples/validate-config.yaml}"
|
||||||
|
if [[ ! -f "$VALIDATE_CONFIG" ]]; then
|
||||||
|
echo "ERROR: Validation config not found at: $VALIDATE_CONFIG"
|
||||||
|
echo "Set STEAMCACHE2_VALIDATE_CONFIG=/path/to/your-config.yaml or place a copy at docs/examples/validate-config.yaml"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract the listen port from the config (supports ":80", "127.0.0.1:80", etc.)
|
||||||
|
# Falls back to 80 if we can't parse it.
|
||||||
|
PORT=$(grep -E '^\s*listen_address:' "$VALIDATE_CONFIG" | head -1 | sed -E 's/.*:([0-9]+).*/\1/' || true)
|
||||||
|
if [[ -z "$PORT" || ! "$PORT" =~ ^[0-9]+$ ]]; then
|
||||||
|
PORT=80
|
||||||
|
fi
|
||||||
|
|
||||||
|
SERVER_URL="http://localhost:${PORT}"
|
||||||
|
|
||||||
|
# For privileged ports (<1024, i.e. the default :80) we require the
|
||||||
|
# cap_net_bind_service capability. We apply it automatically here (via sudo
|
||||||
|
# setcap) on the binary we are about to run. This happens after any build
|
||||||
|
# so the cap is never "lost" when the binary is rebuilt.
|
||||||
|
if [ "$PORT" -lt 1024 ] && [ "$(id -u)" -ne 0 ]; then
|
||||||
|
if ! command -v getcap >/dev/null 2>&1 || ! getcap "$BINARY" 2>/dev/null | grep -q "cap_net_bind_service"; then
|
||||||
|
echo "Setting cap_net_bind_service on the binary (sudo may prompt)..."
|
||||||
|
if ! sudo setcap 'cap_net_bind_service=+ep' "$BINARY"; then
|
||||||
|
echo "ERROR: Failed to set capability."
|
||||||
|
echo "Run 'make setcap' manually, then retry."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Safely kill only steamcache2 processes listening on this specific port.
|
||||||
|
# We look up PIDs on the port, check their actual process name/command,
|
||||||
|
# and only kill via PID if it looks like steamcache2.
|
||||||
|
echo "Checking for leftover steamcache2 processes on port ${PORT}..."
|
||||||
|
|
||||||
|
kill_steamcache_on_port() {
|
||||||
|
local port=$1
|
||||||
|
local pids=""
|
||||||
|
|
||||||
|
# Try ss first (modern, usually available)
|
||||||
|
if command -v ss >/dev/null 2>&1; then
|
||||||
|
pids=$(ss -tlnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | sort -u)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Fallback to lsof
|
||||||
|
if [[ -z "$pids" ]] && command -v lsof >/dev/null 2>&1; then
|
||||||
|
pids=$(lsof -ti :${port} 2>/dev/null | sort -u)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$pids" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for pid in $pids; do
|
||||||
|
# Get process name and command line
|
||||||
|
local proc_name
|
||||||
|
local cmdline
|
||||||
|
proc_name=$(ps -p "$pid" -o comm= 2>/dev/null || true)
|
||||||
|
cmdline=$(ps -p "$pid" -o cmd= 2>/dev/null || true)
|
||||||
|
|
||||||
|
# Check if this looks like a steamcache2 process
|
||||||
|
if echo "$proc_name $cmdline" | grep -qi "steamcache"; then
|
||||||
|
echo " → Found steamcache2 on port ${port} (PID $pid, name: ${proc_name:-unknown})"
|
||||||
|
kill -TERM "$pid" 2>/dev/null || true
|
||||||
|
sleep 0.3
|
||||||
|
# If still alive, force kill
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
kill -9 "$pid" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo " Killed PID $pid"
|
||||||
|
else
|
||||||
|
echo " → Skipping PID $pid on port ${port} (not steamcache2: ${proc_name:-$cmdline})"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
kill_steamcache_on_port "$PORT"
|
||||||
|
sleep 0.5
|
||||||
|
|
||||||
|
# --- Launch the server in the background ---
|
||||||
|
echo "Starting steamcache2 with validation config (small caches for disk/GC testing)..."
|
||||||
|
"$BINARY" --config "$VALIDATE_CONFIG" --log-level info &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
# Ensure we always clean up the child on exit / Ctrl-C / error
|
||||||
|
cleanup() {
|
||||||
|
echo ""
|
||||||
|
echo "Stopping steamcache2 (pid $SERVER_PID)..."
|
||||||
|
if kill "$SERVER_PID" 2>/dev/null; then
|
||||||
|
wait "$SERVER_PID" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
echo "Server stopped."
|
||||||
|
}
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Give the server a moment to bind and pass its own startup checks
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Basic readiness probe using the actual configured port
|
||||||
|
if ! curl -s --max-time 3 "${SERVER_URL}/" >/dev/null 2>&1; then
|
||||||
|
echo "WARNING: Server did not respond quickly on ${SERVER_URL}/"
|
||||||
|
echo " It may still be starting or bound to a different address."
|
||||||
|
echo " Check the server logs above. You can still try the SteamPrefill commands."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${VALIDATE_QUIET:-}" != "1" ]]; then
|
||||||
|
echo ""
|
||||||
|
echo "======================================================================"
|
||||||
|
echo "steamcache2 is running (validation mode) on ${SERVER_URL}"
|
||||||
|
echo ""
|
||||||
|
echo "In another terminal (or on a machine that can reach this one), run:"
|
||||||
|
echo ""
|
||||||
|
echo " # One-time workload creation (run on a machine with SteamPrefill + Steam):"
|
||||||
|
echo " SteamPrefill benchmark setup --preset LargeChunks"
|
||||||
|
echo " # (or --use-selected, --all, --appid ..., or your own preset)"
|
||||||
|
echo ""
|
||||||
|
echo " # Copy the generated workload file to this machine if needed."
|
||||||
|
echo ""
|
||||||
|
echo " # Then run the actual benchmark (this is the realistic client simulator):"
|
||||||
|
echo " SteamPrefill benchmark run -c 20 -i 3"
|
||||||
|
echo ""
|
||||||
|
echo "After the benchmark completes, you can inspect the cache with:"
|
||||||
|
echo " curl -s ${SERVER_URL}/metrics | cat"
|
||||||
|
echo ""
|
||||||
|
echo "Or run: make validate-check (if the Makefile target exists)"
|
||||||
|
echo ""
|
||||||
|
echo "When you are finished, press Ctrl-C in this window to stop the server cleanly."
|
||||||
|
echo "======================================================================"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for the background server (or for the user to Ctrl-C)
|
||||||
|
wait $SERVER_PID || true
|
||||||
@@ -110,6 +110,7 @@ func (sc *SteamCache) handleCacheHit(w http.ResponseWriter, r *http.Request, cac
|
|||||||
sc.metrics.IncrementCacheHits()
|
sc.metrics.IncrementCacheHits()
|
||||||
sc.metrics.AddResponseTime(time.Since(tstart))
|
sc.metrics.AddResponseTime(time.Since(tstart))
|
||||||
sc.metrics.AddBytesServed(int64(len(cachedData)))
|
sc.metrics.AddBytesServed(int64(len(cachedData)))
|
||||||
|
sc.metrics.AddBytesSaved(int64(len(cachedData)))
|
||||||
sc.metrics.IncrementServiceRequests(service.Name)
|
sc.metrics.IncrementServiceRequests(service.Name)
|
||||||
|
|
||||||
logger.Logger.Debug().
|
logger.Logger.Debug().
|
||||||
@@ -197,6 +198,7 @@ func (sc *SteamCache) waitForCoalesced(w http.ResponseWriter, r *http.Request, c
|
|||||||
sc.metrics.IncrementCacheCoalesced()
|
sc.metrics.IncrementCacheCoalesced()
|
||||||
sc.metrics.AddResponseTime(time.Since(tstart))
|
sc.metrics.AddResponseTime(time.Since(tstart))
|
||||||
sc.metrics.AddBytesServed(int64(len(responseData)))
|
sc.metrics.AddBytesServed(int64(len(responseData)))
|
||||||
|
sc.metrics.AddBytesSaved(int64(len(responseData)))
|
||||||
sc.metrics.IncrementServiceRequests(service.Name)
|
sc.metrics.IncrementServiceRequests(service.Name)
|
||||||
|
|
||||||
logger.Logger.Info().
|
logger.Logger.Info().
|
||||||
@@ -233,9 +235,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
defer sc.requestSemaphore.Release(1)
|
defer sc.requestSemaphore.Release(1)
|
||||||
|
|
||||||
// Track total requests
|
|
||||||
sc.metrics.IncrementTotalRequests()
|
|
||||||
|
|
||||||
// Apply per-client rate limiting
|
// Apply per-client rate limiting
|
||||||
clientLimiter := sc.getOrCreateClientLimiter(clientIP)
|
clientLimiter := sc.getOrCreateClientLimiter(clientIP)
|
||||||
|
|
||||||
@@ -305,6 +304,11 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
Str("client_ip", clientIP).
|
Str("client_ip", clientIP).
|
||||||
Msg("Generated cache key")
|
Msg("Generated cache key")
|
||||||
|
|
||||||
|
// Only count real cacheable service traffic toward total_requests / hit_rate.
|
||||||
|
// Special endpoints (/, /metrics, /lancache-heartbeat) and unsupported services
|
||||||
|
// are intentionally excluded so that idle monitoring doesn't dilute the hit rate.
|
||||||
|
sc.metrics.IncrementTotalRequests()
|
||||||
|
|
||||||
if sc.handleCacheHit(w, r, cachePath, cacheKey, urlPath, service, clientIP, tstart) {
|
if sc.handleCacheHit(w, r, cachePath, cacheKey, urlPath, service, clientIP, tstart) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -588,8 +592,6 @@ func (sc *SteamCache) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
sc.metrics.IncrementServiceError("cache_write")
|
sc.metrics.IncrementServiceError("cache_write")
|
||||||
_ = sc.vfs.Delete(cachePath) // best-effort removal of partial corrupt cache entry on write failure; non-fatal. Deferred cacheWriter.Close() (from earlier in block) runs after this on error unwind path (harmless per DiskFS design)
|
_ = sc.vfs.Delete(cachePath) // best-effort removal of partial corrupt cache entry on write failure; non-fatal. Deferred cacheWriter.Close() (from earlier in block) runs after this on error unwind path (harmless per DiskFS design)
|
||||||
} else {
|
} else {
|
||||||
// Track successful cache write
|
|
||||||
sc.metrics.AddBytesCached(int64(len(cacheData)))
|
|
||||||
logger.Logger.Debug().
|
logger.Logger.Debug().
|
||||||
Str("key", cacheKey).
|
Str("key", cacheKey).
|
||||||
Str("url", urlPath).
|
Str("url", urlPath).
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ type Metrics struct {
|
|||||||
// Performance metrics
|
// Performance metrics
|
||||||
TotalResponseTime int64 // in nanoseconds
|
TotalResponseTime int64 // in nanoseconds
|
||||||
TotalBytesServed int64
|
TotalBytesServed int64
|
||||||
TotalBytesCached int64
|
TotalBytesSaved int64 // bytes served from cache instead of being re-downloaded from upstream
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Cache metrics
|
// Cache metrics
|
||||||
MemoryCacheSize int64
|
MemoryCacheSize int64
|
||||||
@@ -98,9 +100,10 @@ func (m *Metrics) AddBytesServed(bytes int64) {
|
|||||||
atomic.AddInt64(&m.TotalBytesServed, bytes)
|
atomic.AddInt64(&m.TotalBytesServed, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddBytesCached adds bytes cached to the total
|
// AddBytesSaved records bytes that were served from the cache instead of being
|
||||||
func (m *Metrics) AddBytesCached(bytes int64) {
|
// fetched again from the upstream (the main value metric for a cache).
|
||||||
atomic.AddInt64(&m.TotalBytesCached, bytes)
|
func (m *Metrics) AddBytesSaved(bytes int64) {
|
||||||
|
atomic.AddInt64(&m.TotalBytesSaved, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetMemoryCacheSize sets the current memory cache size
|
// SetMemoryCacheSize sets the current memory cache size
|
||||||
@@ -192,7 +195,7 @@ func (m *Metrics) GetStats() *Stats {
|
|||||||
HitRate: hitRate,
|
HitRate: hitRate,
|
||||||
AvgResponseTime: avgResponseTime,
|
AvgResponseTime: avgResponseTime,
|
||||||
TotalBytesServed: atomic.LoadInt64(&m.TotalBytesServed),
|
TotalBytesServed: atomic.LoadInt64(&m.TotalBytesServed),
|
||||||
TotalBytesCached: atomic.LoadInt64(&m.TotalBytesCached),
|
TotalBytesSaved: atomic.LoadInt64(&m.TotalBytesSaved),
|
||||||
MemoryCacheSize: atomic.LoadInt64(&m.MemoryCacheSize),
|
MemoryCacheSize: atomic.LoadInt64(&m.MemoryCacheSize),
|
||||||
DiskCacheSize: atomic.LoadInt64(&m.DiskCacheSize),
|
DiskCacheSize: atomic.LoadInt64(&m.DiskCacheSize),
|
||||||
MemoryCacheHits: atomic.LoadInt64(&m.MemoryCacheHits),
|
MemoryCacheHits: atomic.LoadInt64(&m.MemoryCacheHits),
|
||||||
@@ -218,7 +221,7 @@ func (m *Metrics) Reset() {
|
|||||||
atomic.StoreInt64(&m.RateLimited, 0)
|
atomic.StoreInt64(&m.RateLimited, 0)
|
||||||
atomic.StoreInt64(&m.TotalResponseTime, 0)
|
atomic.StoreInt64(&m.TotalResponseTime, 0)
|
||||||
atomic.StoreInt64(&m.TotalBytesServed, 0)
|
atomic.StoreInt64(&m.TotalBytesServed, 0)
|
||||||
atomic.StoreInt64(&m.TotalBytesCached, 0)
|
atomic.StoreInt64(&m.TotalBytesSaved, 0)
|
||||||
atomic.StoreInt64(&m.MemoryCacheHits, 0)
|
atomic.StoreInt64(&m.MemoryCacheHits, 0)
|
||||||
atomic.StoreInt64(&m.DiskCacheHits, 0)
|
atomic.StoreInt64(&m.DiskCacheHits, 0)
|
||||||
atomic.StoreInt64(&m.Promotions, 0)
|
atomic.StoreInt64(&m.Promotions, 0)
|
||||||
@@ -248,8 +251,10 @@ type Stats struct {
|
|||||||
HitRate float64
|
HitRate float64
|
||||||
AvgResponseTime time.Duration
|
AvgResponseTime time.Duration
|
||||||
TotalBytesServed int64
|
TotalBytesServed int64
|
||||||
TotalBytesCached int64
|
TotalBytesSaved int64
|
||||||
MemoryCacheSize int64
|
MemoryCacheSize int64
|
||||||
|
|
||||||
|
|
||||||
DiskCacheSize int64
|
DiskCacheSize int64
|
||||||
MemoryCacheHits int64
|
MemoryCacheHits int64
|
||||||
DiskCacheHits int64
|
DiskCacheHits int64
|
||||||
@@ -291,7 +296,8 @@ func WriteText(w http.ResponseWriter, stats *Stats) {
|
|||||||
_, _ = fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate)
|
_, _ = fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate)
|
||||||
_, _ = fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6)
|
_, _ = fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6)
|
||||||
_, _ = fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed)
|
_, _ = fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed)
|
||||||
_, _ = fmt.Fprintf(w, "total_bytes_cached %d\n", stats.TotalBytesCached)
|
_, _ = fmt.Fprintf(w, "total_bytes_saved %d\n", stats.TotalBytesSaved)
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize)
|
_, _ = fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize)
|
||||||
_, _ = fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize)
|
_, _ = fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize)
|
||||||
_, _ = fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds())
|
_, _ = fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds())
|
||||||
|
|||||||
@@ -179,6 +179,17 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
|
|||||||
metrics: metrics.NewMetrics(),
|
metrics: metrics.NewMetrics(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wire metrics into TieredCache so promotions are counted.
|
||||||
|
c.SetMetrics(sc.metrics)
|
||||||
|
|
||||||
|
// Wire metrics into the concrete storage tiers (MemoryFS / DiskFS) so per-tier hits and evictions are counted.
|
||||||
|
if m != nil {
|
||||||
|
m.SetMetrics(sc.metrics)
|
||||||
|
}
|
||||||
|
if d != nil {
|
||||||
|
d.SetMetrics(sc.metrics)
|
||||||
|
}
|
||||||
|
|
||||||
// Wire the request processor (constructor injection of interfaces for the owned wrappers + vfs + client + scalars).
|
// Wire the request processor (constructor injection of interfaces for the owned wrappers + vfs + client + scalars).
|
||||||
// Done after all fields including wrappers are set; before attach goroutines (no impact on lifecycle paths).
|
// Done after all fields including wrappers are set; before attach goroutines (no impact on lifecycle paths).
|
||||||
sc.processor = newRequestProcessor(sc)
|
sc.processor = newRequestProcessor(sc)
|
||||||
@@ -209,12 +220,16 @@ func New(address string, memorySize string, diskSize string, diskPath, upstream,
|
|||||||
sc.wg.Add(1)
|
sc.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer sc.wg.Done()
|
defer sc.wg.Done()
|
||||||
|
t0 := time.Now()
|
||||||
_ = d.Size()
|
_ = d.Size()
|
||||||
select {
|
select {
|
||||||
case <-sc.shutdownCh:
|
case <-sc.shutdownCh:
|
||||||
return
|
return
|
||||||
default:
|
default:
|
||||||
c.SetSlow(dgc)
|
c.SetSlow(dgc)
|
||||||
|
logger.Logger.Info().
|
||||||
|
Dur("attach_delay", time.Since(t0)).
|
||||||
|
Msg("Disk slow tier attached (mixed mode); prior traffic was memory-only")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+11
@@ -3,6 +3,7 @@ package cache
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"io"
|
||||||
|
"s1d3sw1ped/steamcache2/steamcache/metrics"
|
||||||
"s1d3sw1ped/steamcache2/vfs"
|
"s1d3sw1ped/steamcache2/vfs"
|
||||||
"s1d3sw1ped/steamcache2/vfs/vfserror"
|
"s1d3sw1ped/steamcache2/vfs/vfserror"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -12,6 +13,7 @@ import (
|
|||||||
type TieredCache struct {
|
type TieredCache struct {
|
||||||
fast *atomic.Value // Memory cache (fast) - atomic.Value for lock-free access
|
fast *atomic.Value // Memory cache (fast) - atomic.Value for lock-free access
|
||||||
slow *atomic.Value // Disk cache (slow) - atomic.Value for lock-free access
|
slow *atomic.Value // Disk cache (slow) - atomic.Value for lock-free access
|
||||||
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new tiered cache
|
// New creates a new tiered cache
|
||||||
@@ -22,6 +24,11 @@ func New() *TieredCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMetrics allows wiring the top-level metrics collector (called from SteamCache).
|
||||||
|
func (tc *TieredCache) SetMetrics(m *metrics.Metrics) {
|
||||||
|
tc.metrics = m
|
||||||
|
}
|
||||||
|
|
||||||
// SetFast sets the fast (memory) tier atomically
|
// SetFast sets the fast (memory) tier atomically
|
||||||
func (tc *TieredCache) SetFast(vfs vfs.VFS) {
|
func (tc *TieredCache) SetFast(vfs vfs.VFS) {
|
||||||
tc.fast.Store(vfs)
|
tc.fast.Store(vfs)
|
||||||
@@ -221,6 +228,10 @@ func (tc *TieredCache) promoteToFast(key string, reader io.ReadCloser) {
|
|||||||
// Failure (e.g. mem pressure, concurrent evict) is non-fatal and does not affect correctness of slow tier.
|
// Failure (e.g. mem pressure, concurrent evict) is non-fatal and does not affect correctness of slow tier.
|
||||||
_, _ = writer.Write(content)
|
_, _ = writer.Write(content)
|
||||||
_ = writer.Close()
|
_ = writer.Close()
|
||||||
|
|
||||||
|
if tc.metrics != nil {
|
||||||
|
tc.metrics.IncrementPromotions()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-2
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"s1d3sw1ped/steamcache2/steamcache/logger"
|
"s1d3sw1ped/steamcache2/steamcache/logger"
|
||||||
|
"s1d3sw1ped/steamcache2/steamcache/metrics"
|
||||||
"s1d3sw1ped/steamcache2/vfs"
|
"s1d3sw1ped/steamcache2/vfs"
|
||||||
"s1d3sw1ped/steamcache2/vfs/locks"
|
"s1d3sw1ped/steamcache2/vfs/locks"
|
||||||
"s1d3sw1ped/steamcache2/vfs/lru"
|
"s1d3sw1ped/steamcache2/vfs/lru"
|
||||||
@@ -43,6 +44,7 @@ type DiskFS struct {
|
|||||||
// initCloseOnce ensures initDone closed exactly once even on panic in bg populator (panic safety for Issue 1).
|
// initCloseOnce ensures initDone closed exactly once even on panic in bg populator (panic safety for Issue 1).
|
||||||
initCloseOnce sync.Once
|
initCloseOnce sync.Once
|
||||||
startupEvict func(vfs.VFS, uint) uint // passed to New (via gc.GetGCAlgorithm); invoked as last step of bg init if over cap (no post-ctor race)
|
startupEvict func(vfs.VFS, uint) uint // passed to New (via gc.GetGCAlgorithm); invoked as last step of bg init if over cap (no post-ctor race)
|
||||||
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// shardPath converts a Steam cache key to a sharded directory path to reduce inode pressure
|
// shardPath converts a Steam cache key to a sharded directory path to reduce inode pressure
|
||||||
@@ -134,6 +136,12 @@ func New(root string, capacity int64, evict func(vfs.VFS, uint) uint) (*DiskFS,
|
|||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMetrics allows the owner (SteamCache) to inject the metrics collector
|
||||||
|
// so that per-tier hit and eviction counters can be recorded.
|
||||||
|
func (d *DiskFS) SetMetrics(met *metrics.Metrics) {
|
||||||
|
d.metrics = met
|
||||||
|
}
|
||||||
|
|
||||||
// calculateSizeAndPopulateIndex runs in background from New to avoid blocking startup or O(N) RAM for large caches (millions of Steam files).
|
// calculateSizeAndPopulateIndex runs in background from New to avoid blocking startup or O(N) RAM for large caches (millions of Steam files).
|
||||||
// It streams batch inserts (bounded by maxEvictBatch) to keep lock times short and eliminate giant temporary slice.
|
// It streams batch inserts (bounded by maxEvictBatch) to keep lock times short and eliminate giant temporary slice.
|
||||||
// Startup over-capacity eviction (if needed) runs as the very last step (using the evict func passed to New, selected via gc.GetGCAlgorithm).
|
// Startup over-capacity eviction (if needed) runs as the very last step (using the evict func passed to New, selected via gc.GetGCAlgorithm).
|
||||||
@@ -489,8 +497,14 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use memory mapping for large files (>1MB) to improve performance
|
// Use memory mapping for large files to improve performance.
|
||||||
const mmapThreshold = 1024 * 1024 // 1MB
|
// We use 8 MiB as the threshold because:
|
||||||
|
// - Most Steam chunks are ~1 MiB (see current disk cache analysis).
|
||||||
|
// - mmap has non-trivial fixed overhead (page tables, TLB, faults).
|
||||||
|
// - For files < ~4-8 MiB the overhead often outweighs the zero-copy benefit
|
||||||
|
// on mostly sequential access patterns.
|
||||||
|
// - Larger files benefit more from kernel readahead + zero-copy.
|
||||||
|
const mmapThreshold = 8 * 1024 * 1024 // 8 MiB
|
||||||
if fi.Size > mmapThreshold {
|
if fi.Size > mmapThreshold {
|
||||||
// Close the regular file handle
|
// Close the regular file handle
|
||||||
_ = file.Close() // best-effort; mmap path takes over or falls back
|
_ = file.Close() // best-effort; mmap path takes over or falls back
|
||||||
@@ -505,9 +519,21 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
_ = mmapFile.Close() // best-effort close before fallback open
|
_ = mmapFile.Close() // best-effort close before fallback open
|
||||||
// Fallback to regular file reading (intentional 3rd open of same path after mmap failure; pre-existing pattern, no leak)
|
// Fallback to regular file reading (intentional 3rd open of same path after mmap failure; pre-existing pattern, no leak)
|
||||||
|
if d.metrics != nil {
|
||||||
|
d.metrics.IncrementDiskCacheHits()
|
||||||
|
}
|
||||||
return os.Open(path)
|
return os.Open(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hint to the kernel (on supported platforms) that we will access
|
||||||
|
// this mapping sequentially. This enables better readahead.
|
||||||
|
if err := madviseSequential(mapped); err != nil {
|
||||||
|
logger.Logger.Debug().
|
||||||
|
Err(err).
|
||||||
|
Str("key", key).
|
||||||
|
Msg("madvise(MADV_SEQUENTIAL) failed on mmap'd chunk")
|
||||||
|
}
|
||||||
|
|
||||||
return &mmapReadCloser{
|
return &mmapReadCloser{
|
||||||
data: mapped,
|
data: mapped,
|
||||||
file: mmapFile,
|
file: mmapFile,
|
||||||
@@ -515,6 +541,9 @@ func (d *DiskFS) Open(key string) (io.ReadCloser, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.metrics != nil {
|
||||||
|
d.metrics.IncrementDiskCacheHits()
|
||||||
|
}
|
||||||
return file, nil
|
return file, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -677,6 +706,10 @@ func (d *DiskFS) EvictLRU(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.metrics != nil && evicted > 0 {
|
||||||
|
d.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,6 +759,10 @@ func (d *DiskFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.metrics != nil && evicted > 0 {
|
||||||
|
d.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -773,6 +810,10 @@ func (d *DiskFS) EvictFIFO(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.metrics != nil && evicted > 0 {
|
||||||
|
d.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,6 +866,10 @@ func (d *DiskFS) EvictLFU(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.metrics != nil && evicted > 0 {
|
||||||
|
d.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -878,5 +923,9 @@ func (d *DiskFS) EvictHybrid(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.mu.Unlock()
|
d.mu.Unlock()
|
||||||
|
|
||||||
|
if d.metrics != nil && evicted > 0 {
|
||||||
|
d.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package disk
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// madviseSequential gives the OS a hint that the memory region will be
|
||||||
|
// accessed sequentially. This is a no-op or best-effort on some platforms.
|
||||||
|
func madviseSequential(b []byte) error {
|
||||||
|
return unix.Madvise(b, unix.MADV_SEQUENTIAL)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package disk
|
||||||
|
|
||||||
|
// madviseSequential is a no-op on Windows.
|
||||||
|
// Windows file mappings don't have a direct equivalent to MADV_SEQUENTIAL
|
||||||
|
// in the same way. Sequential access hints are better done via
|
||||||
|
// FILE_FLAG_SEQUENTIAL_SCAN at file open time (future improvement possible).
|
||||||
|
func madviseSequential(b []byte) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"s1d3sw1ped/steamcache2/steamcache/metrics"
|
||||||
"s1d3sw1ped/steamcache2/vfs"
|
"s1d3sw1ped/steamcache2/vfs"
|
||||||
"s1d3sw1ped/steamcache2/vfs/locks"
|
"s1d3sw1ped/steamcache2/vfs/locks"
|
||||||
"s1d3sw1ped/steamcache2/vfs/lru"
|
"s1d3sw1ped/steamcache2/vfs/lru"
|
||||||
@@ -33,6 +34,7 @@ type MemoryFS struct {
|
|||||||
keyLocks []sync.Map // Sharded lock pools for better concurrency
|
keyLocks []sync.Map // Sharded lock pools for better concurrency
|
||||||
LRU *lru.LRUList[*types.FileInfo]
|
LRU *lru.LRUList[*types.FileInfo]
|
||||||
timeUpdater *types.BatchedTimeUpdate // Batched time updates for better performance
|
timeUpdater *types.BatchedTimeUpdate // Batched time updates for better performance
|
||||||
|
metrics *metrics.Metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new MemoryFS
|
// New creates a new MemoryFS
|
||||||
@@ -55,6 +57,12 @@ func New(capacity int64) (*MemoryFS, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetMetrics allows the owner (SteamCache) to inject the metrics collector
|
||||||
|
// so that per-tier hit and eviction counters can be recorded.
|
||||||
|
func (m *MemoryFS) SetMetrics(met *metrics.Metrics) {
|
||||||
|
m.metrics = met
|
||||||
|
}
|
||||||
|
|
||||||
// Name returns the name of this VFS
|
// Name returns the name of this VFS
|
||||||
func (m *MemoryFS) Name() string {
|
func (m *MemoryFS) Name() string {
|
||||||
return "MemoryFS"
|
return "MemoryFS"
|
||||||
@@ -209,6 +217,10 @@ func (m *MemoryFS) Open(key string) (io.ReadCloser, error) {
|
|||||||
// Use zero-copy approach - return reader that reads directly from buffer
|
// Use zero-copy approach - return reader that reads directly from buffer
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil {
|
||||||
|
m.metrics.IncrementMemoryCacheHits()
|
||||||
|
}
|
||||||
|
|
||||||
return &memoryReadCloser{
|
return &memoryReadCloser{
|
||||||
buffer: buffer,
|
buffer: buffer,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
@@ -344,6 +356,10 @@ func (m *MemoryFS) EvictLRU(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil && evicted > 0 {
|
||||||
|
m.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,6 +411,10 @@ func (m *MemoryFS) EvictBySize(bytesNeeded uint, ascending bool) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil && evicted > 0 {
|
||||||
|
m.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,6 +462,10 @@ func (m *MemoryFS) EvictFIFO(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil && evicted > 0 {
|
||||||
|
m.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +518,10 @@ func (m *MemoryFS) EvictLFU(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil && evicted > 0 {
|
||||||
|
m.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,5 +576,9 @@ func (m *MemoryFS) EvictHybrid(bytesNeeded uint) uint {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.mu.Unlock()
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
if m.metrics != nil && evicted > 0 {
|
||||||
|
m.metrics.IncrementEvictions()
|
||||||
|
}
|
||||||
return evicted
|
return evicted
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user