mirror of
https://github.com/hanzoai/agent.git
synced 2026-09-11 21:08:51 +00:00
Every workflow in this repo was deleted and nothing replaced them, so the sole producer of ghcr.io/hanzoai/agent no longer existed anywhere and there was no CI at all. That is silent staleness: no failing run, just an artifact that quietly stops being rebuilt. .github/workflows now holds sync.yml and nothing else, copied verbatim from universe infra/ci/sync.yml. It replaces a mirror-nudge file that POSTed /api/v1/repos/.../mirror-sync and described git.hanzo.ai as a downstream pull mirror; the forge is canonical and refs are pushed to it. Without it a .hanzo/workflows file has nothing to trigger it. deploy.yml restores docker-publish.yml. Two things could not come across as-is: - Its body was one `uses: hanzoai/.github/.github/workflows/docker-build.yml@main`. A private cross-repo reusable workflow does not resolve on the forge, so the amd64 build/push is vendored inline, as hanzoai/bot already does. - It built `context: control-plane`, and no control-plane directory exists on main; the orchestrator was hoisted to the root module in v0.1.1 and the directory survives only on the unmerged consolidate/held-work branch. Restoring that context restores a build that cannot run. It builds Dockerfile.sdk — the one Dockerfile on main whose context exists, previously referenced by nothing despite sdk-* tags being live on the image — and tags it sdk-* so the two Dockerfiles cannot collide in one tag namespace. The tag rule is shell rather than docker/metadata-action, so it is two readable lines with no action semantics to guess at, and it was tested standalone: v* -> :sdk-X.Y.Z + :sdk-latest, main -> :sdk-sha-<sha7> + :sdk-latest. ci.yml replaces tests.yml and test.yml, which checked the same things twice, the second with `|| true` on most steps so it could not fail. It gates what is verifiable on main today — gofmt/build/vet/test for the Go module, and the locked install plus `import agents` for Python, which is what Dockerfile.sdk's builder stage and HEALTHCHECK do. It deliberately does not run ruff, mypy or pytest: `make sync` cannot resolve (requires-python >=3.9, a transitive numpy needs >=3.10), ruff reports 2337 errors, and pytest INTERNALERRORs at collection because a test module sys.exit(1)s at import. tests.yml ran `make sync` first in all five jobs, so none of them could have gone green. Those four are written down in LLM.md as work rather than shipped as a permanently red check, which gets read as broken CI instead of broken code. gofmt over eight files so that gate is real instead of omitted; build, vet and test re-run green afterwards. Alignment whitespace only. Not restored, with reasons in LLM.md: docs.yml (mkdocs gh-deploy is GitHub Pages, never published anything, and Pages is not how we serve sites), publish.yml (pointed at the PyPI package hanzoai/python-sdk owns; deliberately removed earlier), issues.yml (upstream's stale-issue bot). LLM.md documents the chain, records the four broken Python checks, and states the open question this repo actually has: nothing in hanzoai/universe references ghcr.io/hanzoai/agent, so the image has no consumer and no App CR. .gitignore ignored .hanzo/ under an "# IDE" heading, which silently dropped both new workflows from the commit — the same silent-staleness shape this is meant to remove, one level down. Narrowed to .hanzo/* with .hanzo/workflows/ re-included. Co-authored-by: Hanzo Dev <[email protected]>
266 lines
7.3 KiB
Go
266 lines
7.3 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// MemoryMetrics holds memory measurement results
|
|
type MemoryMetrics struct {
|
|
Name string
|
|
AllocBytes uint64
|
|
TotalAlloc uint64
|
|
HeapAlloc uint64
|
|
HeapInuse uint64
|
|
NumGC uint32
|
|
Iterations int
|
|
Duration time.Duration
|
|
}
|
|
|
|
// PerIterationKB returns memory per iteration in KB
|
|
func (m *MemoryMetrics) PerIterationKB() float64 {
|
|
if m.Iterations == 0 {
|
|
return 0
|
|
}
|
|
return float64(m.HeapAlloc) / 1024 / float64(m.Iterations)
|
|
}
|
|
|
|
// HeapAllocMB returns heap allocation in MB
|
|
func (m *MemoryMetrics) HeapAllocMB() float64 {
|
|
return float64(m.HeapAlloc) / 1024 / 1024
|
|
}
|
|
|
|
// measureMemory executes a function and measures memory usage
|
|
func measureMemory(name string, iterations int, fn func(int)) *MemoryMetrics {
|
|
// Force GC before measurement
|
|
runtime.GC()
|
|
|
|
var memBefore, memAfter runtime.MemStats
|
|
runtime.ReadMemStats(&memBefore)
|
|
|
|
start := time.Now()
|
|
fn(iterations)
|
|
duration := time.Since(start)
|
|
|
|
// Force GC to get accurate readings
|
|
runtime.GC()
|
|
runtime.ReadMemStats(&memAfter)
|
|
|
|
return &MemoryMetrics{
|
|
Name: name,
|
|
AllocBytes: memAfter.TotalAlloc - memBefore.TotalAlloc,
|
|
TotalAlloc: memAfter.TotalAlloc,
|
|
HeapAlloc: memAfter.HeapAlloc,
|
|
HeapInuse: memAfter.HeapInuse,
|
|
NumGC: memAfter.NumGC - memBefore.NumGC,
|
|
Iterations: iterations,
|
|
Duration: duration,
|
|
}
|
|
}
|
|
|
|
// TestInMemoryBackendMemoryPerformance tests memory efficiency of InMemoryBackend
|
|
func TestInMemoryBackendMemoryPerformance(t *testing.T) {
|
|
t.Run("Memory bounded with many entries", func(t *testing.T) {
|
|
metrics := measureMemory("InMemoryBackend_ManyEntries", 10000, func(n int) {
|
|
backend := NewInMemoryBackend()
|
|
|
|
for i := 0; i < n; i++ {
|
|
key := fmt.Sprintf("key_%06d", i)
|
|
// Create ~1KB payload per entry
|
|
value := strings.Repeat("x", 1000)
|
|
_ = backend.Set(ScopeSession, "test-session", key, value)
|
|
}
|
|
})
|
|
|
|
t.Logf("InMemoryBackend Memory Performance:")
|
|
t.Logf(" Iterations: %d", metrics.Iterations)
|
|
t.Logf(" Heap Alloc: %.2f MB", metrics.HeapAllocMB())
|
|
t.Logf(" Per Iteration: %.2f KB", metrics.PerIterationKB())
|
|
t.Logf(" Duration: %v", metrics.Duration)
|
|
|
|
// With 10000 entries at ~1KB each, should be under 20MB
|
|
if metrics.HeapAllocMB() > 20.0 {
|
|
t.Errorf("Memory too high: %.2f MB (expected < 20 MB)", metrics.HeapAllocMB())
|
|
}
|
|
})
|
|
|
|
t.Run("Scope isolation memory efficiency", func(t *testing.T) {
|
|
metrics := measureMemory("InMemoryBackend_ScopeIsolation", 1000, func(n int) {
|
|
backend := NewInMemoryBackend()
|
|
|
|
scopes := []MemoryScope{ScopeGlobal, ScopeUser, ScopeSession, ScopeWorkflow}
|
|
|
|
for i := 0; i < n; i++ {
|
|
for _, scope := range scopes {
|
|
key := fmt.Sprintf("key_%06d", i)
|
|
value := strings.Repeat("y", 500)
|
|
scopeID := fmt.Sprintf("scope_%d", i%10)
|
|
_ = backend.Set(scope, scopeID, key, value)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Logf("Scope Isolation Memory Performance:")
|
|
t.Logf(" Iterations: %d (x4 scopes)", metrics.Iterations)
|
|
t.Logf(" Heap Alloc: %.2f MB", metrics.HeapAllocMB())
|
|
t.Logf(" Per Iteration: %.2f KB", metrics.PerIterationKB())
|
|
t.Logf(" Duration: %v", metrics.Duration)
|
|
})
|
|
|
|
t.Run("ClearScope releases memory", func(t *testing.T) {
|
|
backend := NewInMemoryBackend()
|
|
|
|
// Add many entries
|
|
for i := 0; i < 5000; i++ {
|
|
key := fmt.Sprintf("key_%06d", i)
|
|
value := strings.Repeat("z", 2000)
|
|
_ = backend.Set(ScopeSession, "test-session", key, value)
|
|
}
|
|
|
|
// Force GC and measure before clear
|
|
runtime.GC()
|
|
var memBefore runtime.MemStats
|
|
runtime.ReadMemStats(&memBefore)
|
|
|
|
// Clear the scope
|
|
backend.ClearScope(ScopeSession, "test-session")
|
|
|
|
// Force GC and measure after clear
|
|
runtime.GC()
|
|
var memAfter runtime.MemStats
|
|
runtime.ReadMemStats(&memAfter)
|
|
|
|
// Memory should be released
|
|
heapBefore := float64(memBefore.HeapAlloc) / 1024 / 1024
|
|
heapAfter := float64(memAfter.HeapAlloc) / 1024 / 1024
|
|
reduction := ((heapBefore - heapAfter) / heapBefore) * 100
|
|
|
|
t.Logf("ClearScope Memory Release:")
|
|
t.Logf(" Before Clear: %.2f MB", heapBefore)
|
|
t.Logf(" After Clear: %.2f MB", heapAfter)
|
|
t.Logf(" Reduction: %.1f%%", reduction)
|
|
|
|
// Should release at least 50% of memory
|
|
if reduction < 50.0 && heapBefore > 1.0 {
|
|
t.Logf("Warning: Less than 50%% memory released (%.1f%%)", reduction)
|
|
}
|
|
})
|
|
}
|
|
|
|
// BenchmarkInMemoryBackendSet benchmarks Set operation
|
|
func BenchmarkInMemoryBackendSet(b *testing.B) {
|
|
backend := NewInMemoryBackend()
|
|
value := strings.Repeat("x", 1000)
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
key := fmt.Sprintf("key_%d", i)
|
|
_ = backend.Set(ScopeSession, "bench-session", key, value)
|
|
}
|
|
}
|
|
|
|
// BenchmarkInMemoryBackendGet benchmarks Get operation
|
|
func BenchmarkInMemoryBackendGet(b *testing.B) {
|
|
backend := NewInMemoryBackend()
|
|
|
|
// Pre-populate with data
|
|
for i := 0; i < 10000; i++ {
|
|
key := fmt.Sprintf("key_%d", i)
|
|
value := strings.Repeat("x", 1000)
|
|
_ = backend.Set(ScopeSession, "bench-session", key, value)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
key := fmt.Sprintf("key_%d", i%10000)
|
|
_, _, _ = backend.Get(ScopeSession, "bench-session", key)
|
|
}
|
|
}
|
|
|
|
// BenchmarkInMemoryBackendList benchmarks List operation
|
|
func BenchmarkInMemoryBackendList(b *testing.B) {
|
|
backend := NewInMemoryBackend()
|
|
|
|
// Pre-populate with data
|
|
for i := 0; i < 1000; i++ {
|
|
key := fmt.Sprintf("key_%d", i)
|
|
value := strings.Repeat("x", 100)
|
|
_ = backend.Set(ScopeSession, "bench-session", key, value)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
b.ReportAllocs()
|
|
|
|
for i := 0; i < b.N; i++ {
|
|
_, _ = backend.List(ScopeSession, "bench-session")
|
|
}
|
|
}
|
|
|
|
// TestMemoryPerformanceReport generates a comprehensive memory report
|
|
func TestMemoryPerformanceReport(t *testing.T) {
|
|
var metrics []*MemoryMetrics
|
|
|
|
// Test 1: InMemoryBackend with completions
|
|
metrics = append(metrics, measureMemory("InMemoryBackend_1K", 1000, func(n int) {
|
|
backend := NewInMemoryBackend()
|
|
for i := 0; i < n; i++ {
|
|
key := fmt.Sprintf("k_%d", i)
|
|
_ = backend.Set(ScopeSession, "s", key, strings.Repeat("x", 10000))
|
|
}
|
|
}))
|
|
|
|
// Test 2: Multiple scopes
|
|
metrics = append(metrics, measureMemory("InMemoryBackend_MultiScope", 1000, func(n int) {
|
|
backend := NewInMemoryBackend()
|
|
scopes := []MemoryScope{ScopeGlobal, ScopeUser, ScopeSession, ScopeWorkflow}
|
|
for i := 0; i < n; i++ {
|
|
for _, scope := range scopes {
|
|
key := fmt.Sprintf("k_%d", i)
|
|
_ = backend.Set(scope, fmt.Sprintf("id_%d", i%10), key, strings.Repeat("y", 1000))
|
|
}
|
|
}
|
|
}))
|
|
|
|
// Test 3: High-frequency operations
|
|
metrics = append(metrics, measureMemory("InMemoryBackend_HighFreq", 10000, func(n int) {
|
|
backend := NewInMemoryBackend()
|
|
for i := 0; i < n; i++ {
|
|
key := fmt.Sprintf("k_%d", i%100)
|
|
_ = backend.Set(ScopeSession, "s", key, i)
|
|
_, _, _ = backend.Get(ScopeSession, "s", key)
|
|
}
|
|
}))
|
|
|
|
// Print report
|
|
t.Log("")
|
|
t.Log("=" + strings.Repeat("=", 69))
|
|
t.Log("GO SDK MEMORY PERFORMANCE REPORT")
|
|
t.Log("=" + strings.Repeat("=", 69))
|
|
t.Logf("%-35s %10s %10s %12s", "Test Name", "Heap (MB)", "Alloc (MB)", "Per Iter (KB)")
|
|
t.Log("-" + strings.Repeat("-", 69))
|
|
|
|
for _, m := range metrics {
|
|
t.Logf("%-35s %10.2f %10.2f %12.2f",
|
|
m.Name,
|
|
m.HeapAllocMB(),
|
|
float64(m.AllocBytes)/1024/1024,
|
|
m.PerIterationKB(),
|
|
)
|
|
}
|
|
t.Log("=" + strings.Repeat("=", 69))
|
|
|
|
// Assertions
|
|
for _, m := range metrics {
|
|
if m.HeapAllocMB() > 50.0 {
|
|
t.Errorf("%s: Heap allocation too high: %.2f MB", m.Name, m.HeapAllocMB())
|
|
}
|
|
}
|
|
}
|