11package is .hail .variant
22
3+ import is .hail .ExecStrategy
34import is .hail .TestUtils ._
4- import is .hail .annotations .Region
5+ import is .hail .annotations .{Region , RowSeq }
6+ import is .hail .asm4s ._
57import is .hail .backend .ExecuteContext
8+ import is .hail .expr .ir ._
9+ import is .hail .expr .ir .TestUtils .collect
10+ import is .hail .expr .ir .defs .{Literal , TableWrite }
11+ import is .hail .types .physical .LocusRepresentations .withReference
12+ import is .hail .types .physical .PCanonicalLocus
13+ import is .hail .types .physical .stypes .interfaces .SLocusValue
14+ import is .hail .types .virtual .{TArray , TInt32 , TLocus , TString }
15+ import is .hail .variant .ContigIndexCacheSuite .{
16+ compileCanonical , custom , locusFromLiterals , storeCanonical ,
17+ }
618
719import scala .collection .immutable .ArraySeq
820
@@ -12,13 +24,6 @@ import org.scalacheck.Prop.forAll
1224
1325class ContigIndexCacheSuite {
1426
15- // Contigs that share prefixes and lengths, so that a comparison that stops early or checks only
16- // the length gives a wrong index.
17- private val custom : ReferenceGenome = {
18- val contigs = ArraySeq (" 1" , " 10" , " 11" , " chr1" , " chr10" , " chr2" , " a" , " ab" , " ba" )
19- ReferenceGenome (" contig_index_cache_test" , contigs, contigs.map(_ -> 1000 ).toMap)
20- }
21-
2227 private def genLookups (rg : ReferenceGenome ): Gen [List [String ]] =
2328 Gen .listOf(Gen .zip(Gen .oneOf(rg.contigs), Gen .choose(1 , 4 ))).map(_.flatMap {
2429 case (contig, run) => List .fill(run)(contig)
@@ -67,4 +72,109 @@ class ContigIndexCacheSuite {
6772 assertEq(lookup(" chr1" ), 3 )
6873 assertEq(lookup(" chr10" ), 4 )
6974 }
75+
76+ /* Clearing the region and storing a contig of the same length at the same address would fool a
77+ * cache keyed on the address, or on a view of the region, instead of a copy of the bytes. */
78+ @ Test def testRegionReuse (implicit ctx : ExecuteContext ): Unit = {
79+ val rg = ctx.references(ReferenceGenome .GRCh38 )
80+ val pt = PCanonicalLocus (rg)
81+ val f = compileCanonical(ctx, rg)((cb, l) => cb.memoize(l.contigIdx(cb).toL))
82+ ctx.r.pool.scopedRegion { r =>
83+ val first = storeCanonical(ctx, rg, Locus (" chr1" , 1 ), r)
84+ val firstContig = pt.representation.loadField(first, 0 )
85+ assertEq(f(first), rg.getContigIndex(" chr1" ).toLong)
86+
87+ r.clear()
88+ val second = storeCanonical(ctx, rg, Locus (" chr2" , 1 ), r)
89+ assertEq(second, first)
90+ assertEq(pt.representation.loadField(second, 0 ), firstContig)
91+ assertEq(f(second), rg.getContigIndex(" chr2" ).toLong)
92+ }
93+ }
94+
95+ @ Test def testUnknownContigInGeneratedCode (implicit ctx : ExecuteContext ): Unit = {
96+ val rg = ctx.references(ReferenceGenome .GRCh38 )
97+ val f = compileCanonical(ctx, rg)((cb, l) => cb.memoize(l.contigIdx(cb).toL))
98+ def lookup (contig : String ): Long = f(storeCanonical(ctx, rg, Locus (contig, 1 ), ctx.r))
99+
100+ assertEq(lookup(" chr1" ), 0L )
101+ // "chrZ" has the cached contig's length, "chr" is a prefix of it.
102+ for (unknown <- Seq (" chrZ" , " chrZ" , " chr" ))
103+ assertEq(outcome(lookup(unknown).toInt), outcome(rg.getContigIndex(unknown)))
104+ assertEq(lookup(" chr1" ), 0L )
105+ assertEq(lookup(" chr10" ), 9L )
106+ }
107+
108+ /* Partitions run concurrently on the local backend, each with its own instance of the generated
109+ * code. Contigs change every two rows, so lookups both hit and miss. Under unstable encodings,
110+ * writing packs each canonical locus through the cache, so a wrong index reads back as a wrong
111+ * contig. */
112+ @ Test def testConcurrentPartitions (implicit ctx : ExecuteContext ): Unit = {
113+ implicit val execStrats = ExecStrategy .lowering
114+ withReference(custom) { implicit ctx =>
115+ withUnstableEncodings { implicit ctx =>
116+ val n = 64 * 40
117+ val contigs = ArraySeq .tabulate(n)(i => custom.contigs((i / 2 ) % custom.contigs.length))
118+ val positions = ArraySeq .tabulate(n)(i => i % 997 + 1 )
119+ val table = TableRange (n, 64 ).mapRows { (_, row) =>
120+ row.get(" idx" ).bind { idx =>
121+ locusFromLiterals(contigs, positions)(idx).bind { locus =>
122+ makestruct(
123+ " idx" -> idx,
124+ " locus" -> locus,
125+ " contig_idx" -> invoke(" contig_idx" , TInt32 , locus),
126+ )
127+ }
128+ }
129+ }
130+
131+ val path = ctx.createTmpPath(" contig-index-cache-concurrency" , " ht" )
132+ assertEvalsTo(TableWrite (table, TableNativeWriter (path)), ())
133+ val expected = ArraySeq .tabulate(n) { i =>
134+ RowSeq (i, Locus (contigs(i), positions(i)), custom.getContigIndex(contigs(i)))
135+ }
136+ val read = TableIR .read(ctx.fs, ctx.references, path, requestedType = Some (table.typ))
137+ assertEvalsTo(collect(read), RowSeq (expected, RowSeq ()))
138+ }
139+ }
140+ }
141+ }
142+
143+ object ContigIndexCacheSuite {
144+
145+ // Contigs that share prefixes and lengths, so that a comparison that stops early or checks only
146+ // the length gives a wrong index.
147+ val custom : ReferenceGenome = {
148+ val contigs = ArraySeq (" 1" , " 10" , " 11" , " chr1" , " chr10" , " chr2" , " a" , " ab" , " ba" )
149+ ReferenceGenome (" contig_index_cache_test" , contigs, contigs.map(_ -> 1000 ).toMap)
150+ }
151+
152+ /** Constructs a canonical locus on [[custom ]] from the `idx`th contig and position. */
153+ def locusFromLiterals (contigs : IndexedSeq [String ], positions : IndexedSeq [Int ])(idx : IR ): IR =
154+ invoke(
155+ " Locus" ,
156+ TLocus (custom),
157+ Literal (TArray (TString ), contigs).at(idx),
158+ Literal (TArray (TInt32 ), positions).at(idx),
159+ )
160+
161+ type LocusEmitter = (EmitCodeBuilder , SLocusValue ) => Value [Long ]
162+
163+ def storeCanonical (ctx : ExecuteContext , rg : ReferenceGenome , l : Locus , r : Region ): Long =
164+ PCanonicalLocus (rg).unstagedStoreJavaObject(l, r)
165+
166+ /** Compiles `f` over a canonical locus, given its address. Each call of the result runs the same
167+ * generated instance, so a cache it holds persists across calls.
168+ */
169+ def compileCanonical (
170+ ctx : ExecuteContext ,
171+ rg : ReferenceGenome ,
172+ )(
173+ f : LocusEmitter
174+ ): AsmFunction1 [Long , Long ] = {
175+ val pt = PCanonicalLocus (rg)
176+ val fb = EmitFunctionBuilder [Long , Long ](ctx, " canonical_locus" )
177+ fb.emitWithBuilder(cb => f(cb, pt.loadCheapSCode(cb, fb.getCodeParam[Long ](1 )).asLocus))
178+ fb.resultWithIndex()(ctx.theHailClassLoader, ctx.fs, ctx.taskContext, ctx.r)
179+ }
70180}
0 commit comments