f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
340 lines
11 KiB
Go
340 lines
11 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestRealDBUsagePayload measures the JSON payload the dashboard must
|
|
// serialize, transfer, parse, and render — the cost query timing hides.
|
|
//
|
|
// REAL_DB=/Users/wesm/.agentsview/sessions.db \
|
|
// CGO_ENABLED=1 go test -tags fts5 -run TestRealDBUsagePayload \
|
|
// -v -timeout 600s ./internal/db/
|
|
func TestRealDBUsagePayload(t *testing.T) {
|
|
path := os.Getenv("REAL_DB")
|
|
if path == "" {
|
|
t.Skip("set REAL_DB to the sessions.db path to run")
|
|
}
|
|
reader, err := sql.Open("sqlite3", makeDSN(path, true))
|
|
if err != nil {
|
|
t.Fatalf("open reader: %v", err)
|
|
}
|
|
reader.SetMaxOpenConns(4)
|
|
defer reader.Close()
|
|
d := &DB{path: path}
|
|
d.reader.Store(reader)
|
|
ctx := context.Background()
|
|
tz := "America/New_York"
|
|
|
|
f := UsageFilter{From: "2000-01-01", To: "2035-01-01", Timezone: tz, Breakdowns: true}
|
|
r, err := d.GetDailyUsage(ctx, f)
|
|
if err != nil {
|
|
t.Fatalf("GetDailyUsage: %v", err)
|
|
}
|
|
var proj, agent, model int
|
|
for _, day := range r.Daily {
|
|
proj += len(day.ProjectBreakdowns)
|
|
agent += len(day.AgentBreakdowns)
|
|
model += len(day.ModelBreakdowns)
|
|
}
|
|
start := time.Now()
|
|
blob, _ := json.Marshal(r.Daily)
|
|
t.Logf("summary .Daily: %d days, breakdown entries: %d project + %d agent + %d model",
|
|
len(r.Daily), proj, agent, model)
|
|
t.Logf("summary .Daily JSON: %.2f MB, marshal=%s",
|
|
float64(len(blob))/1e6, round(time.Since(start)))
|
|
|
|
ix, err := d.GetSidebarSessionIndex(ctx, SessionFilter{})
|
|
if err != nil {
|
|
t.Fatalf("sidebar: %v", err)
|
|
}
|
|
start = time.Now()
|
|
sb, _ := json.Marshal(ix)
|
|
t.Logf("sidebar-index: %d rows, JSON %.2f MB, marshal=%s",
|
|
len(ix.Sessions), float64(len(sb))/1e6, round(time.Since(start)))
|
|
if out := os.Getenv("DUMP_SIDEBAR"); out != "" {
|
|
if err := dumpSidebarJSON(out, path, sb); err != nil {
|
|
t.Fatalf("dump sidebar: %v", err)
|
|
}
|
|
t.Logf("wrote sidebar JSON to %s", out)
|
|
}
|
|
}
|
|
|
|
// TestRealDBUsagePerf times every query the usage dashboard triggers,
|
|
// against a real prod DB. Gated behind REAL_DB so it never runs in CI.
|
|
//
|
|
// REAL_DB=/Users/wesm/.agentsview/sessions.db \
|
|
// CGO_ENABLED=1 go test -tags fts5 -run TestRealDBUsagePerf \
|
|
// -v -timeout 1200s ./internal/db/
|
|
func TestRealDBUsagePerf(t *testing.T) {
|
|
path := os.Getenv("REAL_DB")
|
|
if path == "" {
|
|
t.Skip("set REAL_DB to the sessions.db path to run")
|
|
}
|
|
|
|
// makeDSN(path, true) sets mode=ro: this connection cannot write.
|
|
// No Open(), so no migrations / drops touch the archive.
|
|
reader, err := sql.Open("sqlite3", makeDSN(path, true))
|
|
if err != nil {
|
|
t.Fatalf("open reader: %v", err)
|
|
}
|
|
reader.SetMaxOpenConns(4) // matches production reader pool
|
|
defer reader.Close()
|
|
|
|
d := &DB{path: path}
|
|
d.reader.Store(reader)
|
|
ctx := context.Background()
|
|
tz := "America/New_York"
|
|
|
|
walActive := fileExists(path + "-wal")
|
|
t.Logf("DB=%s reader_pool=4 wal_active=%v (writes in flight inflate reads)",
|
|
path, walActive)
|
|
|
|
allHist := UsageFilter{From: "2000-01-01", To: "2035-01-01", Timezone: tz}
|
|
win30 := UsageFilter{
|
|
From: time.Now().AddDate(0, 0, -30).Format("2006-01-02"),
|
|
To: time.Now().Format("2006-01-02"),
|
|
Timezone: tz,
|
|
}
|
|
|
|
// Each DB method the dashboard's load fan-out triggers. Two runs
|
|
// per entry: run 1 is cold (disk), run 2 is warm (OS page cache).
|
|
type probe struct {
|
|
name string
|
|
fn func() (string, error)
|
|
}
|
|
probes := []probe{
|
|
{"stats", func() (string, error) {
|
|
s, err := d.GetStats(ctx, true, true)
|
|
return fmt.Sprintf("%+v", s), err
|
|
}},
|
|
{"projects (GROUP BY sessions)", func() (string, error) {
|
|
p, err := d.GetProjects(ctx, false, false)
|
|
return fmt.Sprintf("%d projects", len(p)), err
|
|
}},
|
|
{"agents (GROUP BY sessions)", func() (string, error) {
|
|
a, err := d.GetAgents(ctx, false, false)
|
|
return fmt.Sprintf("%d agents", len(a)), err
|
|
}},
|
|
{"machines (DISTINCT)", func() (string, error) {
|
|
m, err := d.GetMachines(ctx, false, false)
|
|
return fmt.Sprintf("%d machines", len(m)), err
|
|
}},
|
|
{"sidebar-index (all sessions)", func() (string, error) {
|
|
ix, err := d.GetSidebarSessionIndex(ctx, SessionFilter{})
|
|
return fmt.Sprintf("%d rows", len(ix.Sessions)), err
|
|
}},
|
|
{"usage/summary: GetDailyUsage allHist (breakdowns)", func() (string, error) {
|
|
f := allHist
|
|
f.Breakdowns = true
|
|
r, err := d.GetDailyUsage(ctx, f)
|
|
return fmt.Sprintf("%d days, $%.0f", len(r.Daily), r.Totals.TotalCost), err
|
|
}},
|
|
{"usage/session-counts diagnostic allHist (not live path)", func() (string, error) {
|
|
c, err := d.GetUsageSessionCounts(ctx, allHist)
|
|
return fmt.Sprintf("%d sessions", c.Total), err
|
|
}},
|
|
{"usage/comparison: GetDailyUsage prior-window", func() (string, error) {
|
|
// prior period for an all-history view: empty window far in past
|
|
f := UsageFilter{From: "1900-01-01", To: "1999-12-31", Timezone: tz}
|
|
r, err := d.GetDailyUsage(ctx, f)
|
|
return fmt.Sprintf("%d days", len(r.Daily)), err
|
|
}},
|
|
{"usage/top-sessions: GetTopSessionsByCost allHist", func() (string, error) {
|
|
e, err := d.GetTopSessionsByCost(ctx, allHist, 20)
|
|
return fmt.Sprintf("%d rows", len(e)), err
|
|
}},
|
|
{"usage/summary: GetDailyUsage 30d (breakdowns)", func() (string, error) {
|
|
f := win30
|
|
f.Breakdowns = true
|
|
r, err := d.GetDailyUsage(ctx, f)
|
|
return fmt.Sprintf("%d days, $%.0f", len(r.Daily), r.Totals.TotalCost), err
|
|
}},
|
|
{"usage/top-sessions: GetTopSessionsByCost 30d", func() (string, error) {
|
|
e, err := d.GetTopSessionsByCost(ctx, win30, 20)
|
|
return fmt.Sprintf("%d rows", len(e)), err
|
|
}},
|
|
}
|
|
|
|
t.Logf("%-52s %10s %10s %s", "QUERY (isolated)", "cold", "warm", "result")
|
|
for _, p := range probes {
|
|
var cold, warm time.Duration
|
|
var info string
|
|
for run := range 2 {
|
|
start := time.Now()
|
|
res, err := p.fn()
|
|
d := time.Since(start)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", p.name, err)
|
|
}
|
|
if run == 0 {
|
|
cold, info = d, res
|
|
} else {
|
|
warm = d
|
|
}
|
|
}
|
|
t.Logf("%-52s %10s %10s %s",
|
|
p.name, round(cold), round(warm), info)
|
|
}
|
|
|
|
// Concurrent pattern 1: usage.fetchAll() = summary + comparison +
|
|
// top-sessions firing at once (3 live dashboard endpoints, 4-conn pool).
|
|
t.Logf("")
|
|
timeConcurrent(t, "CONCURRENT fetchAll (summary+comparison+top, allHist)", []func() error{
|
|
func() error { f := allHist; f.Breakdowns = true; _, e := d.GetDailyUsage(ctx, f); return e },
|
|
func() error {
|
|
f := UsageFilter{From: "1900-01-01", To: "1999-12-31", Timezone: tz}
|
|
_, e := d.GetDailyUsage(ctx, f)
|
|
return e
|
|
},
|
|
func() error { _, e := d.GetTopSessionsByCost(ctx, allHist, 20); return e },
|
|
})
|
|
|
|
// Concurrent pattern 2: the full page-open fan-out (everything the
|
|
// browser fires when you click the Usage tab), through one pool.
|
|
timeConcurrent(t, "CONCURRENT full page-open fan-out (8 endpoints, allHist)", []func() error{
|
|
func() error { _, e := d.GetStats(ctx, true, true); return e },
|
|
func() error { _, e := d.GetProjects(ctx, false, false); return e },
|
|
func() error { _, e := d.GetAgents(ctx, false, false); return e },
|
|
func() error { _, e := d.GetMachines(ctx, false, false); return e },
|
|
func() error { _, e := d.GetSidebarSessionIndex(ctx, SessionFilter{}); return e },
|
|
func() error { f := allHist; f.Breakdowns = true; _, e := d.GetDailyUsage(ctx, f); return e },
|
|
func() error {
|
|
f := UsageFilter{From: "1900-01-01", To: "1999-12-31", Timezone: tz}
|
|
_, e := d.GetDailyUsage(ctx, f)
|
|
return e
|
|
},
|
|
func() error { _, e := d.GetTopSessionsByCost(ctx, allHist, 20); return e },
|
|
})
|
|
}
|
|
|
|
func TestDumpSidebarJSONRejectsDBAndSidecars(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "sessions.db")
|
|
require.NoError(t, os.WriteFile(dbPath, []byte("db"), 0o644))
|
|
|
|
for _, out := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} {
|
|
t.Run(filepath.Base(out), func(t *testing.T) {
|
|
err := dumpSidebarJSON(out, dbPath, []byte(`{"sessions":[]}`))
|
|
require.Error(t, err)
|
|
|
|
got, readErr := os.ReadFile(dbPath)
|
|
require.NoError(t, readErr)
|
|
assert.Equal(t, "db", string(got))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDumpSidebarJSONDoesNotClobberExistingFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "sessions.db")
|
|
outPath := filepath.Join(dir, "sidebar.json")
|
|
require.NoError(t, os.WriteFile(dbPath, []byte("db"), 0o644))
|
|
require.NoError(t, os.WriteFile(outPath, []byte("existing"), 0o644))
|
|
|
|
err := dumpSidebarJSON(outPath, dbPath, []byte(`{"sessions":[]}`))
|
|
require.Error(t, err)
|
|
|
|
got, readErr := os.ReadFile(outPath)
|
|
require.NoError(t, readErr)
|
|
assert.Equal(t, "existing", string(got))
|
|
}
|
|
|
|
func TestDumpSidebarJSONCreatesNewFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
dbPath := filepath.Join(dir, "sessions.db")
|
|
outPath := filepath.Join(dir, "sidebar.json")
|
|
payload := []byte(`{"sessions":[]}`)
|
|
require.NoError(t, os.WriteFile(dbPath, []byte("db"), 0o644))
|
|
|
|
require.NoError(t, dumpSidebarJSON(outPath, dbPath, payload))
|
|
|
|
got, err := os.ReadFile(outPath)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, payload, got)
|
|
}
|
|
|
|
func dumpSidebarJSON(out, dbPath string, payload []byte) error {
|
|
if err := rejectSidebarDumpPath(out, dbPath); err != nil {
|
|
return err
|
|
}
|
|
|
|
f, err := os.OpenFile(out, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return fmt.Errorf("create sidebar dump %q: %w", out, err)
|
|
}
|
|
if _, err := f.Write(payload); err != nil {
|
|
_ = f.Close()
|
|
return fmt.Errorf("write sidebar dump %q: %w", out, err)
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
return fmt.Errorf("close sidebar dump %q: %w", out, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func rejectSidebarDumpPath(out, dbPath string) error {
|
|
outPath, err := cleanAbsPath(out)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve sidebar dump path: %w", err)
|
|
}
|
|
for _, forbidden := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} {
|
|
forbiddenPath, err := cleanAbsPath(forbidden)
|
|
if err != nil {
|
|
return fmt.Errorf("resolve protected DB path %q: %w", forbidden, err)
|
|
}
|
|
if outPath == forbiddenPath {
|
|
return fmt.Errorf("refusing to write sidebar dump over protected DB path %q", out)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cleanAbsPath(path string) (string, error) {
|
|
abs, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return filepath.Clean(abs), nil
|
|
}
|
|
|
|
func timeConcurrent(t *testing.T, label string, fns []func() error) {
|
|
start := time.Now()
|
|
var wg sync.WaitGroup
|
|
errs := make([]error, len(fns))
|
|
for i, fn := range fns {
|
|
wg.Add(1)
|
|
go func(i int, fn func() error) {
|
|
defer wg.Done()
|
|
errs[i] = fn()
|
|
}(i, fn)
|
|
}
|
|
wg.Wait()
|
|
for _, e := range errs {
|
|
if e != nil {
|
|
t.Fatalf("%s: %v", label, e)
|
|
}
|
|
}
|
|
t.Logf("%-52s wall=%s", label, round(time.Since(start)))
|
|
}
|
|
|
|
func round(d time.Duration) string {
|
|
return d.Round(time.Millisecond).String()
|
|
}
|
|
|
|
func fileExists(p string) bool {
|
|
_, err := os.Stat(p)
|
|
return err == nil
|
|
}
|