neoremind commented on PR #16684:
URL: https://github.com/apache/lucene/pull/16684#issuecomment-5864620351
Interesting discussion. I also want to include a test to solidify the
proposed "different fd, different VMA" approach over the current "same fd, same
VMA via clone". And indeed, "same fd, different VMA" is not possible in Lucene
today.
The test is a single Java file, each case is one run: open MMapDirectory
with advice, do cold read of a page in the middle of a large file, count what
the kernel loads.
<details>
<summary>
test code
</summary>
```
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.file.Files;
import java.nio.file.Path;
import org.apache.lucene.store.DataAccessHint;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.IndexInput;
import org.apache.lucene.store.MMapDirectory;
public class ReadAdviceCases {
static final int PAGE_SIZE = 4096;
static final long SPACING = 4096; // pages between reads = 16 MB
static long basePage;
static int numOfReads = 0;
public static void main(String[] args) throws Exception {
Path file = Path.of(args[0]).toAbsolutePath();
String name = file.getFileName().toString();
int testCase = Integer.parseInt(args[1]);
basePage = Files.size(file) / PAGE_SIZE / 4; // purposely reading starts
from 1/4 of the file
IOContext RANDOM = IOContext.DEFAULT.withHints(DataAccessHint.RANDOM);
IOContext SEQUENTIAL =
IOContext.DEFAULT.withHints(DataAccessHint.SEQUENTIAL);
switch (testCase) {
case 1 -> {
System.out.println(
"case 1: two opens (two mmaps): RANDOM then SEQUENTIAL"
+ " | read via RANDOM | expected pages: 1");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput r = mMapDir.openInput(name, RANDOM);
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
pause(testCase);
read("RANDOM", r);
}
}
case 2 -> {
System.out.println(
"case 2: two opens (two mmaps): RANDOM then SEQUENTIAL"
+ " | read via SEQUENTIAL | expected pages: 32");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput r = mMapDir.openInput(name, RANDOM);
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
pause(testCase);
read("SEQUENTIAL", s);
}
}
case 3 -> {
System.out.println(
"case 3: two opens (two mmaps): SEQUENTIAL then RANDOM"
+ " | read via SEQUENTIAL | expected pages: 32");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
IndexInput r = mMapDir.openInput(name, RANDOM);
pause(testCase);
read("SEQUENTIAL", s);
}
}
case 4 -> {
System.out.println(
"case 4: two opens (two mmaps): SEQUENTIAL then RANDOM"
+ " | read via RANDOM | expected pages: 1");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
IndexInput r = mMapDir.openInput(name, RANDOM);
pause(testCase);
read("RANDOM", r);
}
}
case 5 -> {
System.out.println(
"case 5: one mmap, clone->RANDOM then clone->SEQUENTIAL
(last)"
+ " | read via RANDOM clone | expected pages: 32
(last advice wins)");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput base = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL, no madvise
IndexInput r = base.clone();
r.updateIOContext(RANDOM);
IndexInput s = base.clone();
s.updateIOContext(SEQUENTIAL); // overwrites RANDOM on the shared
VMA
pause(testCase);
read("RANDOM-clone", r);
}
}
case 6 -> {
System.out.println(
"case 6: one mmap, clone->SEQUENTIAL then clone->RANDOM
(last)"
+ " | read via SEQUENTIAL clone | expected pages: 1
(last advice wins)");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput base = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL, no madvise
IndexInput s = base.clone();
s.updateIOContext(SEQUENTIAL);
IndexInput r = base.clone();
r.updateIOContext(RANDOM); // overwrites SEQUENTIAL on the shared
VMA
pause(testCase);
read("SEQUENTIAL-clone", s);
}
}
case 7 -> {
System.out.println(
"case 7: as case 5, then close the SEQUENTIAL clone"
+ " | read via RANDOM clone | expected pages: 32
(closing a clone re-advises nothing)");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput base = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL, no madvise
IndexInput r = base.clone();
r.updateIOContext(RANDOM);
IndexInput s = base.clone();
s.updateIOContext(SEQUENTIAL); // overwrites RANDOM on the shared
VMA
pause(testCase);
s.close();
read("RANDOM-clone", r);
}
}
case 8 -> {
System.out.println(
"case 8: as case 6, then close the RANDOM clone"
+ " | read via SEQUENTIAL clone | expected pages: 1
(closing a clone re-advises nothing)");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput base = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL, no madvise
IndexInput s = base.clone();
s.updateIOContext(SEQUENTIAL);
IndexInput r = base.clone();
r.updateIOContext(RANDOM); // overwrites SEQUENTIAL on the shared
VMA
pause(testCase);
r.close();
read("SEQUENTIAL-clone", s);
}
}
case 9 -> {
System.out.println(
"case 9: one open NORMAL (no madvise); 100 cold misses, then
5 more cold reads"
+ " | read via NORMAL | expected pages:
fault_around_bytes=65536 (default): 105 x 32 pages,"
+ " no fallback; fault_around_bytes=4096: 100 x 32
pages, then fallback to 5 x 1 after 100 misses."
+ " Events are pages/4 (8 per read-around, not 32)
if the fs uses 16 KB folios, so the perf count is"
+ " 840 = 100 x 8 + 5 x 8 with fault-around on
(default) and 805 = 100 x 8 + 5 x 1 with fault-around off");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput n = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL
pause(testCase);
readMany("NORMAL", n, 100);
for (int i = 0; i < 5; i++) {
read("NORMAL", n);
}
}
}
case 10 -> {
System.out.println(
"case 10: one open SEQUENTIAL; 100 cold misses, then 5 more
cold reads"
+ " | read via SEQUENTIAL | expected pages: phase 1
= 100 x 32 = 3200,"
+ " phase 2 = 5 x 32 = 160 (no fallback for
SEQUENTIAL), total 3360");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
pause(testCase);
readMany("SEQUENTIAL", s, 100);
for (int i = 0; i < 5; i++) {
read("SEQUENTIAL", s);
}
}
}
case 11 -> {
System.out.println(
"case 11: two opens NORMAL + SEQUENTIAL; 100 cold misses via
NORMAL, then 5 via NORMAL and 5 via"
+ " SEQUENTIAL | expected pages:
fault_around_bytes=65536: 105 x 32 + 5 x 32;"
+ " fault_around_bytes=4096: 100 x 32 + 5 x 1 + 5 x
32 (SEQUENTIAL never falls back; it does not consult mmap_miss)."
+ " Events are pages/4 for the NORMAL reads if the
fs uses 16 KB folios, so the perf count is"
+ " 1000 = 100 x 8 + 5 x 8 + 5 x 32 with
fault-around on (default) and 965 = 100 x 8 + 5 x 1 + 5 x 32 with fault-around
off");
try (MMapDirectory mMapDir = dir(file)) {
IndexInput n = mMapDir.openInput(name, IOContext.DEFAULT); //
NORMAL
IndexInput s = mMapDir.openInput(name, SEQUENTIAL);
pause(testCase);
readMany("NORMAL", n, 100);
for (int i = 0; i < 5; i++) {
read("NORMAL", n);
}
for (int i = 0; i < 5; i++) {
read("SEQUENTIAL", s);
}
}
}
default -> throw new IllegalArgumentException("case must be 1..11");
}
System.out.println("done");
}
static MMapDirectory dir(Path file) throws Exception {
MMapDirectory d = new MMapDirectory(file.getParent());
d.setReadAdvice(MMapDirectory.ADVISE_BY_CONTEXT);
d.setGroupingFunction(MMapDirectory.NO_GROUPING);
return d;
}
static void pause(int testCase) throws Exception {
pause("case " + testCase + " pid " + ProcessHandle.current().pid() + "
mapped. Attach perf, press Enter.");
}
static void pause(String message) throws Exception {
System.out.println(message);
new BufferedReader(new InputStreamReader(System.in)).readLine();
}
static void read(String advice, IndexInput input) throws Exception {
long page = nextPage(input);
input.seek(page * PAGE_SIZE);
byte b = input.readByte();
System.out.println("read #" + numOfReads + " with advice " + advice + "
at page " + page);
numOfReads++;
}
static void readMany(String advice, IndexInput in, int count) throws
Exception {
int first = numOfReads;
for (int i = 0; i < count; i++) {
in.seek(nextPage(in) * PAGE_SIZE);
byte b = in.readByte();
numOfReads++;
}
System.out.println(
"read #" + first + ".." + (numOfReads - 1) + " with advice " +
advice + " (" + count
+ " cold pages, " + (SPACING * PAGE_SIZE / 1024 / 1024)
+ " MB apart)");
}
static long nextPage(IndexInput input) {
long page = basePage + numOfReads * SPACING;
if ((page + SPACING) * PAGE_SIZE > input.length()) {
throw new IllegalStateException("file too small: read #" + numOfReads
+ " needs page " + page);
}
return page;
}
}
```
Run with
```
F=/root/bench-16G.dat
INO=$(stat -c %i $F)
CP=~/lucene/lucene/core/build/libs/lucene-core-11.0.0-SNAPSHOT.jar
javac -cp $CP -d . ReadAdviceCases.java
for N in 1 2 3 4 5 6 7 8 9 10 11; do
sync && echo 3 > /proc/sys/vm/drop_caches
echo | perf stat -e filemap:mm_filemap_add_to_page_cache --filter "i_ino
== $INO" \
java --enable-native-access=ALL-UNNAMED -cp $CP:.
com.neoremind.mylucene.ReadAdviceCases $F $N 2>&1 \
| grep -E "^case [0-9]+:|read #|filemap|Exception"
echo
done
```
</details>
I use `perf stat -e filemap:mm_filemap_add_to_page_cache --filter` to
measure the readahead loading behavior, it fires every time a page (or folio)
of file data is added into the page cache, which is exactly what we need to see
the difference between RANDOM and SEQUENTIAL hint on how much readahead happens
behind one fault.
Run on ecs.g9i.xlarge, kernel 6.6.102-8.alnx4.x86_64, ext4,
read_ahead_kb=128 ( ra_pages=32).
The result is
```
case 1: two opens (two mmaps): RANDOM then SEQUENTIAL | read via RANDOM |
expected pages: 1
1 filemap:mm_filemap_add_to_page_cache
case 2: two opens (two mmaps): RANDOM then SEQUENTIAL | read via SEQUENTIAL
| expected pages: 32
32 filemap:mm_filemap_add_to_page_cache
case 3: two opens (two mmaps): SEQUENTIAL then RANDOM | read via SEQUENTIAL
| expected pages: 32
32 filemap:mm_filemap_add_to_page_cache
case 4: two opens (two mmaps): SEQUENTIAL then RANDOM | read via RANDOM |
expected pages: 1
1 filemap:mm_filemap_add_to_page_cache
case 5: one mmap, clone->RANDOM then clone->SEQUENTIAL (last) | read via
RANDOM clone | expected pages: 32 (last advice wins)
32 filemap:mm_filemap_add_to_page_cache
case 6: one mmap, clone->SEQUENTIAL then clone->RANDOM (last) | read via
SEQUENTIAL clone | expected pages: 1 (last advice wins)
1 filemap:mm_filemap_add_to_page_cache
case 7: as case 5, then close the SEQUENTIAL clone | read via RANDOM clone |
expected pages: 32 (closing a clone re-advises nothing)
32 filemap:mm_filemap_add_to_page_cache
case 8: as case 6, then close the RANDOM clone | read via SEQUENTIAL clone |
expected pages: 1 (closing a clone re-advises nothing)
1 filemap:mm_filemap_add_to_page_cache
case 9: one open NORMAL (no madvise); 100 cold misses, then 5 more cold
reads | read via NORMAL | expected pages: fault_around_bytes=65536 (default):
105 x 32 pages, no fallback; fault_around_bytes=4096: 100 x 32 pages, then
fallback to 5 x 1 after 100 misses. Events are pages/4 (8 per read-around, not
32) if the fs uses 16 KB folios, so the perf count is 840 = 100 x 8 + 5 x 8
with fault-around on (default) and 805 = 100 x 8 + 5 x 1 with fault-around off
840 filemap:mm_filemap_add_to_page_cache
case 10: one open SEQUENTIAL; 100 cold misses, then 5 more cold reads | read
via SEQUENTIAL | expected pages: phase 1 = 100 x 32 = 3200, phase 2 = 5 x 32 =
160 (no fallback for SEQUENTIAL), total 3360
3,360 filemap:mm_filemap_add_to_page_cache
case 11: two opens NORMAL + SEQUENTIAL; 100 cold misses via NORMAL, then 5
via NORMAL and 5 via SEQUENTIAL | expected pages: fault_around_bytes=65536: 105
x 32 + 5 x 32; fault_around_bytes=4096: 100 x 32 + 5 x 1 + 5 x 32 (SEQUENTIAL
never falls back; it does not consult mmap_miss). Events are pages/4 for the
NORMAL reads if the fs uses 16 KB folios, so the perf count is 1000 = 100 x 8 +
5 x 8 + 5 x 32 with fault-around on (default) and 965 = 100 x 8 + 5 x 1 + 5 x
32 with fault-around off
1,000 filemap:mm_filemap_add_to_page_cache
```
`echo 4096 > /sys/kernel/debug/fault_around_bytes` to disable fault-around,
then cases 9-11 again:
```
case 9: one open NORMAL (no madvise); 100 cold misses, then 5 more cold
reads | read via NORMAL | expected pages: fault_around_bytes=65536 (default):
105 x 32 pages, no fallback; fault_around_bytes=4096: 100 x 32 pages, then
fallback to 5 x 1 after 100 misses. Events are pages/4 (8 per read-around, not
32) if the fs uses 16 KB folios, so the perf count is 840 = 100 x 8 + 5 x 8
with fault-around on (default) and 805 = 100 x 8 + 5 x 1 with fault-around off
805 filemap:mm_filemap_add_to_page_cache
case 10: one open SEQUENTIAL; 100 cold misses, then 5 more cold reads | read
via SEQUENTIAL | expected pages: phase 1 = 100 x 32 = 3200, phase 2 = 5 x 32 =
160 (no fallback for SEQUENTIAL), total 3360
3,360 filemap:mm_filemap_add_to_page_cache
case 11: two opens NORMAL + SEQUENTIAL; 100 cold misses via NORMAL, then 5
via NORMAL and 5 via SEQUENTIAL | expected pages: fault_around_bytes=65536: 105
x 32 + 5 x 32; fault_around_bytes=4096: 100 x 32 + 5 x 1 + 5 x 32 (SEQUENTIAL
never falls back; it does not consult mmap_miss). Events are pages/4 for the
NORMAL reads if the fs uses 16 KB folios, so the perf count is 1000 = 100 x 8 +
5 x 8 + 5 x 32 with fault-around on (default) and 965 = 100 x 8 + 5 x 1 + 5 x
32 with fault-around off
965 filemap:mm_filemap_add_to_page_cache
```
As we can see in cases 1-8, advice is a per-VMA flag. Two fds, two mappings
(cases 1-4): RANDOM reads 1 page and SEQUENTIAL reads 32, no matter the open
order. One fd, one mapping, two clones (cases 5-8): the last
`updateIOContext()` wins and decides the readahead behaviour for both clones,
and closing a clone changes nothing. That's the benefit of a separate openInput
(different fd, different VMA) that avoids, and I think it strengthens the
choice made in this PR.
--
But I want to share one finding and call out here, for the idea of "same fd,
different VMA", the concern above that "the merge's readahead window and the
mmap_miss counter would mix with the searchers'", and the Claude's summary of
"mmap_miss counter that disables readahead after ~100 misses", it's like a
weaker point. The justification is "in theory" rather than something that can
be seen in practice. Two reasons and vet in the test (I use one mapping same fd
since it can also represent two mappings with same fd as they share a struct
file).
First, mmap_miss is only incremented or consulted by an un-hinted (NORMAL)
mapping (The Claude narrative doesn't emphasis on this), In
do_sync_mmap_readahead, the VM_RAND_READ
and VM_SEQ_READ checks return before the mmap_miss++ (see kernel code
[here](https://github.com/torvalds/linux/blob/v6.6/mm/filemap.c#L3168)). So a
searcher advised RANDOM and a merge advised SEQUENTIAL cannot interfere even if
they share a struct file. Case 10 (SEQUENTIAL alone): 100 cold misses and then
5 more reads, loads total 3360 = 105 x 32 pages. Case 11 (a NORMAL open + a
SEQUENTIAL open, fault-around off): after the NORMAL mapping's 100 misses, it
collapsed to 1 page, yet the SEQUENTIAL reads still load 32 aggressive pages,
the mmap_miss counter doesn't affect SEQUENTIAL.
Second, even with two NORMAL mappings, the "~100 misses disables readahead"
is practically unreachable with the default fault_around_bytes=65536. Case 9
shows: 105 cold misses through a NORMAL mapping, and read-around ran on each
random read (840 = 105 x 8 with folios) with no degradation to 1 page after 100
misses. The reason is that a cold fault is retried, and on the retry
fault-around runs
([code](https://github.com/torvalds/linux/blob/v6.6/mm/memory.c#L4560-L4564))
and maps the 16 pages from middle, counts each of them as a hit [erasing the
misses
counter](https://github.com/torvalds/linux/blob/v6.6/mm/filemap.c#L3612-L3616).
Only by [disabling
fault-around](https://github.com/torvalds/linux/blob/v6.6/mm/memory.c#L4547)
(`echo 4096 > /sys/kernel/debug/fault_around_bytes`), case 9 drops to total 805
= 100 x 8 + 5 x 1 pages loading, read-around for the first 100 misses and
exactly the one needed page from the 101st read.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]