Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ jobs:
python -m pip check
- name: Run backend tests
run: python -m unittest discover -s tests -v
- name: Run CI-safe real-load benchmark
run: python benchmarks/long_soak_runner.py --profile light_desktop --duration-sec 5 --events-per-sec 100 --flows 10 --websocket-clients 1 --sample-interval-sec 0.25 --ci-safe --output .runtime/benchmarks/ci-safe-real-load

frontend-build:
name: Frontend Build (${{ matrix.os }})
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ or PCAP artifacts.
| Live Ring Buffer | Foundation step | Recent packet, flow, alert, and Expert summaries are held in per-category bounded memory with eviction/query metrics and Ops Snapshot visibility. | Stored and returned records are redacted summaries; raw packet payloads and credentials are excluded. |
| Batch Persistence / Storage Backpressure | Foundation step | Redacted packet, alert, and flow records use bounded batches with queue health, write latency, retry, backlog, failure, and Ops Snapshot visibility. | Audit and report exports remain synchronous; this is not a distributed storage engine or the complete performance pipeline. |
| WebSocket Event Aggregator | Foundation step | Realtime packet/alert batching, slow-client protection, WebSocket pressure metrics, and Ops Snapshot visibility are tested. | Realtime delivery batching only. |
| Performance Benchmark Suite | Validated foundation | Synthetic stage benchmarks, a bounded full-pipeline soak mode, JSON/Markdown reports, and a CI-safe structural smoke test cover the current pipeline. | No live capture, external network, admin privileges, or production throughput claims; see [Performance Validation](docs/PERFORMANCE_VALIDATION.md). |
| Performance Benchmark Suite | Validated foundation | Synthetic stage benchmarks, bounded full-pipeline soak mode, named load profiles, resource sampling, JSON/Markdown/CSV reports, and a CI-safe structural smoke test cover the current pipeline. | No live capture, external network, admin privileges, or production throughput claims; see [Performance Validation](docs/PERFORMANCE_VALIDATION.md) and [Real Load Testing](docs/REAL_LOAD_TESTING.md). |
| Demo and operational QA | Ready | Token-safe demo and Agent script behavior is tested. | Demo launchers and status commands do not print raw tokens. |
| Windows release packaging | Validated path | Desktop smoke, version consistency, and release workflow checks run in CI. | Versioned artifacts include SHA256 checksums. |
| Linux desktop packaging | Staged | Build workflow exists; native production validation remains pending. | Publish only after native smoke and release QA. |
Expand Down Expand Up @@ -257,6 +257,7 @@ and surfaced in Ops Snapshot metrics.
- [Agent Mode](docs/AGENT_MODE.md)
- [Performance Pipeline](docs/PERFORMANCE_PIPELINE.md)
- [Performance Validation](docs/PERFORMANCE_VALIDATION.md)
- [Real Load And Long Soak Testing](docs/REAL_LOAD_TESTING.md)
- [Flow Analysis And Protocol Intelligence](docs/FLOW_ANALYSIS.md)
- [Service Attribution And Destination Intelligence](docs/SERVICE_ATTRIBUTION.md)
- [Incident Correlation](docs/INCIDENT_CORRELATION.md)
Expand Down Expand Up @@ -529,6 +530,7 @@ GitHub Actions builds CI on push and pull request. Desktop release artifacts are
- [Install and run guide](docs/INSTALL.md)
- [Release checklist](docs/RELEASE_CHECKLIST.md)
- [Release QA checklist](docs/RELEASE_QA_CHECKLIST.md)
- [Real load and long soak testing](docs/REAL_LOAD_TESTING.md)
- [Desktop shell](docs/DESKTOP_SHELL.md)
- [Web migration notes](docs/WEB_MIGRATION.md)
- [Contributing](CONTRIBUTING.md)
19 changes: 19 additions & 0 deletions benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,22 @@ Results are machine-dependent. They validate bounded behavior, observable
drops, ordering, report generation, and resource trends; they are not a
production capacity guarantee. Use authorized workload traces separately when
sizing a deployment.

## Real load and long soak profiles

Step 11 adds named load profiles and resource time-series reports:

```powershell
python benchmarks/long_soak_runner.py `
--profile light_desktop `
--duration-sec 5 `
--events-per-sec 100 `
--flows 10 `
--websocket-clients 1 `
--ci-safe `
--output .runtime/benchmarks/ci-safe-real-load
```

Manual profiles include `normal_desktop`, `heavy_desktop`, `server_medium`,
and `stress_short`. They remain local and synthetic by default and do not
capture live traffic. See `docs/REAL_LOAD_TESTING.md`.
109 changes: 109 additions & 0 deletions benchmarks/load_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

from dataclasses import dataclass, replace


@dataclass(frozen=True)
class LoadProfile:
name: str
duration_sec: int
events_per_sec: int
flows: int
websocket_clients: int
expected_target: str

def with_overrides(
self,
*,
duration_sec: float | None = None,
events_per_sec: int | None = None,
flows: int | None = None,
websocket_clients: int | None = None,
ci_safe: bool = False,
) -> "LoadProfile":
profile = replace(
self,
duration_sec=(
int(duration_sec)
if duration_sec is not None
else int(self.duration_sec)
),
events_per_sec=(
int(events_per_sec)
if events_per_sec is not None
else int(self.events_per_sec)
),
flows=int(flows) if flows is not None else int(self.flows),
websocket_clients=(
int(websocket_clients)
if websocket_clients is not None
else int(self.websocket_clients)
),
)
if not ci_safe:
return profile
return replace(
profile,
duration_sec=max(1, min(profile.duration_sec, 10)),
events_per_sec=max(1, min(profile.events_per_sec, 200)),
flows=max(1, min(profile.flows, 25)),
websocket_clients=max(1, min(profile.websocket_clients, 2)),
)


LOAD_PROFILES: dict[str, LoadProfile] = {
"light_desktop": LoadProfile(
name="light_desktop",
duration_sec=5 * 60,
events_per_sec=100,
flows=20,
websocket_clients=1,
expected_target="very low pressure",
),
"normal_desktop": LoadProfile(
name="normal_desktop",
duration_sec=15 * 60,
events_per_sec=250,
flows=75,
websocket_clients=2,
expected_target="stable CPU/RAM with no unbounded growth",
),
"heavy_desktop": LoadProfile(
name="heavy_desktop",
duration_sec=30 * 60,
events_per_sec=500,
flows=150,
websocket_clients=3,
expected_target="bounded pressure allowed, no crash or unbounded memory",
),
"server_medium": LoadProfile(
name="server_medium",
duration_sec=60 * 60,
events_per_sec=1000,
flows=300,
websocket_clients=5,
expected_target="stable under server-like pressure",
),
"stress_short": LoadProfile(
name="stress_short",
duration_sec=5 * 60,
events_per_sec=2500,
flows=600,
websocket_clients=6,
expected_target="visible bounded pressure; explained drops are acceptable",
),
}


def profile_names() -> tuple[str, ...]:
return tuple(LOAD_PROFILES)


def get_profile(name: str) -> LoadProfile:
try:
return LOAD_PROFILES[name]
except KeyError as exc:
raise ValueError(f"Unknown load profile: {name}") from exc


__all__ = ["LOAD_PROFILES", "LoadProfile", "get_profile", "profile_names"]
Loading