func writeGeneList(ctx context.Context, geneListOutputPath string, geneDB *fusion.GeneDB) {
	out, err := file.Create(ctx, geneListOutputPath)
	if err != nil {
		log.Panic(err)
	}
	w := bufio.NewWriter(out.Writer(ctx))
	min, limit := geneDB.GeneIDRange()
	er := errors.Once{}
	n := 0
	for id := min; id < limit; id++ {
		gene := geneDB.GeneInfo(id)
		_, err := w.WriteString(gene.Gene)
		er.Set(err)
		er.Set(w.WriteByte('\n'))
		n++
	}
	er.Set(w.Flush())
	er.Set(out.Close(ctx))
	if er.Err() != nil {
		log.Panic(err)
	}
	log.Printf("Wrote %d genes to %s", n, geneListOutputPath)
}
writeGeneList dumps names of all the genes registered in geneDB.
package cmd
import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"io"
	"math"
	"os"
	"runtime"
	"sort"
	"strings"
	"sync"
	"time"
	"github.com/grailbio/base/compress"
	"github.com/grailbio/base/errors"
	"github.com/grailbio/base/file"
	"github.com/grailbio/base/grail"
	"github.com/grailbio/base/log"
	gunsafe "github.com/grailbio/base/unsafe"
	"github.com/grailbio/base/vcontext"
	"github.com/grailbio/bio/encoding/fastq"
	"github.com/grailbio/bio/fusion"
)
type memStats struct {
	mu sync.Mutex
	alloc      uint64
	totalAlloc uint64
	sys        uint64
	heapSys    uint64
}
func (m *memStats) String() string {
	m.mu.Lock()
	defer m.mu.Unlock()
	return fmt.Sprintf("Alloc: %v TotalAlloc: %v, Sys: %v, HeapSys: %v",
		m.alloc, m.totalAlloc, m.sys, m.heapSys)
}
func (m *memStats) update() {
	var s runtime.MemStats
	runtime.ReadMemStats(&s)
	m.mu.Lock()
	if m.alloc < s.Alloc {
		m.alloc = s.Alloc
	}
	if m.totalAlloc < s.TotalAlloc {
		m.totalAlloc = s.TotalAlloc
	}
	if m.sys < s.Sys {
		m.sys = s.Sys
	}
	if m.heapSys < s.HeapSys {
		m.heapSys = s.HeapSys
	}
	m.mu.Unlock()
}
type fusionFlags struct {
	transcriptPath     string
	cosmicFusionPath   string
	r1, r2             string
	fastaOutputPath    string
	rioOutputPath      string
	rioInputPath       string
	filteredOutputPath string
	geneListInputPath  string
	geneListOutputPath string
}
func writeFASTA(out io.Writer, c fusion.Candidate, geneDB *fusion.GeneDB, opts fusion.Opts) {
	writeString := func(strings ...string) {
		for _, s := range strings {
			if _, err := out.Write(gunsafe.StringToBytes(s)); err != nil {
				log.Panic(err)
			}
		}
	}
	writeGeneLocation := func(gid fusion.GeneID) {
		gi := geneDB.GeneInfo(gid)
		if _, err := fmt.Fprintf(out, "%s:%d-%d:%d", gi.Chrom, gi.Start, gi.End, gi.Index); err != nil {
			log.Panic(err)
		}
	}
	writeReadRange := func(r fusion.CrossReadPosRange) {
		start := r.Start
		if r.Start.ReadType() == fusion.R2 {
			start = fusion.Pos(start.R2Off() + len(c.Frag.R1Seq) + 1)
		}
		end := r.End
		if end.ReadType() == fusion.R2 {
			end = fusion.Pos(end.R2Off() + len(c.Frag.R1Seq) + 1)
		}
		if _, err := fmt.Fprintf(out, "%d:%d", start, end-1  ); err != nil {
			log.Panic(err)
		}
	}
	geneOrder := fusion.CosmicOrder
	if opts.Denovo {
		geneOrder = fusion.AlphabeticalOrder
	}
	writeString(">", c.Frag.Name, "|")
	for i, fi := range c.Fusions {
		if i > 0 {
			writeString(",")
		}
		g1, g2 := fusion.SortGenePair(geneDB, fi.G1ID, fi.G2ID, geneOrder)
		writeString(geneDB.GeneInfo(g1).Gene)
		writeString("/")
		writeString(geneDB.GeneInfo(g2).Gene)
	}
	writeString("|")
	for i, fi := range c.Fusions {
		if i > 0 {
			writeString(",")
		}
		g1, g2 := fusion.SortGenePair(geneDB, fi.G1ID, fi.G2ID, geneOrder)
		writeGeneLocation(g1)
		writeString("/")
		writeGeneLocation(g2)
	}
	writeString("|")
	if _, err := fmt.Fprintf(out, "%d/%d|", c.Fusions[0].G1Span, c.Fusions[0].G2Span); err != nil {
		panic(err.Error())
	}
	writeReadRange(c.Fusions[0].G1Range)
	writeString("/")
	writeReadRange(c.Fusions[0].G2Range)
	writeString("\n")
	writeString(c.Frag.R1Seq)
	if c.Frag.R2Seq != "" {
		writeString("|", c.Frag.R2Seq)
	}
	writeString("\n")
}
func newSeq(fileseq, readseq uint) uint64 {
	return (uint64(fileseq) << 48) | uint64(readseq)
}
const invalidSeq = math.MaxUint64
type req struct {
	seq                uint64
	name, r1Seq, r2Seq string
}
type res struct {
	seq       uint64
	candidate fusion.Candidate
	stats fusion.Stats
}
func processRequests(reqCh chan req, resCh chan res, geneDB *fusion.GeneDB, opts fusion.Opts) {
	stitcher := fusion.NewStitcher(opts.KmerLength, opts.LowComplexityFraction)
	stats := fusion.Stats{}
	for req := range reqCh {
		stats.Fragments++
		name, r1Seq, r2Seq := fusion.MaybeRemoveUMI(req.name, req.r1Seq, req.r2Seq, opts)
		r1Seq, r2Seq = fusion.RemoveLowComplexityReads(r1Seq, r2Seq, &stats, opts)
		frag := stitcher.Stitch(name, r1Seq, r2Seq, &stats)
		fusions := fusion.DetectFusion(geneDB, frag, &stats, opts)
		if len(fusions) == 0 {
			stitcher.FreeFragment(frag)
			continue
		}
		resCh <- res{seq: req.seq, candidate: fusion.Candidate{frag, fusions}}
	}
	resCh <- res{seq: invalidSeq, stats: stats}
}
func readFASTQ(ctx context.Context, reqCh chan req, fileseq uint, r1Path, r2Path string) {
	var (
		sc       *fastq.PairScanner
		r1R, r2R fastq.Read
		nRead    uint
	)
	openFASTQ := func(path string) (file.File, io.ReadCloser) {
		in, err := file.Open(ctx, path)
		if err != nil {
			log.Panicf("open %v: %v", path, err)
		}
		gz, _ := compress.NewReader(in.Reader(ctx))
		return in, gz
	}
	closeFASTQ := func(in file.File, r io.ReadCloser, path string) {
		if err := r.Close(); err != nil {
			log.Panicf("close gzip %s: %v", path, err)
		}
		if err := in.Close(ctx); err != nil {
			log.Panicf("close %s: %v", path, err)
		}
	}
	in1, inr1 := openFASTQ(r1Path)
	in2, inr2 := openFASTQ(r2Path)
	sc = fastq.NewPairScanner(inr1, inr2, fastq.ID|fastq.Seq)
	for {
		if !sc.Scan(&r1R, &r2R) {
			break
		}
		nRead++
		if nRead%(1024*1024) == 0 {
			log.Printf("%s: %dMi readpairs", r1Path, nRead/(1024*1024))
		}
		id := r1R.ID
		if len(id) == 0 || id[0] != '@' {
			log.Panicf("Corrupt fastq record: %+v", r1R)
		}
		id = id[1:]
		reqCh <- req{newSeq(fileseq, nRead), id, r1R.Seq, r2R.Seq}
	}
	log.Printf("Processed %d reads in %s", nRead, r1Path)
	if err := sc.Err(); err != nil {
		log.Panicf("close pair: %v", err)
	}
	closeFASTQ(in1, inr1, r1Path)
	closeFASTQ(in2, inr2, r2Path)
}
func processFASTQ(ctx context.Context, fileseq uint,
	r1Path, r2Path string,
	geneDB *fusion.GeneDB, opts fusion.Opts) ([]res, fusion.Stats) {
	reqCh := make(chan req, 1024*64)
	resCh := make(chan res, 1024)
	wg1 := sync.WaitGroup{}
	parallelism := runtime.NumCPU()
	for i := 0; i < parallelism; i++ {
		wg1.Add(1)
		go func() {
			processRequests(reqCh, resCh, geneDB, opts)
			wg1.Done()
		}()
	}
	wg2 := sync.WaitGroup{}
	wg2.Add(1)
	var (
		results []res
		stats   fusion.Stats
	)
	go func() {
		for res := range resCh {
			if res.seq == invalidSeq {
				stats = stats.Merge(res.stats)
				continue
			}
			results = append(results, res)
		}
		wg2.Done()
	}()
	readFASTQ(ctx, reqCh, fileseq, r1Path, r2Path)
	close(reqCh)
	wg1.Wait()
	close(resCh)
	wg2.Wait()
	return results, stats
}