package test

import (
	"encoding/hex"
	"flag"
	"math/rand"
	"os"
	"runtime"
	"sort"
	"sync"
	"sync/atomic"
	"testing"
	"time"
)

var (
	sink    atomic.Value
	ballast *link

	busyLen        = flag.Int("busy-bytes", 1000000, "busy loop size")
	bucketDuration = flag.Duration("bucket-duration", 100*time.Microsecond, "size of time buckets")
	workerAlloc    = flag.Int("worker-alloc", 30000, "target for memory allocation between sleeps")
	ballastSize    = flag.Int("ballast", 30000000, "Number of bytes to retain")
)

const (
	ballastPointerCount = 100
	ballastByteCount    = 1000
)

type link struct {
	a [ballastPointerCount]*link
	b [ballastByteCount]byte
}

func TestMain(m *testing.M) {
	flag.Parse()

	for i := 0; i < (*ballastSize)/(8*ballastPointerCount+ballastByteCount); i++ {
		var next link
		for i := range next.a {
			next.a[i] = ballast
		}
		ballast = &next
	}

	os.Exit(m.Run())
}

// BenchmarkResume attempts to demonstrate issue 45894. It generates some
// measurements, but its main result is execution traces that show an
// application that is slow to resume work after the GC's mark phase ends.
//
// It does its work by launching many goroutines that allocate memory in several
// sizes: so each P will have work to do in mcache.prepareForSweep, and for many
// chances for goroutines to get blocked on mark assist credit.
//
// It records the time when each goroutine was able to make progress. That might
// end up as a concise measure of how well the program is able to make progress
// while the GC runs, but for now the clearest signal comes from looking at
// execution traces (via the "-test.trace" flag and "go tool trace" command).
func BenchmarkResume(b *testing.B) {
	whenCh := make(chan time.Time, b.N)
	when := make([]time.Time, 0, b.N)
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		for w := range whenCh {
			when = append(when, w)
		}
	}()

	var mem runtime.MemStats
	runtime.ReadMemStats(&mem)

	busy := make([]byte, *busyLen)

	var seed int64
	b.SetParallelism(10)
	b.ResetTimer()
	b.RunParallel(func(pb *testing.PB) {
		rng := rand.New(rand.NewSource(atomic.AddInt64(&seed, 1)))
		busyDest := make([]byte, hex.EncodedLen(len(busy)))
		for pb.Next() {
			runtime.Gosched()
			hex.Encode(busyDest, busy[:rng.Intn(len(busy))])
			todo := *workerAlloc
			var v []byte
			for _, class := range mem.BySize {
				if todo < int(class.Size) {
					break
				}
				todo -= int(class.Size)
				v = make([]byte, int(class.Size))
			}
			sink.Store(v)
			v = make([]byte, todo)
			whenCh <- time.Now()
		}
	})
	b.StopTimer()
	close(whenCh)
	wg.Wait()

	// Trim off first and last quarter of the time range, to catch the test in
	// steady state.
	sort.Slice(when, func(i, j int) bool { return when[i].Before(when[j]) })
	when = when[len(when)/4 : 3*len(when)/4]

	if len(when) < 2 {
		return
	}

	start := when[0]
	end := when[len(when)-1]

	buckets := make([]int, 1+int(end.Sub(start)/(*bucketDuration)))
	for _, t := range when {
		delta := t.Sub(start)
		bucket := int(delta.Truncate((*bucketDuration)) / (*bucketDuration))
		buckets[bucket]++
	}
	// Trim off the last bucket, which may not cover the full interval.
	buckets = buckets[:len(buckets)-1]

	if len(buckets) < 1 {
		return
	}

	sort.Sort(sort.IntSlice(buckets))

	first := len(buckets)
	for i, n := range buckets {
		if n > 0 {
			first = i
			break
		}
	}

	// See note at top about how useful these measurements are for describing
	// the issue.
	b.ReportMetric(100*float64(first)/float64(len(buckets)), "empty-percent")
	b.ReportMetric(float64(len(buckets)), "buckets")
	b.ReportMetric(float64(buckets[len(buckets)/2]), "count-p50")
	b.ReportMetric(float64(buckets[len(buckets)/10]), "count-p90")
	b.ReportMetric(float64(buckets[len(buckets)/20]), "count-p95")
}
