package main

import (
	"flag"
	"log"
	"os"
	"os/signal"
	"runtime/trace"
	"time"
)

const (
	ballastBytes      = 1000 << 20
	tickInterval      = 1 * time.Millisecond
	desiredGCInterval = 1 * time.Second
	workers           = 10
	testDuration      = 10 * time.Second

	ptrBytes = 8 // 8 bytes per pointer on amd64
)

var (
	ballast interface{}
	sink    = make([]interface{}, workers)
)

// makeBallast controls which GC pause experiment is active.
var makeBallast func() interface{} = makeMapNopointer

// makeSliceNopointer creates a large pointer-free slice.
//
// In go1.6.3 and go1.7, this has no measured effect on mutator availability.
func makeSliceNopointer() interface{} { return make([]uintptr, ballastBytes/ptrBytes) }

// makeSlicePointer creates a large slice of nil pointers.
//
// In go1.6.3 and go1.7, this leads to long periods of mutator unavailability
// (hundreds of milliseconds).
func makeSlicePointer() interface{} { return make([]*uintptr, ballastBytes/ptrBytes) }

// makeSliceLivePointer creates a large slice containing pointers to many
// pieces of live memory.
//
// In go1.6.3 and go1.7, this leads to long periods of mutator unavailability
// (around a second).
func makeSliceLivePointer() interface{} {
	b := make([]*uintptr, ballastBytes/ptrBytes/2)
	for i := range b {
		b[i] = new(uintptr)
	}
	return b
}

// makeChanPointer creates a large channel of nil pointers.
//
// In go1.6.3 and go1.7, this leads to long periods of mutator unavailability
// (hundreds of milliseconds).
func makeChanPointer() interface{} { return make(chan *uintptr, ballastBytes/ptrBytes) }

// makeChanPointer creates a large channel of nil pointers.
//
// In go1.6.3 and go1.7, this leads to long periods of mutator unavailability
// (around a second).
func makeChanLivePointer() interface{} {
	c := make(chan *uintptr, ballastBytes/ptrBytes/2)
	for i := 0; i < cap(c); i++ {
		c <- new(uintptr)
	}
	return c
}

// makeMapNopointer creates a large map with non-pointer keys and values.
//
// In go1.6.3 and go1.7, this leads to long periods of mutator unavailability
// (tens of milliseconds).
//
// Populating and churning the map may take a few minutes.
func makeMapNopointer() interface{} {
	const (
		// mapMemoryInefficiencyEstimate estimates how many bytes of memory
		// does a map require to store one additional byte of key+value
		mapMemoryInefficiencyEstimate = 2

		kvsize     = 4
		mapsize    = (ballastBytes / ptrBytes) / kvsize / mapMemoryInefficiencyEstimate
		churnTimes = 30
	)

	b := make(map[uintptr][kvsize - 1]uintptr)
	// the key size and value size must both be below 128 bytes:
	// https://github.com/golang/go/blob/go1.7/src/runtime/hashmap.go#L70-L75

	// populate and churn the map to grow (https://golang.org/issue/16070) the
	// overflow bucket list:
	// https://github.com/golang/go/blob/go1.7/src/runtime/hashmap.go#L117-L126
	for i := 0; i < mapsize*(1+churnTimes); i++ {
		b[uintptr(i)] = [kvsize - 1]uintptr{}
		delete(b, uintptr(i-mapsize))
	}
	return b
}

func main() {
	tname := flag.String("trace", "", "Execution trace file name")
	exp := flag.String("experiment", "",
		"Name of experiment (slice-nopointer, slice-pointer, slice-livepointer, map-nopointer, chan-pointer, chan-livepointer)")
	flag.Parse()

	if *tname == "" {
		log.Fatalf("The -trace flag must be used to set the execution trace file destination")
	}

	switch *exp {
	case "slice-nopointer":
		makeBallast = makeSliceNopointer
	case "slice-pointer":
		makeBallast = makeSlicePointer
	case "slice-livepointer":
		makeBallast = makeSliceLivePointer
	case "map-nopointer":
		makeBallast = makeMapNopointer
	case "chan-pointer":
		makeBallast = makeChanPointer
	case "chan-livepointer":
		makeBallast = makeChanLivePointer
	case "":
	default:
		log.Fatalf("Unknown -experiment %q", *exp)
	}

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, os.Interrupt)

	tfile, err := os.Create(*tname)
	if err != nil {
		log.Fatalf("trace open: %v", err)
	}
	defer func() {
		err := tfile.Close()
		if err != nil {
			log.Fatalf("trace close: %v", err)
		}
	}()

	err = trace.Start(tfile)
	if err != nil {
		log.Fatalf("trace start: %v", err)
	}
	defer trace.Stop()

	ballast = makeBallast()

	ticker := time.NewTicker(tickInterval)
	defer ticker.Stop()

	for i := 0; i < workers; i++ {
		go work(ticker.C, i)
	}

	select {
	case <-sigs:
	case <-time.After(testDuration):
	}
}

func work(ch <-chan time.Time, i int) {
	for t := range ch {
		d := time.Since(t)
		if d > 1*time.Millisecond {
			log.Printf("long pause: %s", d)
		}

		sink[i] = make([]byte, ballastBytes/(desiredGCInterval/tickInterval))
	}
}
