Skip to content
quarry Artifact [7d9376f957]
Login

Artifact [7d9376f957]

Artifact 7d9376f9579f705b2deb12642fb8ced610dff5a37742e85fb6af4ac686c25276:


package obs

import (
	"testing"
	"time"
)

// The number that made this package necessary is CONCURRENCY, not
// duration: one slow listing is a performance note, and thirteen of the
// same listing at once is an incident. Live must count what is running,
// and peak must survive them finishing — otherwise the page shows zero
// after the fire is out and the operator learns nothing.
func TestLiveAndPeakSubprocessCount(t *testing.T) {
	r := NewRecorder(10, nil)
	for range 13 {
		r.Spawned()
	}
	if s := r.Snapshot(); s.LiveSubproc != 13 || s.PeakSubproc != 13 {
		t.Fatalf("during: live=%d peak=%d, want 13/13", s.LiveSubproc, s.PeakSubproc)
	}
	for range 13 {
		r.RecordSubprocess(Subprocess{Verb: "ls", Duration: time.Second})
	}
	s := r.Snapshot()
	if s.LiveSubproc != 0 {
		t.Errorf("after: live=%d, want 0", s.LiveSubproc)
	}
	if s.PeakSubproc != 13 {
		t.Errorf("after: peak=%d, want it to survive — the evidence outlives the incident", s.PeakSubproc)
	}
}

// A recorder that never goes negative, however the pairing breaks. A
// subprocess recorded without a matching Spawned is a bug, but it must
// not make the panel show a nonsense number.
func TestLiveNeverGoesNegative(t *testing.T) {
	r := NewRecorder(4, nil)
	r.RecordSubprocess(Subprocess{Verb: "cat"})
	if s := r.Snapshot(); s.LiveSubproc != 0 {
		t.Errorf("live=%d, want 0", s.LiveSubproc)
	}
}

// Slowest first by p95, which is the question the page answers. A mean
// would have hidden the 4.24s files page behind hundreds of 30ms ones.
func TestPathsRankByTailNotAverage(t *testing.T) {
	r := NewRecorder(500, nil)
	// A path that is usually fast and occasionally terrible.
	for range 99 {
		r.RecordRender(Render{Path: "/r/{repo}", Elapsed: 30 * time.Millisecond})
	}
	for range 20 {
		r.RecordRender(Render{Path: "/r/{repo}", Elapsed: 4 * time.Second})
	}
	// A path that is uniformly middling — a higher MEAN is not the point.
	for range 60 {
		r.RecordRender(Render{Path: "/timeline", Elapsed: 300 * time.Millisecond})
	}
	s := r.Snapshot()
	if len(s.Paths) != 2 {
		t.Fatalf("paths = %d, want 2", len(s.Paths))
	}
	if s.Paths[0].Path != "/r/{repo}" {
		t.Errorf("first = %q, want the one with the bad tail", s.Paths[0].Path)
	}
	if s.Paths[0].P95 < time.Second {
		t.Errorf("p95 = %v, want it to reflect the slow tail", s.Paths[0].P95)
	}
	if s.Paths[0].Max != 4*time.Second {
		t.Errorf("max = %v, want 4s", s.Paths[0].Max)
	}
	if s.Paths[0].N != 119 {
		t.Errorf("n = %d, want every render counted even though samples are windowed", s.Paths[0].N)
	}
}

// The sink is where history goes; the rings are what the page reads.
// A forge with no analytics store must still fill its panels.
func TestRingsWorkWithoutASink(t *testing.T) {
	r := NewRecorder(3, nil)
	r.RecordRender(Render{Path: "/a", Elapsed: time.Millisecond})
	if s := r.Snapshot(); len(s.Renders) != 1 {
		t.Fatalf("renders = %d, want 1 with a nil sink", len(s.Renders))
	}
}

type capture struct{ renders, subs int }

func (c *capture) Render(Render)         { c.renders++ }
func (c *capture) Subprocess(Subprocess) { c.subs++ }

func TestSinkReceivesEverythingTheRingsDo(t *testing.T) {
	c := &capture{}
	r := NewRecorder(2, c)
	// More than the ring holds: the ring is a window, the sink is not.
	for range 5 {
		r.RecordRender(Render{Path: "/a"})
		r.Spawned()
		r.RecordSubprocess(Subprocess{Verb: "ls"})
	}
	if c.renders != 5 || c.subs != 5 {
		t.Errorf("sink saw %d renders / %d subs, want 5/5 — the window must not truncate history",
			c.renders, c.subs)
	}
	if s := r.Snapshot(); len(s.Renders) != 2 {
		t.Errorf("ring held %d, want its cap of 2", len(s.Renders))
	}
}