blob: 54e06d926bc3a1a3c4ba724afb49dcee59cefbe0 [file]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package perf
import (
"context"
"sync"
"testing"
"time"
"chromiumos/tast/errors"
)
type testTimelineDatasource struct {
setUp, started bool
errSetup, errStart, errSnapshot, errStop error
snapshotCount int
snapshotChannel chan int
snapshotDuration time.Duration
stopCount int
clock *fakeClock
}
func newDatasource() *testTimelineDatasource {
ds := &testTimelineDatasource{}
// A value is added to this channel each time a snapshot is taken. Tests should take no more snapshots than the buffer size of this channel.
ds.snapshotChannel = make(chan int, 1000)
return ds
}
func (d *testTimelineDatasource) Setup(_ context.Context, _ string) error {
if d.errSetup != nil {
return d.errSetup
}
d.setUp = true
return nil
}
func (d *testTimelineDatasource) Start(_ context.Context) error {
if d.errStart != nil {
return d.errStart
}
d.started = true
return nil
}
func (d *testTimelineDatasource) Snapshot(ctx context.Context, v *Values) error {
if d.clock != nil {
// Simulate a slow snapshot. The main goroutine does not advance the clock until this function returns.
d.clock.Advance(d.snapshotDuration)
}
if d.errSnapshot != nil {
return d.errSnapshot
}
d.snapshotCount++
select {
case d.snapshotChannel <- d.snapshotCount - 1:
default:
}
return nil
}
func (d *testTimelineDatasource) Stop(ctx context.Context, v *Values) error {
if d.errStop != nil {
return d.errStop
}
d.stopCount++
return nil
}
// WaitForSnapshot waits until a snapshot is taken or until the snapshotting goroutine returns.
func (d *testTimelineDatasource) WaitForSnapshot() {
select {
case <-d.snapshotChannel:
case <-time.After(5 * time.Second):
panic("WaitForSnapshot timed out")
}
}
func (t *Timeline) WaitForSnapshottingDone() {
select {
case err := <-t.recordingStatus:
t.recordingStatus <- err
case <-time.After(5 * time.Second):
panic("WaitForSnapshottingDone timed out")
}
}
// sleeper stores state of a sleeping goroutine.
type sleeper struct {
// until is the time when the goroutine should wake up.
until time.Time
// done is a channel that is being closed to wake up the goroutine.
done chan struct{}
}
// fakeClock maintains a fake clock for simulating Sleep() statements.
type fakeClock struct {
// clock is the current time of the fake clock.
clock time.Time
// sleepers are waiting for time to advance.
sleepers []*sleeper
// waiters are waiting for at least one sleeper to appear.
waiters []chan struct{}
// lock prevents concurrent reads/writes of sleepers, waiters and the clock.
lock sync.Mutex
}
// NewFakeClock returns a new fake clock for Sleep() statements in unit tests.
func NewFakeClock() *fakeClock {
return &fakeClock{clock: time.Unix(0, 0)}
}
// Advance increases the fake time and may wake up Sleep()-ing goroutines.
func (c *fakeClock) Advance(d time.Duration) {
c.lock.Lock()
defer c.lock.Unlock()
newTime := c.clock.Add(d)
// Maybe wake up sleepers.
var newSleepers []*sleeper
for _, s := range c.sleepers {
if newTime.Before(s.until) {
// Continue sleeping.
newSleepers = append(newSleepers, s)
} else {
close(s.done)
}
}
c.sleepers = newSleepers
c.clock = newTime
}
// Now returns the fake time.
func (c *fakeClock) Now() time.Time {
c.lock.Lock()
defer c.lock.Unlock()
return c.clock
}
// Sleep sleeps until a fake time of d has passed. Sleep returns with an error if the context is done.
func (c *fakeClock) Sleep(ctx context.Context, d time.Duration) error {
done := make(chan struct{})
if d <= 0 {
close(done)
} else {
s := &sleeper{until: c.Now().Add(d), done: done}
c.lock.Lock()
c.sleepers = append(c.sleepers, s)
// Notify waiters.
for _, w := range c.waiters {
close(w)
}
c.waiters = []chan struct{}{}
c.lock.Unlock()
}
select {
case <-done:
return nil
case <-ctx.Done():
// Remove sleeper from list.
c.lock.Lock()
var newSleepers []*sleeper
for _, s := range c.sleepers {
if s.done != done {
newSleepers = append(newSleepers, s)
}
}
c.sleepers = newSleepers
c.lock.Unlock()
return ctx.Err()
case <-time.After(5 * time.Second):
panic("Sleep timed out")
}
}
// WaitForSleep waits until a goroutine starts fake-sleeping.
func (c *fakeClock) WaitForSleep() {
c.lock.Lock()
if len(c.sleepers) > 0 {
// At least one goroutine is already sleeping.
c.lock.Unlock()
return
}
done := make(chan struct{})
c.waiters = append(c.waiters, done)
c.lock.Unlock()
select {
case <-done:
case <-time.After(5 * time.Second):
panic("WaitForSleep timed out")
}
}
func TestTimeline(t *testing.T) {
p := NewValues()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clock := NewFakeClock()
d1 := newDatasource()
d2 := newDatasource()
tl, err := NewTimeline(ctx, []TimelineDatasource{d1, d2}, Interval(1*time.Second), WithClock(clock))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if !d1.setUp || !d2.setUp {
t.Error("Failed to set up both datasources")
}
if err := tl.Start(ctx); err != nil {
t.Error("Failed to start timeline: ", err)
}
if !d1.started || !d2.started {
t.Error("Failed to call start on both datasources")
}
// First round of recording.
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// Take 2 samples.
clock.WaitForSleep()
for i := 0; i < 2; i++ {
clock.Advance(1050 * time.Millisecond)
d1.WaitForSnapshot()
clock.WaitForSleep()
}
if v, err := tl.StopRecording(ctx); err != nil {
t.Error("Error while recording: ", err)
} else {
p.Merge(v)
}
if d1.snapshotCount != 2 || d2.snapshotCount != 2 {
t.Errorf("Wrong number of snapshots collected: got (%d, %d), want (2, 2)", d1.snapshotCount, d2.snapshotCount)
}
if d1.stopCount != 1 || d2.stopCount != 1 {
t.Errorf("Wrong number of stops collected: got (%d, %d), want (1, 1)", d1.stopCount, d2.stopCount)
}
// Second round of recording.
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// Take 3 more samples.
clock.WaitForSleep()
for i := 2; i < 5; i++ {
clock.Advance(1070 * time.Millisecond)
d1.WaitForSnapshot()
clock.WaitForSleep()
}
if v, err := tl.StopRecording(ctx); err != nil {
t.Error("Error while recording: ", err)
} else {
p.Merge(v)
}
if d1.snapshotCount != 5 || d2.snapshotCount != 5 {
t.Errorf("Wrong number of snapshots collected: got (%d, %d), want (5, 5)", d1.snapshotCount, d2.snapshotCount)
}
if d1.stopCount != 2 || d2.stopCount != 2 {
t.Errorf("Wrong number of stops collected: got (%d, %d), want (2, 2)", d1.stopCount, d2.stopCount)
}
var timestamps []float64
for k, v := range p.values {
if k.Name == "t" {
timestamps = v
}
}
if timestamps == nil {
t.Fatal("Could not find timestamps metric")
}
if len(timestamps) != 5 {
t.Fatalf("Wrong number of timestamps logged, got %d, want 5", len(timestamps))
}
for i := 0; i < len(timestamps)-1; i++ {
if timestamps[i] > timestamps[i+1] {
t.Errorf("Timestamps logged in wrong order, want %f < %f", timestamps[i], timestamps[i+1])
}
}
}
func TestTimelineStartRecordingTwice(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := newDatasource()
tl, err := NewTimeline(ctx, []TimelineDatasource{d}, Interval(200*time.Millisecond))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.Start(ctx); err != nil {
t.Error("Failed to start timeline: ", err)
}
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
if err := tl.StartRecording(ctx); err == nil {
t.Error("StartRecording should have failed")
}
}
func TestTimelineSlowSnapshot(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clock := NewFakeClock()
d := newDatasource()
d.snapshotDuration = 500 * time.Millisecond
d.clock = clock
tl, err := NewTimeline(ctx, []TimelineDatasource{d}, Interval(200*time.Millisecond), WithClock(clock))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.Start(ctx); err != nil {
t.Error("Failed to start timeline: ", err)
}
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// Try to take 1 sample.
clock.WaitForSleep()
clock.Advance(210 * time.Millisecond)
tl.WaitForSnapshottingDone()
if _, err := tl.StopRecording(ctx); err == nil {
t.Error("StopRecording should have failed")
}
}
func TestTimelineNoStart(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clock := NewFakeClock()
d := newDatasource()
tl, err := NewTimeline(ctx, []TimelineDatasource{d}, Interval(1*time.Second), WithClock(clock))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// Try to take 1 sample.
clock.WaitForSleep()
clock.Advance(1100 * time.Millisecond)
tl.WaitForSnapshottingDone()
if _, err := tl.StopRecording(ctx); err == nil {
t.Error("Snapshot should have failed without calling Start first")
}
}
func TestTimelineSetupFail(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := newDatasource()
d.errSetup = errors.New("setup should fail")
if _, err := NewTimeline(ctx, []TimelineDatasource{d}); err == nil {
t.Error("NewTimeline should have failed because of setup failure")
}
}
func TestTimelineStartFail(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
d := newDatasource()
d.errStart = errors.New("start should fail")
tl, err := NewTimeline(ctx, []TimelineDatasource{d})
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.Start(ctx); err == nil {
t.Error("Start should have failed")
}
}
func TestTimelineSnapshotFail(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clock := NewFakeClock()
d := newDatasource()
d.errSnapshot = errors.New("snapshot should fail")
tl, err := NewTimeline(ctx, []TimelineDatasource{d}, Interval(1*time.Second), WithClock(clock))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.Start(ctx); err != nil {
t.Error("Failed to start timeline: ", err)
}
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// Try to take 1 sample.
clock.WaitForSleep()
clock.Advance(1100 * time.Millisecond)
tl.WaitForSnapshottingDone()
if _, err := tl.StopRecording(ctx); err == nil || err != d.errSnapshot {
t.Error("Snapshot should have failed")
}
}
func TestTimelineStopFail(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
clock := NewFakeClock()
d := newDatasource()
d.errStop = errors.New("stop should fail")
tl, err := NewTimeline(ctx, []TimelineDatasource{d}, Interval(1*time.Second), WithClock(clock))
if err != nil {
t.Error("Failed to create Timeline: ", err)
}
if err := tl.Start(ctx); err != nil {
t.Error("Failed to start timeline: ", err)
}
if err := tl.StartRecording(ctx); err != nil {
t.Error("Failed to start recording: ", err)
}
// We don't need any samples to test Stop().
if _, err := tl.StopRecording(ctx); err == nil || err != d.errStop {
t.Error("Stop should have failed")
}
}