package main

import (
	"bufio"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"log"
	"os"
	"runtime/pprof"
	"sort"
	"strings"
	"time"

	"code.justin.tv/rhys/nursery/internal/_vendor/trace"
)

func printStackGoroutines(result trace.ParseResult, fn string) {
	th := &thing{}
	th.prepareGoroutines(result.Events)

	var gsl []uint64
	for g := range th.Goroutines {
		gsl = append(gsl, g)
	}
	sort.Slice(gsl, func(i, j int) bool { return gsl[i] < gsl[j] })

	var ids []uint64
	for id := range result.Stacks {
		ids = append(ids, id)
	}
	sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })

	for _, id := range ids {
		stk := result.Stacks[id]
		if !hasFrameExact(stk, fn) {
			continue
		}
		ng := 0
		for _, g := range gsl {
			nev := 0
			evs := th.Goroutines[g]
			for _, ev := range evs {
				if ev.StkID == id {
					nev++
				}
			}
			if nev > 0 {
				if ng == 0 {
					fmt.Printf("---\n")
					for _, frame := range stk {
						fmt.Printf("  %x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
					}
				}
				fmt.Printf("g=%d n=%d\n", g, nev)
				ng++
			}
		}
	}
}

func printGoroutine(result trace.ParseResult, g uint64) {
	th := &thing{}
	th.prepareGoroutines(result.Events)

	evs := th.Goroutines[g]

	for _, ev := range evs {
		fmt.Printf("%s", eventString(ev, true))
	}
}

func printRegions(result trace.ParseResult, regions []*region) {
	th := &thing{}
	th.prepareGoroutines(result.Events)
	th.prepareLocalRegions(regions)

	for _, r := range regions {
		first := r.Events[0]
		last := r.Events[len(r.Events)-1]
		fmt.Printf("kind=%q g=%d tstart=%d tend=%d delta=%d\n", r.Kind, first.G, first.Ts, last.Ts, last.Ts-first.Ts)
	}
}

// When printing transactions, we want to know:
//
// The name of the inbound call ("server/" region), and the names of the
// outbound calls ("client/" regions). (We maybe don't need to see how many of
// each there were.)
//
// The duration of the inbound call, its start time, its goroutine id.
//
// Across all goroutines we marked as participating in the inbound call, the
// total amount of processing time those goroutines were scheduled to have.
//
// Across all outbound calls, the amount of wall time during which none were
// active.
//
// Across all outbound calls, the "waterfall generation count".
//
// Across all scheduled processing time for involved goroutines and all outbound
// calls, the amount of wall time during which the service made no forward
// movement on the inbound call. (This is scheduler delay, waiting on locks for
// resources that were shared with other inbound calls, and similar lost time.)
//
// If there are runtime/trace Tasks or Regions, we should probably include those
// somehow too. They may include a conflicting view of which inbound call led to
// additional work.
//
// We'll also want a detailed view of which goroutines were involved in each
// outbound call, when the outbound calls started and how long they lasted, when
// processing time started on each goroutine and how long it lasted. (This
// information should allow calculating the above summaries. If not, add the
// missing detail here.)

type transactionReport struct {
	Kind          string
	Goroutine     uint64
	StartNanos    int64
	DurationNanos int64

	CallRegions      []*transactionRegion
	ExecutionRegions []*transactionRegion

	Summary *transactionSummary
}

type transactionRegion struct {
	Kind          string
	Goroutine     uint64
	StartNanos    int64
	DurationNanos int64
}

type transactionSummary struct {
	TotalProcessingNanos     int64
	SelfWallNanos            int64
	SelfIdleNanos            int64
	WaterfallGenerationCount int
}

func followComms(result trace.ParseResult, regions []*region) {
	th := &thing{}
	th.prepareGoroutines(result.Events)
	th.prepareLocalRegions(regions)

	// Step through all events in chronological order.
	//
	// When a region starts in a goroutine, update the stack of regions that are
	// active on that goroutine.
	//
	// When an event links across goroutines, make a note of the regions that
	// were active on the source goroutine at the time of the source event. When
	// the time comes to process the destination event, update the stack of
	// regions that are active on the destination goroutine.
	//
	// The rules for updating region stacks are as follows:
	//
	// When a new region's Kind starts with "server/", clear the existing region
	// stack. If the existing region stack included a "server/" region already,
	// make a note that the goroutine is shared across requests. (Right?)
	//
	// An active region from the current goroutine cannot be unseated by one
	// from a linked event.
	//
	// When a new region's Kind starts with "client/", add it to the region
	// stack.
	//
	// (Does it make sense for region stacks to merge at all, or should one
	// always win? What does it mean for a stack to exist without a "server/"
	// root, and what does it mean for such a stack to appear inbound to a
	// goroutine that already has a region stack?)

	gr := make(map[uint64][]*region)
	via := make(map[uint64][]*trace.Event)
	inbounds := make(map[*trace.Event][]*region)
	inboundVia := make(map[*trace.Event][]*trace.Event)
	localStarts := make(map[*trace.Event][]*region)
	localEnds := make(map[*trace.Event][]*region)
	strRegions := make(map[string][]*region)
	children := make(map[string]map[*region]struct{})

	var txs []*transactionReport

	type exec struct {
		self  uint64
		prev  uint64
		start int64
		end   int64
	}
	execs := make(map[*region][]*exec)

	if false {
		links := make(map[[2]uint64]int)
		for us, them := range th.BackLinks {
			if us.G == them.G {
				continue
			}
			pair := [2]uint64{them.StkID, us.StkID}
			links[pair]++
		}
		for pair, count := range links {
			fmt.Printf("src=%d dst=%d n=%d\n", pair[0], pair[1], count)
		}

		return
	}

	for _, r := range regions {
		first, last := r.Events[0], r.Events[len(r.Events)-1]
		localStarts[first] = append(localStarts[first], r)
		localEnds[last] = append(localEnds[last], r)
	}

	for _, ev := range result.Events {
		var (
			inbound = inbounds[ev]
			inVia   = inboundVia[ev]
			hereVia = via[ev.G]
			start   = localStarts[ev]
			end     = localEnds[ev]
			current = gr[ev.G]
		)

		stackString := func(rs []*region) string {
			var kinds []string
			for _, r := range rs {
				kinds = append(kinds, fmt.Sprintf("%q@%p+%d", r.Kind, r, r.Events[0].StkID))
			}
			return strings.Join(kinds, " ")
		}

		ptrString := func(rs []*region) string {
			var parts []string
			for _, r := range rs {
				parts = append(parts, fmt.Sprintf("%p", r))
			}
			return strings.Join(parts, ";")
		}

		logged := false

		var next []*region
		next = append(next, current...)
		current = next

		// This event is the end of a region; remove that tag from the current
		// goroutine.
		if len(end) > 0 {
			next = nil
			remove := make(map[*region]struct{})
			for _, r := range end {
				remove[r] = struct{}{}
			}
			for _, r := range current {
				if _, ok := remove[r]; !ok {
					next = append(next, r)
				}
			}
			current = next

			// fmt.Printf("g=%d end %s\n", ev.G, stackString(end))
			// logged = true
		}

		// Check if this event marks the start of a region, or if this goroutine
		// is otherwise executing its own region, since that can affect how it
		// accepts region tags from inbound events.
		haveLocal := len(start) > 0
		for _, r := range current {
			if r.Events[0].G == ev.G {
				haveLocal = true
				break
			}
		}

		if len(inbound) > 0 && !haveLocal {
			next = current
			for _, r := range inbound {
				if strings.HasPrefix(r.Kind, "server/") {
					next = nil
				}
				next = append(next, r)
			}
			current = next

			if len(inVia) > 0 {
				via[ev.G] = inVia
			}

			them := th.BackLinks[ev]

			if false {
				fmt.Printf("g=%d inbound %s\n", ev.G, stackString(inbound))
				fmt.Printf("us   %q\n", ev.String())
				fmt.Printf("them %q\n", them.String())
				for _, frame := range them.Stk {
					fmt.Printf("them %#x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
				}
				for _, frame := range ev.Stk {
					fmt.Printf("us   %#x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
				}
				logged = true
			}
		}

		if len(start) > 0 {
			next = current
			for _, r := range start {
				if strings.HasPrefix(r.Kind, "server/") {
					next = nil
				}
				next = append(next, r)
			}
			current = next

			if false && len(current) >= 2 {
				fmt.Printf("g=%d start %s\n", ev.G, stackString(start))
				for _, link := range hereVia {
					fmt.Printf("via %q@%d\n", link.String(), link.StkID)
				}
				for _, frame := range ev.Stk {
					fmt.Printf("%x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
				}
				logged = true
			}
		}

		gr[ev.G] = current
		if len(current) > 0 {
			child := current[len(current)-1]
			parents := current[:len(current)-1]
			str := ptrString(parents)
			strRegions[str] = parents
			cm, ok := children[str]
			if !ok {
				cm = make(map[*region]struct{})
				children[str] = cm
			}
			cm[child] = struct{}{}
		}

		if len(current) > 0 {
			root := current[0]
			if strings.HasPrefix(root.Kind, "server/") {
				if len(start) > 0 {
				}
				if len(inbound) > 0 {
					e := &exec{
						self:  ev.G,
						start: ev.Ts,
					}
					if l := ev.Link; l != nil {
						e.end = l.Ts
					}
					if prev := th.BackLinks[ev]; prev != nil {
						e.prev = prev.G
					}
					execs[root] = append(execs[root], e)
				}
			}
		}

		if dest := ev.Link; dest != nil && dest.G != ev.G && dest.G != 0 {
			apply := false
			if len(current) >= 1 && current[0].Kind == "server/http" {
				if ev.Type == trace.EvGoCreate && dest.Type == trace.EvGoStart {
					// This goroutine created another. Apply the tags from this
					// one (ev) to that one (dest).
					apply = true
				} else if ev.Type == trace.EvGoUnblock && dest.Type == trace.EvGoStart {
					// This goroutine unblocked another. Apply the tags from this
					// one (ev) to that one (dest).
					apply = true
				} else if ev.Type == trace.EvGoBlockNet && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on the network. The other
					// goroutine unblocked this one. It might be appropriate to
					// apply the tags from that one (dest) to this one (ev),
					// though we won't know which tags it has until we get to
					// that point in the timeline. Ignore for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockSelect && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on a channel select
					// operation. The other goroutine unblocked this one. It
					// might be appropriate to apply the tags from that one
					// (dest) to this one (ev), though we won't know which tags
					// it has until we get to that point in the timeline. Ignore
					// for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockRecv && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on a channel receive
					// operation. The other goroutine unblocked this one. It
					// might be appropriate to apply the tags from that one
					// (dest) to this one (ev), though we won't know which tags
					// it has until we get to that point in the timeline. Ignore
					// for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockSend && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on a channel send
					// operation. The other goroutine unblocked this one. It
					// might be appropriate to apply the tags from that one
					// (dest) to this one (ev), though we won't know which tags
					// it has until we get to that point in the timeline. Ignore
					// for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockSync && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on a synchronization
					// primitive. The other goroutine unblocked this one. It
					// might be appropriate to apply the tags from that one
					// (dest) to this one (ev), though we won't know which tags
					// it has until we get to that point in the timeline. Ignore
					// for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockCond && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on a condition variable
					// primitive. The other goroutine unblocked this one. It
					// might be appropriate to apply the tags from that one
					// (dest) to this one (ev), though we won't know which tags
					// it has until we get to that point in the timeline. Ignore
					// for now.
					//
					// TODO: make note of this back-edge in the right part of
					// the timeline.
				} else if ev.Type == trace.EvGoBlockGC && dest.Type == trace.EvGoUnblock {
					// This goroutine blocked waiting on GC assist credit. The
					// other goroutine unblocked this one. There's no relation
					// between the goroutines in the application code: only that
					// they both happen to allocate memory.
				} else if ev.Type == trace.EvGoBlock && dest.Type == trace.EvGoUnblock &&
					ev.Stk == nil && dest.Stk == nil {
					// This is something related to the GC, wherein this
					// goroutine unblocks a dedicated GC worker goroutine.
				} else {
					fmt.Printf("ev   %q\n", ev.String())
					fmt.Printf("dest %q\n", dest.String())
					for _, frame := range dest.Stk {
						fmt.Printf("dest %x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
					}
					for _, frame := range ev.Stk {
						fmt.Printf("ev   %x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
					}
					logged = true
				}
			}
			if apply {
				inbounds[dest] = current
				inboundVia[dest] = make([]*trace.Event, 0, len(hereVia)+1)
				inboundVia[dest] = append(inboundVia[dest], hereVia...)
				inboundVia[dest] = append(inboundVia[dest], ev)
			}
		}

		if logged {
			fmt.Printf("ts=%d g=%d current %s\n", ev.Ts, ev.G, stackString(current))
			fmt.Printf("---\n")
		}
	}

	for str, ch := range children {
		p := strRegions[str]
		for _, r := range p {
			start := r.Events[0]
			end := r.Events[len(r.Events)-1]
			fmt.Printf("parent=%q %p g=%d start=%d end=%d\n", r.Kind, r, start.G, start.Ts, end.Ts)
		}
		var chs []*region
		for r := range ch {
			chs = append(chs, r)
		}
		sort.Slice(chs, func(i, j int) bool { return chs[i].Events[0].Ts < chs[j].Events[0].Ts })
		for _, r := range chs {
			start := r.Events[0]
			end := r.Events[len(r.Events)-1]
			fmt.Printf("child=%q %p g=%d start=%d end=%d\n", r.Kind, r, start.G, start.Ts, end.Ts)
		}
		fmt.Printf("---\n")

		if len(p) != 1 {
			continue
		}

		tx := &transactionReport{
			Kind:          p[0].Kind,
			Goroutine:     p[0].Events[0].G,
			StartNanos:    p[0].Events[0].Ts,
			DurationNanos: p[0].Events[len(p[0].Events)-1].Ts - p[0].Events[0].Ts,

			Summary: &transactionSummary{},
		}

		es := execs[p[0]]
		for _, e := range es {
			if e.end == 0 {
				continue
			}
			// tx.ExecutionRegions = append(tx.ExecutionRegions, &transactionRegion{
			// 	Kind:          fmt.Sprintf("prev:%d", e.prev),
			// 	Goroutine:     e.self,
			// 	StartNanos:    e.start,
			// 	DurationNanos: e.end - e.start,
			// })
		}

		for r := range ch {
			start := r.Events[0]
			end := r.Events[len(r.Events)-1]
			cr := &transactionRegion{
				Kind:          r.Kind,
				Goroutine:     start.G,
				StartNanos:    start.Ts,
				DurationNanos: end.Ts - start.Ts,
			}
			tx.CallRegions = append(tx.CallRegions, cr)
		}
		sort.Slice(tx.CallRegions, func(i, j int) bool {
			ri, rj := tx.CallRegions[i], tx.CallRegions[j]
			if ri.StartNanos != rj.StartNanos {
				return ri.StartNanos < rj.StartNanos
			}
			if ri.DurationNanos != rj.DurationNanos {
				return ri.DurationNanos < rj.DurationNanos
			}
			if ri.Kind != rj.Kind {
				return ri.Kind < rj.Kind
			}
			return ri.Goroutine < rj.Goroutine
		})

		txOut := make([][2]int64, len(tx.CallRegions))
		for i := range tx.CallRegions {
			r := tx.CallRegions[i]
			txOut[i] = [2]int64{r.StartNanos, r.StartNanos + r.DurationNanos}
		}
		txIn := [2]int64{tx.StartNanos, tx.StartNanos + tx.DurationNanos}
		tx.Summary.SelfWallNanos = uncovered(txIn, txOut...)
		tx.Summary.WaterfallGenerationCount = waterfall(txIn, txOut...)

		for _, e := range tx.ExecutionRegions {
			tx.Summary.TotalProcessingNanos += e.DurationNanos
		}

		txs = append(txs, tx)
	}

	sort.Slice(txs, func(i, j int) bool {
		txi, txj := txs[i], txs[j]
		if txi.StartNanos != txj.StartNanos {
			return txi.StartNanos < txj.StartNanos
		}
		if txi.DurationNanos != txj.DurationNanos {
			return txi.DurationNanos < txj.DurationNanos
		}
		if txi.Kind != txj.Kind {
			return txi.Kind < txj.Kind
		}
		return txi.Goroutine < txj.Goroutine
	})

	fmt.Printf("[")
	for i, tx := range txs {
		j, err := json.Marshal(tx)
		if err != nil {
			log.Printf("json.Marshal; err = %v", err)
			continue
		}
		if i > 0 {
			fmt.Printf(",")
		}
		fmt.Printf("\n%s", j)
	}
	fmt.Printf("\n]\n")
}

func main() {
	comms := flag.Bool("follow-comms", false, "")
	dumpRegions := flag.Bool("dump-regions", false, "")
	stackCounts := flag.String("stack-counts", "", "")
	stacks := flag.Bool("stacks", false, "")
	goroutine := flag.Uint64("goroutine", 0, "")
	cpuprofile := flag.String("cpuprofile", "", "Write a CPU profile to the specified file before exiting.")
	flag.Parse()

	if *cpuprofile != "" {
		cpu, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatalf("cpuprofile os.Create(%q); err = %v", *cpuprofile, err)
		}
		err = pprof.StartCPUProfile(cpu)
		if err != nil {
			log.Fatalf("cpuprofile pprof.StartCPUProfile; err = %v", err)
		}
		defer pprof.StopCPUProfile()
	}

	r := io.Reader(bufio.NewReader(os.Stdin))
	result, err := trace.Parse(r, "")
	if err != nil {
		log.Fatalf("Parse; err = %v", err)
	}

	// for i := 0; i < 10; i++ {
	// 	fmt.Print(eventString(result.Events[10000+i], true))
	// }

	// return

	sort.Slice(result.Events, func(i, j int) bool {
		ei, ej := result.Events[i], result.Events[j]
		return ei.Ts < ej.Ts
	})

	gs := make(map[uint64][]*trace.Event)
	for _, ev := range result.Events {
		gs[ev.G] = append(gs[ev.G], ev)
	}
	var gsl []uint64
	for g := range gs {
		gsl = append(gsl, g)
	}
	sort.Slice(gsl, func(i, j int) bool { return gsl[i] < gsl[j] })
	for _, g := range gsl {
		evs := gs[g]
		sort.Slice(evs, func(i, j int) bool { return evs[i].Ts < evs[j].Ts })
	}

	if fn := *stackCounts; fn != "" {
		printStackGoroutines(result, fn)
		return
	}

	if *stacks {
		var ids []uint64
		for id := range result.Stacks {
			ids = append(ids, id)
		}
		sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
		for _, id := range ids {
			stack := result.Stacks[id]
			fmt.Printf("StkID=%d\n", id)
			for _, frame := range stack {
				fmt.Printf("  %x %s %s:%d\n", frame.PC, frame.Fn, frame.File, frame.Line)
			}
			fmt.Printf("---\n")
		}
		return
	}

	if g := *goroutine; g != 0 {
		printGoroutine(result, g)
		return
	}

	// evs := gs[1145]

	var regions []*region

	for _, g := range gsl {
		if g != 6345 {
			// continue
		}

		evs := gs[g]

		t := &track{
			verbose: false,

			flushInbound: func(evs []*trace.Event) {
				regions = append(regions, &region{Kind: "server/http", Events: evs})
				return
				if len(evs) >= 2 {
					start := evs[0]
					end := evs[len(evs)-1]

					delta := time.Duration(end.Ts-start.Ts) * time.Nanosecond
					log.Printf("doneeeee ts=%d g=%d delta=%d evs=%d", start.Ts, start.G,
						delta.Microseconds(), len(evs))
				}
			},
		}

		state := t.inboundRequestPending
		for _, ev := range evs {
			if state != nil {
				state = state(ev)
			}
		}
		if len(t.queue) > 0 {
			// should we flush partial results? or mark the events involved as duds?
			if false {
				t.flushInbound(t.queue)
			}
		}

		{
			t := &mutexTracker{
				verbose: false,

				flush: func(evs []*trace.Event) {
					collapsed := new(strings.Builder)
					fmt.Fprintf(collapsed, "internal/mutex")
					for _, frame := range evs[0].Stk {
						fmt.Fprintf(collapsed, ";%s", frame.Fn)
					}
					regions = append(regions, &region{Kind: collapsed.String(), Events: evs})
				},
			}
			state := t.idle
			for _, ev := range evs {
				if state != nil {
					state = state(ev)
				}
			}
		}

		{
			rt := &redisTracker{
				verbose: false,

				flush: func(evs []*trace.Event) {
					regions = append(regions, &region{Kind: "client/redis", Events: evs})
					return
					if len(evs) >= 2 {
						start := evs[0]
						end := evs[len(evs)-1]

						delta := time.Duration(end.Ts-start.Ts) * time.Nanosecond
						log.Printf("redisdoneeeee ts=%d g=%d delta=%d evs=%d", start.Ts, start.G,
							delta.Microseconds(), len(evs))
					}
				},
			}
			state := rt.idle
			for _, ev := range evs {
				if state != nil {
					state = state(ev)
				}
			}
		}

		{
			t := &libpqTracker{
				verbose: false,

				flush: func(evs []*trace.Event) {
					regions = append(regions, &region{Kind: "client/postgres", Events: evs})
				},
			}
			state := t.idle
			for _, ev := range evs {
				if state != nil {
					state = state(ev)
				}
			}
		}

		{
			t := &httpClientTracker{
				verbose: false,

				flush: func(evs []*trace.Event) {
					regions = append(regions, &region{Kind: "client/http", Events: evs})
				},
			}
			state := t.idle
			for _, ev := range evs {
				if state != nil {
					state = state(ev)
				}
			}
		}

		{
			t := &daxTracker{
				verbose: false,

				flush: func(evs []*trace.Event) {
					regions = append(regions, &region{Kind: "client/dax", Events: evs})
				},
			}
			state := t.idle
			for _, ev := range evs {
				if state != nil {
					state = state(ev)
				}
			}
		}

		continue

		var candidateReqStartEv *trace.Event
		var confirmedReqStartEv *trace.Event
		for _, ev := range evs {
			log.Printf("---")
			stk := ev.Stk
			if ev.Type == trace.EvGoStart {

			}
			if doesReadHTTPRequest(stk) {
				log.Printf("starting soon")
				if candidateReqStartEv == nil {
					candidateReqStartEv = ev
				}
				if confirmedReqStartEv != nil {
					delta := time.Duration(ev.Ts-confirmedReqStartEv.Ts) * time.Nanosecond
					log.Printf("finished request ts=%d g=%d duration=%dµs", confirmedReqStartEv.Ts, ev.G, delta.Microseconds())
					confirmedReqStartEv = nil
				}
			} else {
				switch ev.Type {
				case trace.EvNone:
				case trace.EvHeapAlloc:
				case trace.EvGoStart:
				default:
					if candidateReqStartEv != nil {
						log.Printf("last event started the request")
						confirmedReqStartEv = candidateReqStartEv
						candidateReqStartEv = nil
					}
				}
			}
			log.Printf("ev %v", ev)
			if stk != nil {
				for _, frame := range stk {
					log.Printf("        Frame %s:%d", frame.Fn, frame.Line)
				}
			}
		}
	}

	if *dumpRegions {
		printRegions(result, regions)
		return
	}

	if *comms {
		followComms(result, regions)
		return
	}

	claimedEvents := make(map[*trace.Event]struct{})
	for _, r := range regions {
		for _, ev := range r.Events {
			claimedEvents[ev] = struct{}{}
		}
	}

	unclaimed := 0
	for _, ev := range result.Events {
		_, ok := claimedEvents[ev]
		if !ok {
			unclaimed++
		}
	}
	log.Printf("total=%d claimed=%d unclaimed=%d", len(result.Events), len(claimedEvents), unclaimed)

	regionKindMap := make(map[string][]*region)
	for _, r := range regions {
		regionKindMap[r.Kind] = append(regionKindMap[r.Kind], r)
	}
	var regionKinds []string
	for kind := range regionKindMap {
		regionKinds = append(regionKinds, kind)
	}
	sort.Strings(regionKinds)
	for _, kind := range regionKinds {
		log.Printf("kind=%q count=%d", kind, len(regionKindMap[kind]))
	}

	{
		i := 0
		for _, r := range regionKindMap["server/http"] {
			for _, ev := range r.Events {
				if ev.Link != nil && ev.G != ev.Link.G {
					if i < 4 {
						log.Printf("ev   %s", ev)
						log.Printf("link %s", ev.Link)
					}
					i++
				}
			}
		}
	}

	th := &thing{}
	th.prepareGoroutines(result.Events)
	th.prepareLocalRegions(regions)
	// for i, r := range regionKindMap["client/redis"] {
	// 	rs := th.findCauseRegions(r.Events[0])
	// 	if len(rs) < 2 {
	// 		log.Printf("i=%d %s", i, rs)
	// 		break
	// 	}
	// }
	// rs := th.findCauseRegions(regionKindMap["client/redis"][827].Events[0])
	// log.Printf("regions %q", rs)

	for _, r := range regions {
		first := r.Events[0]
		last := r.Events[len(r.Events)-1]
		log.Printf("kind=%q g=%d tstart=%d tend=%d delta=%d", r.Kind, first.G, first.Ts, last.Ts, last.Ts-first.Ts)
	}

	for _, ev := range result.Events {
		if ev.Link != nil {
			src, dest := ev, ev.Link
			destg := fmt.Sprintf("%d", dest.G)
			if dest.G == src.G {
				destg = "same"
			}
			log.Printf("srcg=%d srcev=%q destg=%s destev=%q srct=%d destt=%d delta=%d",
				src.G, trace.EventDescriptions[ev.Type].Name, destg, trace.EventDescriptions[dest.Type].Name,
				src.Ts, dest.Ts, dest.Ts-src.Ts)
		}
	}

	return

	{
		i := 0
		for _, ev := range result.Events {
			if ev.Type == trace.EvGoSysCall {
				if hasFrameExact(ev.Stk, "syscall.Read") {

					local := th.LocalRegions[ev]
					if len(local) > 0 {
						continue
					}

					if i < 40 {
						log.Printf("write")
						for _, r := range local {
							log.Printf("  region %q", r.Kind)
						}
						for _, frame := range ev.Stk {
							log.Printf("    frame %q", frame.Fn)
						}
					}
					i++
				}
			}
		}
	}

	// th.LocalRegions

	return

	for _, g := range gsl {
		evs := gs[g]
		more := ""

		for _, ev := range evs {
			if doesRoundTrip(ev.Stk) {
				more += " RoundTrip"
			}
		}

		// log.Printf("g=%d n=%d%s", g, len(evs), more)
	}

	return

	i := 0
	for _, ev := range result.Events {
		if i > 400 {
			break
		}
		print := false
		// if ev.Type == trace.EvGoUnblock && doesRoundTrip(result.Stacks(ev.StkID)) {
		// 	print = true
		// }
		if ev.G == 3021465 {
			print = true
		}
		// if ev.Type == trace.EvGoSysExit {
		// 	print = true
		// }
		if print {
			i++
			log.Printf("---")
			log.Printf("ev %v", ev)
			// log.Printf("    g=%d/%d p=%d/%d delta=%d", ev.G, ev.Link.G, ev.P, ev.Link.P, ev.Link.Ts-ev.Ts)
			// log.Printf("    Ts=%d StkID=%d Link=%v", ev.Ts, ev.StkID, ev.Link)
			stk := result.Stacks[ev.StkID]
			if stk != nil {
				for _, frame := range stk {
					log.Printf("        Frame %s:%d", frame.Fn, frame.Line)
				}
			}
			if ev.Link != nil {
				log.Printf("ln %v", ev.Link)
				stk := result.Stacks[ev.Link.StkID]
				if stk != nil {
					for _, frame := range stk {
						log.Printf("        Frame %s:%d", frame.Fn, frame.Line)
					}
				}
			}
		}
	}
}

type thing struct {
	// Goroutines maps goroutine ID to the trace events observed on that
	// goroutine, ordered by timestamp and then by file offset.
	Goroutines map[uint64][]*trace.Event
	// LocalRegions maps a trace event to the regions within the goroutine that
	// were active at that time.
	LocalRegions map[*trace.Event][]*region
	// BackLinks maps a trace event to another which has the first as its Link
	// value.
	BackLinks map[*trace.Event]*trace.Event
}

func (t *thing) prepareGoroutines(evs []*trace.Event) {
	t.Goroutines = make(map[uint64][]*trace.Event)
	t.BackLinks = make(map[*trace.Event]*trace.Event)
	for _, ev := range evs {
		t.Goroutines[ev.G] = append(t.Goroutines[ev.G], ev)
		if ev.Link != nil {
			if other := t.BackLinks[ev.Link]; other != nil {
				panic(fmt.Sprintf("two events link to the same destination %q: %q %q", ev.Link, other, ev))
			}
			t.BackLinks[ev.Link] = ev
		}
	}
	for _, evs := range t.Goroutines {
		sort.Slice(evs, func(i, j int) bool {
			// Sort first by event timestamp, then file offset
			evi, evj := evs[i], evs[j]
			if evi.Off == evj.Off {
				panic(fmt.Sprintf("two events share one offset: %q %q", evi, evj))
			}
			if evi.Ts != evj.Ts {
				return evi.Ts < evj.Ts
			}
			return evs[i].Off < evs[j].Off
		})
	}
}

func (t *thing) prepareLocalRegions(regions []*region) {
	t.LocalRegions = make(map[*trace.Event][]*region)
	byGoroutine := make(map[uint64][]*region)
	for _, r := range regions {
		if len(r.Events) > 0 {
			byGoroutine[r.Events[0].G] = append(byGoroutine[r.Events[0].G], r)
		}
	}
	for _, rs := range byGoroutine {
		sort.Slice(rs, func(i, j int) bool {
			// Sort first by event timestamp, then file offset, to match other sorts
			ri, rj := rs[i], rs[j]
			if tsi, tsj := ri.Events[0].Ts, rj.Events[0].Ts; tsi != tsj {
				return tsi < tsj
			}
			if offi, offj := ri.Events[0].Off, rj.Events[0].Off; offi != offj {
				return offi < offj
			}
			// Break ties, in case we detect one event as the start of two regions
			return ri.Kind < rj.Kind
		})
		for _, r := range rs {
			for _, ev := range r.Events {
				t.LocalRegions[ev] = append(t.LocalRegions[ev], r)
			}
		}
	}
}

func (t *thing) findCauseRegions(ev *trace.Event) []*region {
	log.Printf("g=%d ts=%d", ev.G, ev.Ts)
	evs := t.Goroutines[ev.G]
	for found, i := false, len(evs)-1; i >= 0; i-- {
		if evs[i].Ts == ev.Ts && evs[i].Off == ev.Off {
			found = true
		}
		if !found {
			continue
		}
		// is evs[i] a link destination? is it the start of a region?
		local := t.LocalRegions[evs[i]]
		if ev == evs[i] {
			// print only the first one
			log.Printf("local %v", local)
		}
		prev := t.BackLinks[evs[i]]
		if prev != nil {
			log.Printf("following backlink\n  %s\n  %s\n", evs[i], prev)
			return t.findCauseRegions(prev)
		}
		// return local
	}
	return nil
}
