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]

Reply via email to