diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/FSRecordsImpl.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/FSRecordsImpl.java index 351683d96a55..ae1b111404d6 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/FSRecordsImpl.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/FSRecordsImpl.java @@ -244,7 +244,7 @@ public final class FSRecordsImpl implements Closeable { //bumped main version (60 -> 61) because of PFSRecords header enlargement (HEADER_ERRORS_ACCUMULATED) final int mainVFSFormatVersion = 61; //@formatter:off (nextMask better be aligned) - return nextMask(mainVFSFormatVersion + (PersistentFSRecordsStorageFactory.getRecordsStorageImplementation().ordinal()), /* acceptable range is [0..255] */ 8, + return nextMask(mainVFSFormatVersion + (PersistentFSRecordsStorageFactory.recordsStorageImplementation().ordinal()), /* acceptable range is [0..255] */ 8, nextMask(!USE_CONTENT_STORAGE_OVER_MMAPPED_FILE, //former USE_CONTENT_HASHES=true, this is why negation nextMask(IOUtil.useNativeByteOrderForByteBuffers(), nextMask(PageCacheUtils.LOCK_FREE_PAGE_CACHE_ENABLED && USE_ATTRIBUTES_OVER_NEW_FILE_PAGE_CACHE,//pageSize was changed on old<->new transition diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSLoader.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSLoader.java index 72c5afc7a86e..4fd383ced24a 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSLoader.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSLoader.java @@ -27,6 +27,7 @@ import com.intellij.util.io.*; import com.intellij.util.io.blobstorage.SpaceAllocationStrategy; import com.intellij.util.io.blobstorage.SpaceAllocationStrategy.DataLengthPlusFixedPercentStrategy; import com.intellij.util.io.blobstorage.StreamlinedBlobStorage; +import com.intellij.util.io.dev.StorageFactory; import com.intellij.util.io.dev.mmapped.MMappedFileStorageFactory; import com.intellij.util.io.pagecache.impl.PageContentLockingStrategy; import com.intellij.util.io.storage.*; @@ -86,7 +87,7 @@ public final class PersistentFSLoader { }) .filter(e -> e instanceof IOException) .findFirst().orElse(null); - + if (mainIoException != null && !mainIoException.getMessage().isEmpty()) { for (Throwable exception : exceptions) { if (exception != mainIoException) { @@ -776,15 +777,21 @@ public final class PersistentFSLoader { } public @NotNull PersistentFSRecordsStorage createRecordsStorage(@NotNull Path recordsFile) throws IOException { - PersistentFSRecordsStorage storage = PersistentFSRecordsStorageFactory.createStorage(recordsFile); - if (vfsLog != null) { - var recordsInterceptors = vfsLog.getConnectionInterceptors().stream() - .filter(RecordsInterceptor.class::isInstance) - .map(RecordsInterceptor.class::cast) - .toList(); - storage = InterceptorInjection.INSTANCE.injectInRecords(storage, recordsInterceptors); - } - return storage; + StorageFactory recordsStorageFactory = PersistentFSRecordsStorageFactory.recordsStorageImplementation(); + + LOG.trace("VFS uses " + recordsStorageFactory + " storage for main file records table"); + return recordsStorageFactory.wrapStorageSafely(recordsFile, records -> { + if (vfsLog != null) { + var recordsInterceptors = vfsLog.getConnectionInterceptors().stream() + .filter(RecordsInterceptor.class::isInstance) + .map(RecordsInterceptor.class::cast) + .toList(); + return InterceptorInjection.INSTANCE.injectInRecords(records, recordsInterceptors); + } + else { + return records; + } + }); } /** @return common version of all 3 storages, or -1, if their versions are different (i.e. inconsistent) */ diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorage.java b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorage.java index 4594b99c9eee..982f6d3f136d 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorage.java +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorage.java @@ -8,7 +8,7 @@ import org.jetbrains.annotations.NotNull; import java.io.IOException; @ApiStatus.Internal -public interface PersistentFSRecordsStorage extends CleanableStorage { +public interface PersistentFSRecordsStorage extends CleanableStorage, AutoCloseable { int NULL_ID = FSRecords.NULL_FILE_ID; int MIN_VALID_ID = NULL_ID + 1; @@ -118,10 +118,11 @@ public interface PersistentFSRecordsStorage extends CleanableStorage { // TODO add a synchronization or requirement to be called on the loading @SuppressWarnings("UnusedReturnValue") - boolean processAllRecords(@NotNull PersistentFSRecordsStorage.FsRecordProcessor processor) throws IOException; + boolean processAllRecords(@NotNull FsRecordProcessor processor) throws IOException; void force() throws IOException; + @Override void close() throws IOException; /** Close the storage and remove all its data files */ diff --git a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageFactory.kt b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageFactory.kt index 9a4ea90c50a3..b5c0c233c461 100644 --- a/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageFactory.kt +++ b/platform/platform-impl/src/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageFactory.kt @@ -1,9 +1,12 @@ -// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.vfs.newvfs.persistent -import com.intellij.openapi.diagnostic.Logger import com.intellij.util.SystemProperties.getBooleanProperty -import com.intellij.util.io.* +import com.intellij.util.io.IOUtil +import com.intellij.util.io.PageCacheUtils +import com.intellij.util.io.PagedFileStorageWithRWLockedPageContent +import com.intellij.util.io.StorageLockContext +import com.intellij.util.io.dev.StorageFactory import com.intellij.util.io.dev.mmapped.MMappedFileStorageFactory import com.intellij.util.io.pagecache.impl.PageContentLockingStrategy import org.jetbrains.annotations.VisibleForTesting @@ -13,36 +16,85 @@ import kotlin.io.path.exists object PersistentFSRecordsStorageFactory { - /** - * Fallback option: usually [PersistentFSConnector] deals with storages version changes just fine. - * But there are cases where it fails: e.g. with mmapped storages on Win, there file once mapped is hard - * to remove (and [PersistentFSConnector] relies on ability to just remove legacy storages files). - * In such cases, this option could help get rid of the legacy file early, before it is mapped - * MAYBE RC: we probably outgrow that issues: - * a) it's been 2 releases since we moved to memory-mapped for fs-records, i.e. everybody who could have this - * problem -- already has fixed it - * b) i've done a lot for memory-mapped files to work properly on Windows also - */ - @JvmStatic - private val FAIL_EARLY_IF_LEGACY_STORAGE_DETECTED: Boolean = getBooleanProperty("vfs.fail-early-if-legacy-storage-detected", false) - private val PERSISTENT_FS_STORAGE_CONTEXT_RW = StorageLockContext(true, true, true) + enum class RecordsStorageKind : StorageFactory { - enum class RecordsStorageKind { /** Currently the default impl */ - OVER_MMAPPED_FILE, + OVER_MMAPPED_FILE { + /** + * Fallback option: usually [PersistentFSConnector] deals with storages version changes just fine. + * But there are cases where it fails: e.g. with mmapped storages on Win, there file once mapped is hard + * to remove (and [PersistentFSConnector] relies on ability to just remove legacy storages files). + * In such cases, this option could help get rid of the legacy file early, before it is mapped + * MAYBE RC: we probably outgrow that issues: + * a) it's been 2 releases since we moved to memory-mapped for fs-records, i.e. everybody who could have this + * problem -- already has fixed it + * b) i've done a lot for memory-mapped files to work properly on Windows also + */ + private val FAIL_EARLY_IF_LEGACY_STORAGE_DETECTED: Boolean = getBooleanProperty("vfs.fail-early-if-legacy-storage-detected", false) + + override fun open(storagePath: Path): PersistentFSRecordsLockFreeOverMMappedFile { + //TODO RC: this should be replaced with/encapsulated into StorageFactory + if (FAIL_EARLY_IF_LEGACY_STORAGE_DETECTED) { + val legacyLengthFile = storagePath.resolveSibling(storagePath.fileName.toString() + ".len") + if (legacyLengthFile.exists()) { + //MAYBE RC: actually, here we can _migrate_ legacy file to the new format: move the files to the tmp folder, + // create empty new storage, and just copy all records from legacy storage to the new one... + throw IOException("Legacy records file detected (${legacyLengthFile} exists): VFS rebuild required") + } + } + return MMappedFileStorageFactory.withDefaults() + .pageSize(PersistentFSRecordsLockFreeOverMMappedFile.DEFAULT_MAPPED_CHUNK_SIZE) + .wrapStorageSafely(storagePath) { storage -> + PersistentFSRecordsLockFreeOverMMappedFile(storage) + } + } + }, + /** Fallback impl for [OVER_MMAPPED_FILE] if something goes terribly wrong, and we can't fix it quickly */ - OVER_LOCK_FREE_FILE_CACHE, + OVER_LOCK_FREE_FILE_CACHE { + private val PERSISTENT_FS_STORAGE_CONTEXT_RW = StorageLockContext(true, true, true) + + override fun open(storagePath: Path): PersistentFSRecordsOverLockFreePagedStorage { + val recordLength = PersistentFSRecordsOverLockFreePagedStorage.RECORD_SIZE_IN_BYTES + val pageSize = PageCacheUtils.DEFAULT_PAGE_SIZE + + if (!PageCacheUtils.LOCK_FREE_PAGE_CACHE_ENABLED) { + throw IOException( + "Configuration mismatch: PageCacheUtils.LOCK_FREE_PAGE_CACHE_ENABLED=false " + + "=> can't create PersistentFSRecordsOverLockFreePagedStorage if FilePageCacheLockFree is disabled") + } + + val recordsArePageAligned = pageSize % recordLength == 0 + if (!recordsArePageAligned) { + throw AssertionError("Bug: record length(=$recordLength) is not aligned with page size(=$pageSize)") + } + + val pagedStorage = PagedFileStorageWithRWLockedPageContent( + storagePath, + PERSISTENT_FS_STORAGE_CONTEXT_RW, + pageSize, + IOUtil.useNativeByteOrderForByteBuffers(), + PageContentLockingStrategy.LOCK_PER_PAGE + ) + return IOUtil.wrapSafely(pagedStorage){ + PersistentFSRecordsOverLockFreePagedStorage(it) + } + } + }, + /** For testing/benchmarking: serves as a reference point. Not a prod-level implementation! */ - IN_MEMORY + IN_MEMORY { + override fun open(storagePath: Path) = + @Suppress("TestOnlyProblems") PersistentInMemoryFSRecordsStorage(storagePath, /*max size: */1 shl 24) + } } - private var RECORDS_STORAGE_KIND = RecordsStorageKind.valueOf( - System.getProperty("vfs.records-storage.impl", RecordsStorageKind.OVER_MMAPPED_FILE.name)) + private var RECORDS_STORAGE_KIND = defaultFromSystemProperties() @JvmStatic - fun getRecordsStorageImplementation(): RecordsStorageKind = RECORDS_STORAGE_KIND + fun recordsStorageImplementation(): RecordsStorageKind = RECORDS_STORAGE_KIND @VisibleForTesting @JvmStatic @@ -55,88 +107,9 @@ object PersistentFSRecordsStorageFactory { @JvmStatic @JvmName("resetRecordsStorageImplementation") fun resetRecordsStorageImplementation() { - RECORDS_STORAGE_KIND = RecordsStorageKind.valueOf(System.getProperty("vfs.records-storage.impl", RecordsStorageKind.OVER_MMAPPED_FILE.name)) + RECORDS_STORAGE_KIND = defaultFromSystemProperties() } - - @JvmStatic - @Throws(IOException::class) - fun createStorage(file: Path): PersistentFSRecordsStorage { - FSRecords.LOG.trace("using $RECORDS_STORAGE_KIND storage for VFS records") - - return when (RECORDS_STORAGE_KIND) { - RecordsStorageKind.IN_MEMORY -> @Suppress("TestOnlyProblems") PersistentInMemoryFSRecordsStorage(file, /*max size: */1 shl 24) - RecordsStorageKind.OVER_LOCK_FREE_FILE_CACHE -> createLockFreeStorage(file) - RecordsStorageKind.OVER_MMAPPED_FILE -> { - //TODO RC: this should be replaced with/encapsulated into StorageFactory - if (FAIL_EARLY_IF_LEGACY_STORAGE_DETECTED) { - val legacyLengthFile = file.resolveSibling(file.fileName.toString() + ".len") - if (legacyLengthFile.exists()) { - //MAYBE RC: actually, here we can _migrate_ legacy file to the new format: move the files to the tmp folder, - // create empty new storage, and just copy all records from legacy storage to the new one... - throw IOException("Legacy records file detected (${legacyLengthFile} exists): VFS rebuild required") - } - } - MMappedFileStorageFactory.withDefaults() - .pageSize(PersistentFSRecordsLockFreeOverMMappedFile.DEFAULT_MAPPED_CHUNK_SIZE) - .wrapStorageSafely(file) { storage -> - PersistentFSRecordsLockFreeOverMMappedFile(storage) - } - } - } - } - - @JvmStatic - @VisibleForTesting - @Throws(IOException::class) - fun openRMappedFile(file: Path, - recordLength: Int): ResizeableMappedFile { - val pageSize = PageCacheUtils.DEFAULT_PAGE_SIZE * recordLength / PersistentFSRecordsOverLockFreePagedStorage.RECORD_SIZE_IN_BYTES - - val aligned = pageSize % recordLength == 0 - if (!aligned) { - val message = "Record length(=$recordLength) is not aligned with page size(=$pageSize)" - Logger.getInstance(PersistentFSRecordsStorage::class.java).error(message) - } - - return ResizeableMappedFile(file, recordLength * 1024, - PERSISTENT_FS_STORAGE_CONTEXT_RW, - pageSize, - aligned, - IOUtil.useNativeByteOrderForByteBuffers()) - } - - @JvmStatic - @VisibleForTesting - @Throws(IOException::class) - fun createLockFreeStorage(file: Path): PersistentFSRecordsOverLockFreePagedStorage { - val recordLength = PersistentFSRecordsOverLockFreePagedStorage.RECORD_SIZE_IN_BYTES - val pageSize = PageCacheUtils.DEFAULT_PAGE_SIZE - - if (!PageCacheUtils.LOCK_FREE_PAGE_CACHE_ENABLED) { - throw AssertionError( - "Bug: PageCacheUtils.LOCK_FREE_PAGE_CACHE_ENABLED=false " + - "=> can't create PersistentFSRecordsOverLockFreePagedStorage if FilePageCacheLockFree is disabled") - } - - val recordsArePageAligned = pageSize % recordLength == 0 - if (!recordsArePageAligned) { - throw AssertionError("Bug: record length(=$recordLength) is not aligned with page size(=$pageSize)") - } - - val storage = PagedFileStorageWithRWLockedPageContent( - file, - PERSISTENT_FS_STORAGE_CONTEXT_RW, - pageSize, - IOUtil.useNativeByteOrderForByteBuffers(), - PageContentLockingStrategy.LOCK_PER_PAGE - ) - try { - return PersistentFSRecordsOverLockFreePagedStorage(storage) - } - catch (t: Throwable) { - storage.close() - throw t - } - } + private fun defaultFromSystemProperties() = + RecordsStorageKind.valueOf(System.getProperty("vfs.records-storage.impl", RecordsStorageKind.OVER_MMAPPED_FILE.name)) } \ No newline at end of file diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageOverLockFreePagedStorageTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageOverLockFreePagedStorageTest.java index 9e32309a5a7b..61c15fb13f14 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageOverLockFreePagedStorageTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/vfs/newvfs/persistent/PersistentFSRecordsStorageOverLockFreePagedStorageTest.java @@ -1,13 +1,10 @@ -// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. package com.intellij.openapi.vfs.newvfs.persistent; +import com.intellij.openapi.vfs.newvfs.persistent.PersistentFSRecordsStorageFactory.RecordsStorageKind; import com.intellij.util.io.PageCacheUtils; -import com.intellij.util.io.PagedFileStorageWithRWLockedPageContent; -import com.intellij.util.io.StorageLockContext; -import com.intellij.util.io.pagecache.impl.PageContentLockingStrategy; import org.jetbrains.annotations.NotNull; -import org.junit.After; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,14 +32,12 @@ public class PersistentFSRecordsStorageOverLockFreePagedStorageTest } public static final int MAX_RECORDS_TO_INSERT = 1 << 22; - private PagedFileStorageWithRWLockedPageContent pagedStorage; + private static final int PAGE_SIZE = PageCacheUtils.DEFAULT_PAGE_SIZE; public PersistentFSRecordsStorageOverLockFreePagedStorageTest(final UpdateAPIMethod updateMethodToTest) { super(MAX_RECORDS_TO_INSERT, updateMethodToTest); } - private StorageLockContext storageContext; - @BeforeClass public static void beforeClass() throws Exception { assumeTrue( @@ -54,43 +49,27 @@ public class PersistentFSRecordsStorageOverLockFreePagedStorageTest @NotNull @Override protected PersistentFSRecordsOverLockFreePagedStorage openStorage(final Path storagePath) throws IOException { - final int pageSize; - final boolean nativeBytesOrder; - try (var file = PersistentFSRecordsStorageFactory.openRMappedFile(storagePath, RECORD_SIZE_IN_BYTES)) { - storageContext = file.getStorageLockContext(); - pageSize = file.getPagedFileStorage().getPageSize(); - nativeBytesOrder = file.isNativeBytesOrder(); - } - pagedStorage = new PagedFileStorageWithRWLockedPageContent( - storagePath, - storageContext, - pageSize, - nativeBytesOrder, - PageContentLockingStrategy.LOCK_PER_PAGE - ); - return new PersistentFSRecordsOverLockFreePagedStorage(pagedStorage); + return (PersistentFSRecordsOverLockFreePagedStorage)RecordsStorageKind.OVER_LOCK_FREE_FILE_CACHE.open(storagePath); } @Test public void recordAreAlwaysAlignedFullyOnSinglePage() { - final int pageSize = pagedStorage.getPageSize(); - final int enoughRecords = (pageSize / RECORD_SIZE_IN_BYTES) * 16; + final int enoughRecords = (PAGE_SIZE / RECORD_SIZE_IN_BYTES) * 16; for (int recordId = 0; recordId < enoughRecords; recordId++) { final long recordOffsetInFile = storage.recordOffsetInFileUnchecked(recordId); final long recordEndOffsetInFile = recordOffsetInFile + RECORD_SIZE_IN_BYTES - 1; assertEquals( "Record(#" + recordId + ", offset: " + recordOffsetInFile + ") must start and end on a same page", - recordOffsetInFile / pageSize, - recordEndOffsetInFile / pageSize + recordOffsetInFile / PAGE_SIZE, + recordEndOffsetInFile / PAGE_SIZE ); } } @Test public void recordOffsetCalculatedByStorageIsConsistentWithPlainCalculation() { - final int pageSize = pagedStorage.getPageSize(); - final int enoughRecords = (pageSize / RECORD_SIZE_IN_BYTES) * 16; + final int enoughRecords = (PAGE_SIZE / RECORD_SIZE_IN_BYTES) * 16; long expectedRecordOffsetInFile = PersistentFSRecordsOverLockFreePagedStorage.HEADER_SIZE; for (int recordId = NULL_ID + 1; recordId < enoughRecords; recordId++) { @@ -102,16 +81,15 @@ public class PersistentFSRecordsStorageOverLockFreePagedStorageTest ); expectedRecordOffsetInFile += RECORD_SIZE_IN_BYTES; - if (pageSize - (expectedRecordOffsetInFile % pageSize) < RECORD_SIZE_IN_BYTES) { - expectedRecordOffsetInFile = (expectedRecordOffsetInFile / pageSize + 1) * pageSize; + if (PAGE_SIZE - (expectedRecordOffsetInFile % PAGE_SIZE) < RECORD_SIZE_IN_BYTES) { + expectedRecordOffsetInFile = (expectedRecordOffsetInFile / PAGE_SIZE + 1) * PAGE_SIZE; } } } @Test public void loadRecordsCount_IsConsistentWith_recordOffsetInFile() throws IOException { - final int pageSize = pagedStorage.getPageSize(); - final int enoughRecords = (pageSize / RECORD_SIZE_IN_BYTES) * 16; + final int enoughRecords = (PAGE_SIZE / RECORD_SIZE_IN_BYTES) * 16; for (int recordId = NULL_ID + 1; recordId < enoughRecords; recordId++) { final long recordOffsetInFile = storage.recordOffsetInFileUnchecked(recordId); @@ -128,8 +106,7 @@ public class PersistentFSRecordsStorageOverLockFreePagedStorageTest public void loadRecordsCount_ThrowsIOException_IfUnEvenNumberOfRecordsFound() throws IOException { //RC: method is very slow, so use 1 random excess for each record, instead of iterating through // each excess: [1..RECORD_SIZE_IN_BYTES) on each record - final int pageSize = pagedStorage.getPageSize(); - final int enoughRecords = (pageSize / RECORD_SIZE_IN_BYTES) * 16; + final int enoughRecords = (PAGE_SIZE / RECORD_SIZE_IN_BYTES) * 16; for (int recordId = NULL_ID + 1; recordId < enoughRecords; recordId++) { final long recordOffsetInFile = storage.recordOffsetInFileUnchecked(recordId);