Skip to content

Commit 507d60b

Browse files
committed
Test the contig index cache through generated code
The cache's own suite checked the helper directly. These tests drive it through the canonical locus's generated contigIdx, where a cache could go wrong in ways a direct call does not show: - Region reuse: after the region is cleared, a different contig of the same length lands at the same address. A cache keyed on the address, or on a view of the region, returns the old index. - Unknown contigs: a failed lookup, repeated, leaves the cache giving correct indices for the previously cached contig and a new one. - Concurrency: many small partitions on the local backend, with contigs interleaved within each partition, write canonical loci under unstable encodings and read them back. Expected values come from ReferenceGenome.getContigIndex, the uncached lookup.
1 parent 46c82a8 commit 507d60b

1 file changed

Lines changed: 118 additions & 8 deletions

File tree

‎hail/hail/test/src/is/hail/variant/ContigIndexCacheSuite.scala‎

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
11
package is.hail.variant
22

3+
import is.hail.ExecStrategy
34
import is.hail.TestUtils._
4-
import is.hail.annotations.Region
5+
import is.hail.annotations.{Region, RowSeq}
6+
import is.hail.asm4s._
57
import 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

719
import scala.collection.immutable.ArraySeq
820

@@ -12,13 +24,6 @@ import org.scalacheck.Prop.forAll
1224

1325
class 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

Comments
 (0)