Skip to content

Commit aa25f73

Browse files
committed
Soak test gaps: 200-file fixture, Windows, snapshots, ASan build
- Test project: 377 files (80 Python + 40 Go + 40 TSX + configs) instead of 5 files. RSS baseline now ~47MB (real workload). - Snapshots every 10s (was 30s) — 7+ data points in 1-min run. - Reindex every 2min compressed (was 5min) — more cycles per run. - heap_committed fallback: use RSS when mimalloc reports 0. - Windows soak: added to quick soak matrix (MSYS2 + python3 + git). - ASan soak: builds with -fsanitize=address (was building release). - Collect snapshot with single python3 call (was 6 separate calls).
1 parent 3a457d9 commit aa25f73

File tree

3 files changed

+183
-56
lines changed

3 files changed

+183
-56
lines changed

.github/workflows/dry-run.yml

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -534,18 +534,40 @@ jobs:
534534
goarch: arm64
535535
cc: cc
536536
cxx: c++
537+
shell: bash
538+
- os: windows-latest
539+
goos: windows
540+
goarch: amd64
541+
cc: clang
542+
cxx: clang++
543+
shell: msys2 {0}
537544
steps:
538545
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
539546

547+
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
548+
if: matrix.goos == 'windows'
549+
with:
550+
msystem: CLANG64
551+
path-type: inherit
552+
install: >-
553+
mingw-w64-clang-x86_64-python3
554+
git
555+
coreutils
556+
540557
- name: Install deps (Linux)
541558
if: startsWith(matrix.os, 'ubuntu')
542559
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
543560

544561
- name: Build (release mode)
562+
shell: ${{ matrix.shell || 'bash' }}
545563
run: scripts/build.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
546564

547565
- name: Quick soak (10 min)
548-
run: scripts/soak-test.sh build/c/codebase-memory-mcp 10
566+
shell: ${{ matrix.shell || 'bash' }}
567+
run: |
568+
BIN=build/c/codebase-memory-mcp
569+
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
570+
scripts/soak-test.sh "$BIN" 10
549571
550572
- name: Upload soak metrics
551573
if: always()
@@ -576,14 +598,13 @@ jobs:
576598

577599
- name: Build (ASan + LeakSanitizer)
578600
run: |
579-
scripts/test.sh CC=gcc CXX=g++
580-
# test.sh builds with ASan; the test-runner binary has ASan linked
581-
cp build/c/test-runner build/c/codebase-memory-mcp-asan || true
582-
scripts/build.sh CC=gcc CXX=g++
601+
# Build production binary WITH ASan for leak detection at exit
602+
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer" \
603+
scripts/build.sh CC=gcc CXX=g++ EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
583604
584605
- name: ASan soak (15 min)
585606
env:
586-
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0"
607+
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:log_path=soak-results/asan"
587608
run: scripts/soak-test.sh build/c/codebase-memory-mcp 15
588609

589610
- name: Upload ASan soak metrics

.github/workflows/release.yml

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,15 +535,36 @@ jobs:
535535
goarch: arm64
536536
cc: cc
537537
cxx: c++
538+
shell: bash
539+
- os: windows-latest
540+
goos: windows
541+
goarch: amd64
542+
cc: clang
543+
cxx: clang++
544+
shell: msys2 {0}
538545
steps:
539546
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
547+
- uses: msys2/setup-msys2@4f806de0a5a7294ffabaff804b38a9b435a73bda # v2
548+
if: matrix.goos == 'windows'
549+
with:
550+
msystem: CLANG64
551+
path-type: inherit
552+
install: >-
553+
mingw-w64-clang-x86_64-python3
554+
git
555+
coreutils
540556
- name: Install deps (Linux)
541557
if: startsWith(matrix.os, 'ubuntu')
542558
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
543559
- name: Build (release mode)
560+
shell: ${{ matrix.shell || 'bash' }}
544561
run: scripts/build.sh --version ${{ inputs.version }} CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
545562
- name: Quick soak (10 min)
546-
run: scripts/soak-test.sh build/c/codebase-memory-mcp 10
563+
shell: ${{ matrix.shell || 'bash' }}
564+
run: |
565+
BIN=build/c/codebase-memory-mcp
566+
[ -f "${BIN}.exe" ] && BIN="${BIN}.exe"
567+
scripts/soak-test.sh "$BIN" 10
547568
- name: Upload soak metrics
548569
if: always()
549570
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
@@ -570,11 +591,13 @@ jobs:
570591
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
571592
- name: Install deps
572593
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev python3 git
573-
- name: Build (release mode for soak)
574-
run: scripts/build.sh CC=gcc CXX=g++
594+
- name: Build (ASan + LeakSanitizer)
595+
run: |
596+
SANITIZE="-fsanitize=address,undefined -fno-omit-frame-pointer"
597+
scripts/build.sh CC=gcc CXX=g++ EXTRA_CFLAGS="$SANITIZE" EXTRA_LDFLAGS="$SANITIZE"
575598
- name: ASan soak (15 min)
576599
env:
577-
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0"
600+
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=0:log_path=soak-results/asan"
578601
run: scripts/soak-test.sh build/c/codebase-memory-mcp 15
579602
- name: Upload ASan soak metrics
580603
if: always()

scripts/soak-test.sh

Lines changed: 129 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -35,67 +35,150 @@ DURATION_S=$((DURATION_MIN * 60))
3535

3636
echo "=== soak-test: binary=$BINARY duration=${DURATION_MIN}m ==="
3737

38-
# ── Helper: create test project ──────────────────────────────────
38+
# ── Helper: generate realistic test project (~200 files) ─────────
3939

4040
SOAK_PROJECT=$(mktemp -d)
41-
mkdir -p "$SOAK_PROJECT/src" "$SOAK_PROJECT/lib"
4241

43-
# Create a realistic small project with multiple languages
44-
cat > "$SOAK_PROJECT/src/main.py" << 'PYEOF'
45-
from lib.utils import compute, validate
46-
47-
def process(data):
48-
if validate(data):
49-
return compute(data)
50-
return None
51-
52-
def main():
53-
result = process({"key": "value"})
54-
print(result)
42+
generate_project() {
43+
local root="$1"
44+
# Python package (80 files)
45+
for i in $(seq 1 20); do
46+
local pkg="$root/src/pkg_${i}"
47+
mkdir -p "$pkg"
48+
cat > "$pkg/__init__.py" << PYEOF
49+
from .handlers import handle_${i}
50+
from .models import Model${i}
51+
PYEOF
52+
cat > "$pkg/handlers.py" << PYEOF
53+
from .models import Model${i}
54+
from .utils import validate_${i}, transform_${i}
55+
56+
def handle_${i}(request):
57+
data = Model${i}.from_request(request)
58+
if not validate_${i}(data):
59+
return {"error": "invalid"}
60+
return transform_${i}(data)
61+
62+
def process_batch_${i}(items):
63+
return [handle_${i}(item) for item in items]
64+
PYEOF
65+
cat > "$pkg/models.py" << PYEOF
66+
class Model${i}:
67+
def __init__(self, name, value):
68+
self.name = name
69+
self.value = value
70+
71+
@classmethod
72+
def from_request(cls, req):
73+
return cls(req.get("name", ""), req.get("value", 0))
74+
75+
def to_dict(self):
76+
return {"name": self.name, "value": self.value}
77+
PYEOF
78+
cat > "$pkg/utils.py" << PYEOF
79+
def validate_${i}(data):
80+
return data is not None and hasattr(data, 'name')
5581
56-
if __name__ == "__main__":
57-
main()
82+
def transform_${i}(data):
83+
return {"result": data.name.upper(), "score": data.value * ${i}}
5884
PYEOF
85+
done
5986

60-
cat > "$SOAK_PROJECT/lib/utils.py" << 'PYEOF'
61-
def compute(data):
62-
total = sum(len(str(v)) for v in data.values())
63-
return total
87+
# Go package (40 files)
88+
mkdir -p "$root/internal/api" "$root/internal/store" "$root/cmd"
89+
for i in $(seq 1 20); do
90+
cat > "$root/internal/api/handler_${i}.go" << GOEOF
91+
package api
6492
65-
def validate(data):
66-
return isinstance(data, dict) and len(data) > 0
93+
import "fmt"
6794
68-
def helper():
69-
return 42
70-
PYEOF
95+
func HandleRoute${i}(path string) (string, error) {
96+
result := ProcessData${i}(path)
97+
return fmt.Sprintf("route_%d: %s", ${i}, result), nil
98+
}
7199
72-
cat > "$SOAK_PROJECT/src/server.go" << 'GOEOF'
73-
package main
100+
func ProcessData${i}(input string) string {
101+
return fmt.Sprintf("processed_%d_%s", ${i}, input)
102+
}
103+
GOEOF
104+
cat > "$root/internal/store/repo_${i}.go" << GOEOF
105+
package store
74106
75-
import "fmt"
107+
type Entity${i} struct {
108+
ID int
109+
Name string
110+
Data map[string]interface{}
111+
}
76112
77-
func StartServer(port int) error {
78-
fmt.Printf("listening on %d\n", port)
79-
return nil
113+
func FindEntity${i}(id int) (*Entity${i}, error) {
114+
return &Entity${i}{ID: id, Name: "entity"}, nil
80115
}
81116
82-
func HandleRequest(path string) string {
83-
return "ok: " + path
117+
func SaveEntity${i}(e *Entity${i}) error {
118+
return nil
84119
}
85120
GOEOF
121+
done
122+
123+
# TypeScript (40 files)
124+
mkdir -p "$root/frontend/src/components" "$root/frontend/src/hooks"
125+
for i in $(seq 1 20); do
126+
cat > "$root/frontend/src/components/Component${i}.tsx" << TSEOF
127+
import React from 'react';
128+
import { useData${i} } from '../hooks/useData${i}';
129+
130+
interface Props${i} { id: number; label: string; }
131+
132+
export const Component${i}: React.FC<Props${i}> = ({ id, label }) => {
133+
const { data, loading } = useData${i}(id);
134+
if (loading) return <div>Loading...</div>;
135+
return <div className="comp-${i}">{label}: {JSON.stringify(data)}</div>;
136+
};
137+
TSEOF
138+
cat > "$root/frontend/src/hooks/useData${i}.ts" << TSEOF
139+
import { useState, useEffect } from 'react';
140+
141+
export function useData${i}(id: number) {
142+
const [data, setData] = useState(null);
143+
const [loading, setLoading] = useState(true);
144+
useEffect(() => {
145+
fetch('/api/data/${i}/' + id)
146+
.then(r => r.json())
147+
.then(d => { setData(d); setLoading(false); });
148+
}, [id]);
149+
return { data, loading };
150+
}
151+
TSEOF
152+
done
86153

87-
cat > "$SOAK_PROJECT/config.yaml" << 'YAMLEOF'
154+
# Config files
155+
cat > "$root/config.yaml" << 'YAMLEOF'
88156
database:
89157
host: localhost
90158
port: 5432
159+
pool_size: 10
91160
server:
92161
workers: 4
162+
timeout: 30
93163
YAMLEOF
164+
cat > "$root/Dockerfile" << 'DEOF'
165+
FROM python:3.11-slim
166+
WORKDIR /app
167+
COPY . .
168+
RUN pip install -r requirements.txt
169+
CMD ["python", "-m", "src.main"]
170+
DEOF
171+
}
172+
173+
echo "Generating test project (~200 files)..."
174+
generate_project "$SOAK_PROJECT"
94175

95176
# Init git repo (required for watcher)
96177
git -C "$SOAK_PROJECT" init -q 2>/dev/null
97178
git -C "$SOAK_PROJECT" add -A 2>/dev/null
98179
git -C "$SOAK_PROJECT" -c user.email=test@test -c user.name=test commit -q -m "init" 2>/dev/null
180+
FILE_COUNT=$(find "$SOAK_PROJECT" -type f | wc -l | tr -d ' ')
181+
echo "OK: $FILE_COUNT files in test project"
99182

100183
# ── Helper: run CLI tool call and record latency ─────────────────
101184

@@ -137,14 +220,14 @@ mcp_call() {
137220
collect_snapshot() {
138221
local diag_file="/tmp/cbm-diagnostics-${SERVER_PID}.json"
139222
if [ -f "$diag_file" ]; then
140-
local ts=$(date +%s)
141-
local uptime=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('uptime_s',0))" 2>/dev/null || echo "0")
142-
local rss=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('rss_bytes',0))" 2>/dev/null || echo "0")
143-
local commit=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('heap_committed_bytes',0))" 2>/dev/null || echo "0")
144-
local fds=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('fd_count',0))" 2>/dev/null || echo "0")
145-
local qcount=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('query_count',0))" 2>/dev/null || echo "0")
146-
local qmax=$(python3 -c "import json; d=json.load(open('$diag_file')); print(d.get('query_max_us',0))" 2>/dev/null || echo "0")
147-
echo "$ts,$uptime,$rss,$commit,$fds,$qcount,$qmax" >> "$METRICS_CSV"
223+
python3 -c "
224+
import json, time
225+
d = json.load(open('$diag_file'))
226+
# Use heap_committed if available, otherwise RSS (mimalloc may report 0 for committed)
227+
mem = d.get('heap_committed_bytes', 0)
228+
if mem == 0: mem = d.get('rss_bytes', 0)
229+
print(f\"{int(time.time())},{d.get('uptime_s',0)},{d.get('rss_bytes',0)},{mem},{d.get('fd_count',0)},{d.get('query_count',0)},{d.get('query_max_us',0)}\")
230+
" 2>/dev/null >> "$METRICS_CSV"
148231
fi
149232
}
150233

@@ -215,14 +298,14 @@ while [ "$(date +%s)" -lt "$END_TIME" ]; do
215298
LAST_MUTATE=$NOW
216299
fi
217300

218-
# Full reindex every 5 minutes
219-
if [ $((NOW - LAST_REINDEX)) -ge 300 ]; then
301+
# Full reindex every 2 minutes (compressed — simulates 15min real interval)
302+
if [ $((NOW - LAST_REINDEX)) -ge 120 ]; then
220303
mcp_call index_repository "{\"repo_path\":\"$SOAK_PROJECT\"}"
221304
LAST_REINDEX=$NOW
222305
fi
223306

224-
# Collect diagnostics every 30 seconds
225-
if [ $((CYCLE % 15)) -eq 0 ]; then
307+
# Collect diagnostics every 10 seconds (5 cycles)
308+
if [ $((CYCLE % 5)) -eq 0 ]; then
226309
collect_snapshot
227310
fi
228311

0 commit comments

Comments
 (0)