Skip to content

Finish port of WindowsDirectory and NativeUnixDirectory, #1342#1346

Open
paulirwin wants to merge 4 commits into
apache:masterfrom
paulirwin:issue/1342
Open

Finish port of WindowsDirectory and NativeUnixDirectory, #1342#1346
paulirwin wants to merge 4 commits into
apache:masterfrom
paulirwin:issue/1342

Conversation

@paulirwin

Copy link
Copy Markdown
Contributor
  • You've read the Contributor Guide and Code of Conduct.
  • You've included unit or integration tests for your change, where applicable.
  • You've included inline docs for your change, where applicable.
  • There's an open issue for the PR that you are making. If you'd like to propose a change, please open an issue to discuss the change or find an existing issue.

Finish port of WindowsDirectory and NativeUnixDirectory

Fixes #1342

Description

This PR finishes the work that was started on WindowsDirectory and NativeUnixDirectory.

Upstream, this code is a mix of Java and C++. Instead of adding in true native code, I finished the approach that was started of using P/Invoke to call the Win32/POSIX native functions.

This PR also adds tests for these types, which had no tests upstream. These tests all pass on Windows 11 x64, Ubuntu 24.04 x64 in WSL, Ubuntu 24.04 arm64 in Docker, and macOS 26 arm64.

Benchmark Results

These benchmarks are on macOS 26 arm64:

Dir (64 MB) Write ReadSequential ReadRandom
NativeUnix 19.1 ms 47.8 ms 380.6 ms
SimpleFS 149 ms 3.5 ms 1.0 ms
NIOFS 137 ms 5.4 ms 1.1 ms
MMap 139 ms 618 ms 5.6 ms
RAM 148 ms 1.8 ms 0.06 ms

Notes:

  • NativeUnix is an order of magnitude faster for writes, but is much slower for reads, especially random reads. This is due to deliberately bypassing the OS-level IO cache with O_DIRECT. It is worth reviewing to determine if this read performance should be improved.
  • The MMap slow read performance is likely related to Search performance issue with MMapDirectory under load #1151

I have not yet run benchmarks on WindowsDirectory.

AI: This PR was mostly written using Claude Code, Opus 4.8.

paulirwin and others added 3 commits June 12, 2026 20:41
Complete the previously-excluded WindowsDirectory port from Lucene 4.8.1,
replacing the original Java + C++/JNI native layer with Win32 P/Invoke
(CreateFileW with FILE_FLAG_RANDOM_ACCESS, ReadFile with positioned
NativeOverlapped reads, GetFileInformationByHandle, and SafeFileHandle for
the handle lifecycle). Modernized to current Lucene.NET conventions
(ReadInternal(Span<byte>), Position, DirectoryInfo/string ctors, Dispose
pattern) to match SimpleFSDirectory.

The read handle uses FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE.
Upstream 4.8.1 used only READ | WRITE; FILE_SHARE_DELETE is added to match the
SimpleFSDirectory read-path behavior from apache#1283 so an open read handle does
not block deletion of the underlying file on Windows.

The type is guarded to Windows via a runtime Constants.WINDOWS check
(throwing PlatformNotSupportedException) and [SupportedOSPlatform("windows")]
(under FEATURE_SUPPORTEDOSPLATFORMATTRIBUTE). Enabled AllowUnsafeBlocks in
Lucene.Net.Misc for the fixed read buffer, and narrowed the Store compile
exclusion to only the still-unfinished NativeUnixDirectory/NativePosixUtil.

Added test coverage (the original Lucene type had none): TestWindowsDirectory
runs the full BaseDirectoryTestCase suite (Windows-gated), plus bespoke tests
for random-access positioned reads, independent clone reads, read-past-EOF,
clone dispose not closing the shared handle, and concurrent multi-threaded
clone reads. A cross-platform fixture verifies the platform guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ache#1342)

Complete the previously-excluded NativeUnixDirectory and NativePosixUtil port
from Lucene 4.8.1, replacing the original Java + C++/JNI native layer with
libc P/Invoke. NativePosixUtil does open_direct (Linux O_DIRECT|O_NOATIME;
macOS open + fcntl(F_NOCACHE)), pread, posix_fadvise/madvise with the Java->OS
advice-ordinal remap. NativeUnixDirectory ports the direct-I/O directory and
its IndexInput/IndexOutput faithfully (512-byte alignment, buffer dump/refill,
seek, clone), with the Java NIO FileChannel operations replaced by libc
pwrite/ftruncate/lseek and a block-aligned native buffer in place of the direct
ByteBuffer. Modernized to the current Span-based ReadInternal/WriteBytes and
Position/Dispose conventions.

Both types are guarded to throw PlatformNotSupportedException on Microsoft
Windows (Constants.WINDOWS + [UnsupportedOSPlatform("windows")]); they require
Linux/macOS direct I/O. Removed the Store compile exclusions from
Lucene.Net.Misc so both files now build.

Added test coverage (the original Lucene types had none): a cross-platform
guard fixture (verifies the Windows refusal), targeted direct-path tests
(round-trip, concurrent clone reads, clone dispose not closing the shared
descriptor), and TestNativeUnixDirectoryBase : BaseDirectoryTestCase running
the full directory suite against the direct-I/O path. TestFsyncDoesntCreateNewFiles
is overridden/skipped because NativeUnixDirectory is a composite/delegating
directory: non-MERGE writes go through the delegate and are tracked in its
staleFiles, so FSDirectory.Sync (which intersects with its own staleFiles)
does not fsync them - the base test itself notes it does not handle composite
directories. Unix functional tests are platform-gated and require an
O_DIRECT-capable filesystem.

Verified on Linux (WSL Ubuntu, .NET 10): 23 tests pass, 0 fail; on Windows the
functional/base tests skip and the guard tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…che#1342)

Two platform bugs caused TestNativeUnixDirectory* to fail (5 on macOS, all
on ARM64 Linux):

- macOS EACCES: the write path opened with open(path, O_RDWR|O_CREAT, 0666)
  through a fixed-signature P/Invoke, but C's open() is variadic so the mode
  argument rides the varargs ABI; on macOS (incl. Apple Silicon) the callee
  read garbage and created files without owner-read permission, so the
  subsequent O_RDONLY open failed with EACCES. Now pre-create the file with
  the BCL (applying the normal mode + umask) and open with the two-argument
  open() (no O_CREAT, no varargs). Dropped the broken 3-arg open and the
  O_CREAT/S_RW constants.

- ARM64 Linux ENOTDIR: O_DIRECT was hardcoded to 0x4000, which is correct on
  x86/x86-64 but is O_DIRECTORY on Arm/Arm64 (the two are swapped; O_DIRECT
  there is 0x10000), so open() of a regular file failed with ENOTDIR. Now
  select O_DIRECT by RuntimeInformation.OSArchitecture.

Also tighten the input refill EOF check from n < 0 to n <= 0: upstream used
FileChannel.read() (returns -1 at EOF) but the port wraps libc pread()
(returns 0 at EOF), so read-past-EOF would otherwise silently return zeros.

Verified: TestNativeUnixDirectory* 23/23 pass on macOS arm64, Linux aarch64,
and Linux x86-64 (the latter two in mcr.microsoft.com/dotnet/sdk:10.0 with an
O_DIRECT-capable TMPDIR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@paulirwin paulirwin added the notes:new-feature A new feature label Jun 13, 2026
@paulirwin paulirwin marked this pull request as ready for review June 13, 2026 20:47
@paulirwin paulirwin requested a review from NightOwl888 June 13, 2026 20:47

@NightOwl888 NightOwl888 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks.

Based on the benchmarks, I am starting to wonder whether we should aim to use native calls (or at least RandomAccess) instead of FileStream for writes on all of our FSDirectory implementations, since upstream they simply used FileChannel (which is a thin layer over native calls). Although, whatever solution we come up with, we should aim for getting the durability patched (#894) above all else.

Shouldn't we add these native calls to our existing native layer so we can utilize them to make a backport of RandomAccess for .NET Framework and other things that we are better off using native code for?

I didn't do a full review of this, but I found a few things we should address to make this implementation more complete.

Comment thread src/Lucene.Net.Misc/Store/NativePosixUtil.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/NativePosixUtil.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/NativePosixUtil.cs
Comment thread src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/NativeUnixDirectory.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/WindowsDirectory.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/WindowsDirectory.cs Outdated
Comment thread src/Lucene.Net.Misc/Store/WindowsDirectory.cs Outdated
Comment thread src/Lucene.Net.Tests.Misc/Support/Store/TestNativeUnixDirectory.cs
Comment thread src/Lucene.Net.Tests.Misc/Support/Store/TestWindowsDirectory.cs
@paulirwin

Copy link
Copy Markdown
Contributor Author

Benchmarks for WindowsDirectory, before making PR feedback modifications above, are in:

64 MB file (mean, ms):

Dir ReadSequential ReadRandom ConcurrentCloneReads
Windows 11.84 7.84 103.4
SimpleFS 11.72 8.52 79.0
NIOFS 20.83 7.55 133.3
MMap 415.59 5.97 937.1
RAM 6.49 0.12 4.7

Notes:

  • WindowsDirectory does not override CreateOutput with a custom implementation, so it was not benchmarked
  • Windows is faster than the average times of SimpleFS and NIOFS for non-concurrent reads.
  • Windows is within noise of SimpleFS for sequential reads, and faster for random reads. It is slightly slower than NIOFS for random reads.
  • Concurrent reads track well between SimpleFS and NIOFS.
  • Again, concurrent reads in MMap is likely Search performance issue with MMapDirectory under load #1151

…pache#1342)

Per NightOwl888's review of apache#1346:

- Surface IOException.HResult on the native-error paths so NativeFSLock (which
  inspects HResult to detect share/lock violations) keeps working:
  - errno-derived throws (NativePosixUtil.NewIOException/posix_fadvise;
    NativeUnixDirectory PWrite/FTruncate/FileSize) pass errno into the
    IOException(message, hresult) ctor. On Unix the BCL surfaces the raw errno
    as HResult (a FileStream share violation gives HResult == EWOULDBLOCK), so
    errno-as-HResult matches the convention.
  - Win32-derived throws (WindowsDirectory OpenFile/Read/Length) use
    Marshal.GetHRForLastWin32Error().
  - Exception-wrap sites (WindowsDirectory.ReadInternal; NativeUnixDirectory
    Refill/Length/Clone) preserve the original HResult. Uses the
    IOException(message, hresult) ctor where possible because the HResult setter
    is protected on net462/netstandard2.0; the RuntimeException wrappers guard
    the setter with #if for those TFMs.

- Implement NativeUnixIndexOutput.Checksum via a running CRC32 (like
  FSIndexOutput) instead of throwing NotSupportedException. The base-suite
  TestChecksum now passes against NativeUnixDirectory.

- Move the LuceneNetSpecific test files into the Support folder:
  Store/Test{NativeUnix,Windows}Directory.cs -> Support/Store/...

Not changed: the optional Put/Get micro-optimizations (Unsafe.CopyBlockUnaligned
/ ref byte), which the reviewer flagged as "benchmark to be sure it is worth
it"; the current Span.CopyTo already lowers to memmove.

Verified: builds on net10/net8/netstandard2.0/net462; TestNativeUnix/TestWindows
pass 24/24 on macOS arm64 and 23/23 on Linux aarch64.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@paulirwin paulirwin requested a review from NightOwl888 June 15, 2026 02:27
@paulirwin

Copy link
Copy Markdown
Contributor Author

I meant to reply earlier to your top-level review comment... replies below.

Based on the benchmarks, I am starting to wonder whether we should aim to use native calls (or at least RandomAccess) instead of FileStream for writes on all of our FSDirectory implementations, since upstream they simply used FileChannel (which is a thin layer over native calls). Although, whatever solution we come up with, we should aim for getting the durability patched (#894) above all else.

I like this, but it will not change the public API, so it can be broken out as a separate issue (even for post-beta-19) as a perf optimization opportunity.

Shouldn't we add these native calls to our existing native layer so we can utilize them to make a backport of RandomAccess for .NET Framework and other things that we are better off using native code for?

Same as above, I think this can be broken out separately as a possible post-beta-19 perf optimization opportunity. In this particular case, these calls are very OS-dependent just like they are upstream (hence the names of the directories), so there's no harm in them being alongside the OS-specific directory code for now. That would be an easy refactor later as part of this possible perf improvement as a separate issue/PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

notes:new-feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Finish port of NativeUnixDirectory and WindowsDirectory

2 participants