You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by ni...@apache.org on 2022/06/28 08:37:58 UTC

[ignite] branch master updated: IGNITE-17181 Add free size calculation to index-reader (#10102)

This is an automated email from the ASF dual-hosted git repository.

nizhikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ignite.git


The following commit(s) were added to refs/heads/master by this push:
     new 2703c26e290 IGNITE-17181 Add free size calculation to index-reader (#10102)
2703c26e290 is described below

commit 2703c26e290c96dd2e771a2edd4e03e94d528038
Author: Nikolay <ni...@apache.org>
AuthorDate: Tue Jun 28 11:37:51 2022 +0300

    IGNITE-17181 Add free size calculation to index-reader (#10102)
---
 .../commandline/indexreader/IgniteIndexReader.java | 157 ++++++++-------
 .../commandline/indexreader/PageListsInfo.java     |   8 +-
 .../commandline/indexreader/ScanContext.java       |  36 +++-
 .../indexreader/IgniteIndexReaderTest.java         |   1 +
 .../cache/persistence/freelist/PagesList.java      |   2 +-
 .../persistence/freelist/io/PagesListMetaIO.java   |  11 +-
 .../persistence/freelist/io/PagesListNodeIO.java   |  17 +-
 .../persistence/tree/io/AbstractDataPageIO.java    |  11 +-
 .../cache/persistence/tree/io/BPlusIO.java         |   5 +
 .../cache/persistence/tree/io/BPlusMetaIO.java     |  13 +-
 .../cache/persistence/tree/io/PageIO.java          |   9 +
 .../cache/persistence/tree/io/PageMetaIO.java      |   5 +
 .../tree/io/PagePartitionCountersIO.java           |   7 +-
 .../cache/persistence/tree/io/TrackingPageIO.java  |   5 +
 .../processors/cache/persistence/DummyPageIO.java  |   5 +
 .../persistence/tree/io/PageIOFreeSizeTest.java    | 217 +++++++++++++++++++++
 .../ignite/testsuites/IgnitePdsTestSuite5.java     |   2 +
 17 files changed, 404 insertions(+), 107 deletions(-)

diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReader.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReader.java
index bfd51d1872a..b691972484e 100644
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReader.java
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReader.java
@@ -18,12 +18,11 @@
 package org.apache.ignite.internal.commandline.indexreader;
 
 import java.io.File;
-import java.io.IOException;
 import java.nio.ByteBuffer;
-import java.nio.channels.FileChannel;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -46,6 +45,7 @@ import org.apache.ignite.internal.cache.query.index.sorted.inline.io.InlineIO;
 import org.apache.ignite.internal.commandline.CommandHandler;
 import org.apache.ignite.internal.commandline.ProgressPrinter;
 import org.apache.ignite.internal.commandline.argument.parser.CLIArgumentParser;
+import org.apache.ignite.internal.commandline.indexreader.ScanContext.PagesStatistic;
 import org.apache.ignite.internal.commandline.systemview.SystemViewCommand;
 import org.apache.ignite.internal.pagemem.PageIdAllocator;
 import org.apache.ignite.internal.processors.cache.persistence.IndexStorageImpl;
@@ -64,6 +64,7 @@ import org.apache.ignite.internal.processors.cache.persistence.tree.io.DataPageP
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIO;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageMetaIO;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.PagePartitionMetaIO;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.TrackingPageIO;
 import org.apache.ignite.internal.processors.cache.persistence.wal.crc.IgniteDataIntegrityViolationException;
 import org.apache.ignite.internal.processors.cache.tree.AbstractDataLeafIO;
 import org.apache.ignite.internal.processors.cache.tree.PendingRowIO;
@@ -150,7 +151,7 @@ public class IgniteIndexReader implements AutoCloseable {
         Pattern.compile("(?<id>[-0-9]{1,15})_.*");
 
     /** */
-    private static final int CHECK_PARTS_MAX_ERRORS_PER_PARTITION = 10;
+    private static final int MAX_ERRORS_CNT = 10;
 
     static {
         IndexProcessor.registerIO();
@@ -357,8 +358,6 @@ public class IgniteIndexReader implements AutoCloseable {
      * @return Tree traversal context.
      */
     ScanContext recursiveTreeScan(long rootPageId, String idx, ItemStorage items) {
-        pageIds.add(normalizePageId(rootPageId));
-
         ScanContext ctx = createContext(cacheAndTypeId(idx).get1(), filePageStore(rootPageId), items);
 
         metaPageVisitor.readAndVisit(rootPageId, ctx);
@@ -375,8 +374,6 @@ public class IgniteIndexReader implements AutoCloseable {
      * @return Tree traversal context.
      */
     private ScanContext horizontalTreeScan(long rootPageId, String idx, ItemStorage items) {
-        pageIds.add(normalizePageId(rootPageId));
-
         ScanContext ctx = createContext(cacheAndTypeId(idx).get1(), filePageStore(rootPageId), items);
 
         levelsPageVisitor.readAndVisit(rootPageId, ctx);
@@ -394,7 +391,7 @@ public class IgniteIndexReader implements AutoCloseable {
         return doWithBuffer((buf, addr) -> {
             Map<IgniteBiTuple<Long, Integer>, List<Long>> bucketsData = new HashMap<>();
 
-            Map<Class<? extends PageIO>, Long> ioStat = new HashMap<>();
+            Map<Class<? extends PageIO>, PagesStatistic> stats = new HashMap<>();
 
             Map<Long, List<String>> errors = new HashMap<>();
 
@@ -405,7 +402,7 @@ public class IgniteIndexReader implements AutoCloseable {
                 try {
                     PagesListMetaIO io = readPage(idxStore, currMetaPageId, buf);
 
-                    pageIds.add(normalizePageId(currMetaPageId));
+                    ScanContext.addToStats(io, stats, 1, addr, idxStore.getPageSize());
 
                     Map<Integer, GridLongList> data = new HashMap<>();
 
@@ -419,7 +416,7 @@ public class IgniteIndexReader implements AutoCloseable {
 
                         for (Long listId : listIds) {
                             try {
-                                pagesCnt += visitPageList(listId, ioStat);
+                                pagesCnt += visitPageList(listId, stats);
                             }
                             catch (Exception err) {
                                 errors.put(listId, singletonList(err.getMessage()));
@@ -438,7 +435,7 @@ public class IgniteIndexReader implements AutoCloseable {
                 }
             }
 
-            return new PageListsInfo(bucketsData, pagesCnt, ioStat, errors);
+            return new PageListsInfo(bucketsData, pagesCnt, stats, errors);
         });
     }
 
@@ -446,10 +443,10 @@ public class IgniteIndexReader implements AutoCloseable {
      * Visit single page list.
      *
      * @param listStartPageId Id of the start page of the page list.
-     * @param ioStat Page types statistics.
+     * @param stats Page types statistics.
      * @return List of page ids.
      */
-    private long visitPageList(long listStartPageId, Map<Class<? extends PageIO>, Long> ioStat) throws IgniteCheckedException {
+    private long visitPageList(long listStartPageId, Map<Class<? extends PageIO>, PagesStatistic> stats) throws IgniteCheckedException {
         return doWithBuffer((nodeBuf, nodeAddr) -> doWithBuffer((pageBuf, pageAddr) -> {
             long res = 0;
 
@@ -458,18 +455,16 @@ public class IgniteIndexReader implements AutoCloseable {
             while (currPageId != 0) {
                 PagesListNodeIO io = readPage(idxStore, currPageId, nodeBuf);
 
-                ScanContext.onPageIO(readPage(idxStore, currPageId, pageBuf).getClass(), ioStat, 1);
+                ScanContext.addToStats(io, stats, 1, nodeAddr, idxStore.getPageSize());
 
-                pageIds.add(normalizePageId(normalizePageId(currPageId)));
+                ScanContext.addToStats(readPage(idxStore, currPageId, pageBuf), stats, 1, pageAddr, idxStore.getPageSize());
 
                 res += io.getCount(nodeAddr);
 
                 for (int i = 0; i < io.getCount(nodeAddr); i++) {
                     long pageId = normalizePageId(io.getAt(nodeAddr, i));
 
-                    pageIds.add(pageId);
-
-                    ScanContext.onPageIO(readPage(idxStore, pageId, pageBuf).getClass(), ioStat, 1);
+                    ScanContext.addToStats(readPage(idxStore, pageId, pageBuf), stats, 1, pageAddr, idxStore.getPageSize());
                 }
 
                 currPageId = io.getNextId(nodeAddr);
@@ -499,17 +494,14 @@ public class IgniteIndexReader implements AutoCloseable {
                 try {
                     pageId = pageId(INDEX_PARTITION, FLAG_IDX, i);
 
-                    PageIO io = readPage(ctx, pageId, buf);
+                    PageIO io = readPage(ctx, pageId, buf, false);
 
                     progressPrinter.printProgress();
 
                     if (idxFilter != null)
                         continue;
 
-                    if (io instanceof PageMetaIO || io instanceof PagesListMetaIO)
-                        continue;
-
-                    if (!((io instanceof BPlusMetaIO || io instanceof BPlusInnerIO)))
+                    if (io instanceof TrackingPageIO)
                         continue;
 
                     if (pageIds.contains(normalizePageId(pageId)))
@@ -589,8 +581,8 @@ public class IgniteIndexReader implements AutoCloseable {
                                 ", link=" + cacheAwareLink.link + ']');
                         }
 
-                        if (errors.size() >= CHECK_PARTS_MAX_ERRORS_PER_PARTITION) {
-                            errors.add("Too many errors (" + CHECK_PARTS_MAX_ERRORS_PER_PARTITION +
+                        if (errors.size() >= MAX_ERRORS_CNT) {
+                            errors.add("Too many errors (" + MAX_ERRORS_CNT +
                                 ") found for partId=" + partId + ", stopping analysis for this partition.");
 
                             break;
@@ -684,49 +676,41 @@ public class IgniteIndexReader implements AutoCloseable {
         return partId == INDEX_PARTITION ? idxStore : partStores[partId];
     }
 
-    /**
-     * Reading a page from channel into buffer.
-     *
-     * @param buf Buffer.
-     * @param ch Source for reading pages.
-     * @param pageSize Size of page to read into buffer.
-     */
-    private boolean readNextPage(ByteBuffer buf, FileChannel ch, int pageSize) throws IOException {
-        assert buf.remaining() == pageSize;
-
-        do {
-            if (ch.read(buf) == -1)
-                break;
-        }
-        while (buf.hasRemaining());
-
-        if (!buf.hasRemaining() && PageIO.getPageId(buf) != 0)
-            return true; //pageSize bytes read && pageId != 0
-        else if (buf.remaining() == pageSize)
-            return false; //0 bytes read
-        else
-            // 1 <= readBytes < pageSize || readBytes == pagesIze && pageId != 0
-            throw new IgniteException("Corrupted page in partitionId " +
-                ", readByte=" + buf.position() + ", pageSize=" + pageSize);
-    }
-
     /** */
     static long normalizePageId(long pageId) {
         return pageId(partId(pageId), flag(pageId), pageIndex(pageId));
     }
 
+    /** */
+    private <I extends PageIO> I readPage(FilePageStore store, long pageId, ByteBuffer buf) throws IgniteCheckedException {
+        return readPage(store, pageId, buf, true);
+    }
+
     /**
      * Reads pages into buffer.
      *
      * @param store Source for reading pages.
      * @param pageId Page ID.
      * @param buf Buffer.
+     * @param addToPageIds If {@code true} then add page ID to global set.
      */
-    private <I extends PageIO> I readPage(FilePageStore store, long pageId, ByteBuffer buf) throws IgniteCheckedException {
+    private <I extends PageIO> I readPage(
+        FilePageStore store,
+        long pageId,
+        ByteBuffer buf,
+        boolean addToPageIds
+    ) throws IgniteCheckedException {
         try {
             store.read(pageId, (ByteBuffer)buf.rewind(), false);
 
-            return PageIO.getPageIO(bufferAddress(buf));
+            long addr = bufferAddress(buf);
+
+            if (store == idxStore && addToPageIds)
+                pageIds.add(normalizePageId(pageId));
+
+            I io = PageIO.getPageIO(addr);
+
+            return io;
         }
         catch (IgniteDataIntegrityViolationException | IllegalArgumentException e) {
             // Replacing exception due to security reasons, as IgniteDataIntegrityViolationException prints page content.
@@ -738,9 +722,19 @@ public class IgniteIndexReader implements AutoCloseable {
 
     /** */
     protected <I extends PageIO> I readPage(ScanContext ctx, long pageId, ByteBuffer buf) throws IgniteCheckedException {
-        final I io = readPage(ctx.store, pageId, buf);
+        return readPage(ctx, pageId, buf, true);
+    }
 
-        ctx.onPageIO(io);
+    /** */
+    protected <I extends PageIO> I readPage(
+        ScanContext ctx,
+        long pageId,
+        ByteBuffer buf,
+        boolean addToPageIds
+    ) throws IgniteCheckedException {
+        final I io = readPage(ctx.store, pageId, buf, addToPageIds);
+
+        ctx.addToStats(io, bufferAddress(buf));
 
         return io;
     }
@@ -785,16 +779,16 @@ public class IgniteIndexReader implements AutoCloseable {
             if (rctx.items.size() != hctx.items.size())
                 errors.add(compareError("items", name, rctx.items.size(), hctx.items.size(), null));
 
-            rctx.ioStat.forEach((cls, cnt) -> {
-                long scanCnt = hctx.ioStat.getOrDefault(cls, 0L);
+            rctx.stats.forEach((cls, stat) -> {
+                long scanCnt = hctx.stats.getOrDefault(cls, new PagesStatistic()).cnt;
 
-                if (scanCnt != cnt)
-                    errors.add(compareError("pages", name, cnt, scanCnt, cls));
+                if (scanCnt != stat.cnt)
+                    errors.add(compareError("pages", name, stat.cnt, scanCnt, cls));
             });
 
-            hctx.ioStat.forEach((cls, cnt) -> {
-                if (!rctx.ioStat.containsKey(cls))
-                    errors.add(compareError("pages", name, 0, cnt, cls));
+            hctx.stats.forEach((cls, stat) -> {
+                if (!rctx.stats.containsKey(cls))
+                    errors.add(compareError("pages", name, 0, stat.cnt, cls));
             });
         });
 
@@ -812,7 +806,7 @@ public class IgniteIndexReader implements AutoCloseable {
 
     /** Prints sequential file scan results. */
     private void printSequentialScanInfo(ScanContext ctx) {
-        printIoStat("", "---- These pages types were encountered during sequential scan:", ctx.ioStat);
+        printIoStat("", "---- These pages types were encountered during sequential scan:", ctx.stats);
 
         if (!ctx.errors.isEmpty()) {
             log.severe("----");
@@ -827,7 +821,7 @@ public class IgniteIndexReader implements AutoCloseable {
             null,
             Arrays.asList(STRING, NUMBER),
             Arrays.asList(
-                Arrays.asList("Total pages encountered during sequential scan:", ctx.ioStat.values().stream().mapToLong(a -> a).sum()),
+                Arrays.asList("Total pages encountered during sequential scan:", ctx.stats.values().stream().mapToLong(a -> a.cnt).sum()),
                 Arrays.asList("Total errors occurred during sequential scan: ", ctx.errors.size())
             ),
             log
@@ -861,7 +855,7 @@ public class IgniteIndexReader implements AutoCloseable {
     private void printScanResults(String prefix, Map<String, ScanContext> ctxs) {
         log.info(prefix + "Tree traversal results");
 
-        Map<Class<? extends PageIO>, Long> ioStat = new HashMap<>();
+        Map<Class<? extends PageIO>, PagesStatistic> stats = new HashMap<>();
 
         int totalErr = 0;
 
@@ -874,9 +868,9 @@ public class IgniteIndexReader implements AutoCloseable {
 
             log.info(prefix + "-----");
             log.info(prefix + "Index tree: " + idxName);
-            printIoStat(prefix, "---- Page stat:", ctx.ioStat);
+            printIoStat(prefix, "---- Page stat:", ctx.stats);
 
-            ctx.ioStat.forEach((cls, cnt) -> ScanContext.onPageIO(cls, ioStat, cnt));
+            ctx.stats.forEach((cls, stat) -> ScanContext.addToStats(cls, stats, stat));
 
             log.info(prefix + "---- Count of items found in leaf pages: " + ctx.items.size());
 
@@ -897,7 +891,7 @@ public class IgniteIndexReader implements AutoCloseable {
 
         log.info(prefix + "----");
 
-        printIoStat(prefix, "Total page stat collected during trees traversal:", ioStat);
+        printIoStat(prefix, "Total page stat collected during trees traversal:", stats);
 
         log.info("");
 
@@ -928,7 +922,7 @@ public class IgniteIndexReader implements AutoCloseable {
             Arrays.asList(STRING, NUMBER),
             Arrays.asList(
                 Arrays.asList(prefix + "Total trees: ", ctxs.keySet().size()),
-                Arrays.asList(prefix + "Total pages found in trees: ", ioStat.values().stream().mapToLong(a -> a).sum()),
+                Arrays.asList(prefix + "Total pages found in trees: ", stats.values().stream().mapToLong(a -> a.cnt).sum()),
                 Arrays.asList(prefix + "Total errors during trees traversal: ", totalErr)
             ),
             log
@@ -970,7 +964,7 @@ public class IgniteIndexReader implements AutoCloseable {
             log.info(sb.toString());
         });
 
-        printIoStat(PAGE_LISTS_PREFIX, "---- Page stat:", pageListsInfo.ioStat);
+        printIoStat(PAGE_LISTS_PREFIX, "---- Page stat:", pageListsInfo.stats);
 
         printErrors(PAGE_LISTS_PREFIX, "---- Errors:", "---- No errors.", "Page id: %s, exception: ", pageListsInfo.errors);
 
@@ -1020,20 +1014,27 @@ public class IgniteIndexReader implements AutoCloseable {
     }
 
     /** */
-    private void printIoStat(String prefix, String caption, Map<Class<? extends PageIO>, Long> ioStat) {
+    private void printIoStat(String prefix, String caption, Map<Class<? extends PageIO>, PagesStatistic> stats) {
         if (caption != null)
-            log.info(prefix + caption + (ioStat.isEmpty() ? " empty" : ""));
+            log.info(prefix + caption + (stats.isEmpty() ? " empty" : ""));
 
-        if (ioStat.isEmpty())
+        if (stats.isEmpty())
             return;
 
-        List<List<?>> data = new ArrayList<>(ioStat.size());
+        List<List<?>> data = new ArrayList<>(stats.size());
+
+        stats.forEach((cls, stat) -> data.add(Arrays.asList(
+            prefix + cls.getSimpleName(),
+            stat.cnt,
+            String.format("%.2f", ((double)stat.freeSpace) / U.KB),
+            String.format("%.2f", (stat.freeSpace * 100.0d) / (pageSize * stat.cnt))
+        )));
 
-        ioStat.forEach((cls, cnt) -> data.add(Arrays.asList(prefix + cls.getSimpleName(), cnt)));
+        Collections.sort(data, Comparator.comparingLong(l -> (Long)l.get(1)));
 
         SystemViewCommand.printTable(
-            null,
-            Arrays.asList(STRING, NUMBER),
+            Arrays.asList(prefix + "Type", "Pages", "Free space (Kb)", "Free space (%)"),
+            Arrays.asList(STRING, NUMBER, NUMBER, NUMBER),
             data,
             log
         );
@@ -1141,8 +1142,6 @@ public class IgniteIndexReader implements AutoCloseable {
         public void visit(long addr, ScanContext ctx) throws IgniteCheckedException {
             BPlusInnerIO<?> io = PageIO.getPageIO(addr);
 
-            pageIds.add(normalizePageId(PageIO.getPageId(addr)));
-
             for (long id : children(io, addr))
                 readAndVisit(id, ctx);
         }
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/PageListsInfo.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/PageListsInfo.java
index 84d5cfc0f1f..2c7a94aca72 100644
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/PageListsInfo.java
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/PageListsInfo.java
@@ -37,8 +37,8 @@ class PageListsInfo {
     /** Found pages count. */
     final long pagesCnt;
 
-    /** Page type statistics. */
-    final Map<Class<? extends PageIO>, Long> ioStat;
+    /** Pages statistics. */
+    final Map<Class<? extends PageIO>, ScanContext.PagesStatistic> stats;
 
     /** Map of errors, pageId -> list of exceptions. */
     final Map<Long, List<String>> errors;
@@ -47,12 +47,12 @@ class PageListsInfo {
     public PageListsInfo(
         Map<IgniteBiTuple<Long, Integer>, List<Long>> bucketsData,
         long pagesCnt,
-        Map<Class<? extends PageIO>, Long> ioStat,
+        Map<Class<? extends PageIO>, ScanContext.PagesStatistic> stats,
         Map<Long, List<String>> errors
     ) {
         this.bucketsData = bucketsData;
         this.pagesCnt = pagesCnt;
-        this.ioStat = ioStat;
+        this.stats = stats;
         this.errors = errors;
     }
 }
diff --git a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/ScanContext.java b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/ScanContext.java
index c06f43a03bd..bb1ef520533 100644
--- a/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/ScanContext.java
+++ b/modules/control-utility/src/main/java/org/apache/ignite/internal/commandline/indexreader/ScanContext.java
@@ -35,7 +35,7 @@ class ScanContext {
     final FilePageStore store;
 
     /** Page type statistics. */
-    final Map<Class<? extends PageIO>, Long> ioStat;
+    final Map<Class<? extends PageIO>, PagesStatistic> stats;
 
     /** Map of errors, pageId -> set of exceptions. */
     final Map<Long, List<String>> errors;
@@ -48,22 +48,46 @@ class ScanContext {
         this.cacheId = cacheId;
         this.store = store;
         this.items = items;
-        this.ioStat = new LinkedHashMap<>();
+        this.stats = new LinkedHashMap<>();
         this.errors = new LinkedHashMap<>();
     }
 
     /** */
-    public void onPageIO(PageIO io) {
-        onPageIO(io.getClass(), ioStat, 1);
+    public void addToStats(PageIO io, long addr) {
+        addToStats(io, stats, 1, addr, store.getPageSize());
     }
 
     /** */
-    public static void onPageIO(Class<? extends PageIO> io, Map<Class<? extends PageIO>, Long> ioStat, long cnt) {
-        ioStat.compute(io, (k, v) -> v == null ? cnt : v + cnt);
+    public static void addToStats(PageIO io, Map<Class<? extends PageIO>, PagesStatistic> stats, long cnt, long addr, int pageSize) {
+        PagesStatistic stat = stats.computeIfAbsent(io.getClass(), k -> new PagesStatistic());
+
+        stat.cnt += cnt;
+        stat.freeSpace += io.getFreeSpace(pageSize, addr);
+    }
+
+    /** */
+    public static void addToStats(
+        Class<? extends PageIO> io,
+        Map<Class<? extends PageIO>, PagesStatistic> stats,
+        PagesStatistic toAdd
+    ) {
+        PagesStatistic stat = stats.computeIfAbsent(io, k -> new PagesStatistic());
+
+        stat.cnt += toAdd.cnt;
+        stat.freeSpace += toAdd.freeSpace;
     }
 
     /** */
     public void onLeafPage(long pageId, List<Object> data) {
         data.forEach(items::add);
     }
+
+    /** */
+    static class PagesStatistic {
+        /** Count of pages. */
+        long cnt;
+
+        /** Summary free space. */
+        long freeSpace;
+    }
 }
diff --git a/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReaderTest.java b/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReaderTest.java
index 0b6358337f3..f6d78008a58 100644
--- a/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReaderTest.java
+++ b/modules/control-utility/src/test/java/org/apache/ignite/internal/commandline/indexreader/IgniteIndexReaderTest.java
@@ -129,6 +129,7 @@ public class IgniteIndexReaderTest extends GridCommandHandlerAbstractTest {
     private static final String CHECK_IDX_PTRN_COMMON =
         "<PREFIX>Index tree: I \\[idxName=[\\-_0-9]{1,20}_%s##H2Tree.0, pageId=[0-9a-f]{16}\\]" +
             LINE_DELIM + "<PREFIX>---- Page stat:" +
+            LINE_DELIM + "<PREFIX>Type.*Pages.*Free space.*" +
             LINE_DELIM + "<PREFIX>([0-9a-zA-Z]{1,50}.*[0-9]{1,5}" +
             LINE_DELIM + "<PREFIX>){%s,1000}---- Count of items found in leaf pages: %s" +
             LINE_DELIM;
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
index 879ec667792..1d23fb321f7 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/PagesList.java
@@ -2320,7 +2320,7 @@ public abstract class PagesList extends DataStructure {
          * @param tailId Tail ID.
          * @param empty Empty flag.
          */
-        Stripe(long tailId, boolean empty) {
+        public Stripe(long tailId, boolean empty) {
             this.tailId = tailId;
             this.empty = empty;
         }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
index 6c5507ae2ac..0fae24c6d3e 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListMetaIO.java
@@ -39,10 +39,10 @@ public class PagesListMetaIO extends PageIO {
     private static final int NEXT_META_PAGE_OFF = CNT_OFF + 2;
 
     /** */
-    private static final int ITEMS_OFF = NEXT_META_PAGE_OFF + 8;
+    public static final int ITEMS_OFF = NEXT_META_PAGE_OFF + 8;
 
     /** */
-    private static final int ITEM_SIZE = 10;
+    public static final int ITEM_SIZE = 10;
 
     /** */
     public static final IOVersions<PagesListMetaIO> VERSIONS = new IOVersions<>(
@@ -180,7 +180,7 @@ public class PagesListMetaIO extends PageIO {
      * @param pageAddr Page address.
      * @return Maximum number of items which can be stored in buffer.
      */
-    private int getCapacity(int pageSize, long pageAddr) {
+    public int getCapacity(int pageSize, long pageAddr) {
         return (pageSize - ITEMS_OFF) / ITEM_SIZE;
     }
 
@@ -209,4 +209,9 @@ public class PagesListMetaIO extends PageIO {
 
         sb.a("\n\t}\n]");
     }
+
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return (getCapacity(pageSize, pageAddr) - getCount(pageAddr)) * ITEM_SIZE;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
index 42126dddede..a80651a2820 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/freelist/io/PagesListNodeIO.java
@@ -43,10 +43,10 @@ public class PagesListNodeIO extends PageIO implements CompactablePageIO {
     private static final int PREV_PAGE_ID_OFF = COMMON_HEADER_END;
 
     /** */
-    private static final int NEXT_PAGE_ID_OFF = PREV_PAGE_ID_OFF + 8;
+    private static final int NEXT_PAGE_ID_OFF = PREV_PAGE_ID_OFF + Long.BYTES;
 
     /** */
-    private static final int CNT_OFF = NEXT_PAGE_ID_OFF + 8;
+    private static final int CNT_OFF = NEXT_PAGE_ID_OFF + Long.BYTES;
 
     /** */
     private static final int PAGE_IDS_OFF = CNT_OFF + 2;
@@ -140,8 +140,8 @@ public class PagesListNodeIO extends PageIO implements CompactablePageIO {
      * @param pageSize Page size.
      * @return Capacity of this page in items.
      */
-    private int getCapacity(int pageSize) {
-        return (pageSize - PAGE_IDS_OFF) >>> 3; // /8
+    public int getCapacity(int pageSize) {
+        return (pageSize - PAGE_IDS_OFF) / Long.BYTES;
     }
 
     /**
@@ -149,7 +149,7 @@ public class PagesListNodeIO extends PageIO implements CompactablePageIO {
      * @return Item offset.
      */
     private int offset(int idx) {
-        return PAGE_IDS_OFF + 8 * idx;
+        return PAGE_IDS_OFF + Long.BYTES * idx;
     }
 
     /**
@@ -229,7 +229,7 @@ public class PagesListNodeIO extends PageIO implements CompactablePageIO {
         for (int i = 0; i < cnt; i++) {
             if (PageIdUtils.maskPartitionId(getAt(pageAddr, i)) == PageIdUtils.maskPartitionId(dataPageId)) {
                 if (i != cnt - 1)
-                    copyMemory(pageAddr, offset(i + 1), pageAddr, offset(i), 8 * (cnt - i - 1));
+                    copyMemory(pageAddr, offset(i + 1), pageAddr, offset(i), Long.BYTES * (cnt - i - 1));
 
                 setCount(pageAddr, cnt - 1);
 
@@ -282,4 +282,9 @@ public class PagesListNodeIO extends PageIO implements CompactablePageIO {
 
         sb.a("\n\t}\n]");
     }
+
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return (getCapacity(pageSize) - getCount(pageAddr)) * Long.BYTES;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
index 9063fc95faf..1a60dc537c6 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/AbstractDataPageIO.java
@@ -192,13 +192,13 @@ public abstract class AbstractDataPageIO<T extends Storable> extends PageIO impl
     public static final int ITEMS_OFF = FIRST_ENTRY_OFF + 2;
 
     /** */
-    private static final int ITEM_SIZE = 2;
+    public static final int ITEM_SIZE = 2;
 
     /** */
-    private static final int PAYLOAD_LEN_SIZE = 2;
+    public static final int PAYLOAD_LEN_SIZE = 2;
 
     /** */
-    private static final int LINK_SIZE = 8;
+    public static final int LINK_SIZE = 8;
 
     /** */
     private static final int FRAGMENTED_FLAG = 0b10000000_00000000;
@@ -322,6 +322,11 @@ public abstract class AbstractDataPageIO<T extends Storable> extends PageIO impl
         PageUtils.putShort(pageAddr, FREE_SPACE_OFF, (short)freeSpace);
     }
 
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return getFreeSpace(pageAddr);
+    }
+
     /**
      * Free space refers to a "max row size (without any data page specific overhead) which is guaranteed to fit into
      * this data page".
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
index eb7cedba1be..11ab12a8724 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusIO.java
@@ -469,6 +469,11 @@ public abstract class BPlusIO<L> extends PageIO implements CompactablePageIO {
             .a("\n]");
     }
 
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return (getMaxCount(pageAddr, pageSize) - getCount(pageAddr)) * getItemSize();
+    }
+
     /**
      * @param pageAddr Page address.
      * @return Offset after the last item.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
index c05f072cf39..edcfefc4f88 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/BPlusMetaIO.java
@@ -50,7 +50,7 @@ public class BPlusMetaIO extends PageIO {
     private static final int FLAGS_OFFSET = INLINE_SIZE_OFFSET + 2;
 
     /** */
-    private static final int CREATED_VER_OFFSET = FLAGS_OFFSET + 8;
+    private static final int CREATED_VER_OFFSET = FLAGS_OFFSET + Long.BYTES;
 
     /** */
     private static final int REFS_OFFSET = CREATED_VER_OFFSET + IgniteProductVersion.SIZE_IN_BYTES;
@@ -123,8 +123,8 @@ public class BPlusMetaIO extends PageIO {
      * @param pageSize Page size.
      * @return Max levels possible for this page size.
      */
-    private int getMaxLevels(long pageAddr, int pageSize) {
-        return (pageSize - refsOff) / 8;
+    public int getMaxLevels(long pageAddr, int pageSize) {
+        return (pageSize - refsOff) / Long.BYTES;
     }
 
     /**
@@ -145,7 +145,7 @@ public class BPlusMetaIO extends PageIO {
      * @return Offset for page reference.
      */
     private int offset(int lvl) {
-        return lvl * 8 + refsOff;
+        return lvl * Long.BYTES + refsOff;
     }
 
     /**
@@ -346,6 +346,11 @@ public class BPlusMetaIO extends PageIO {
             //TODO print firstPageIds by level
     }
 
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return (getMaxLevels(pageAddr, pageSize) - getLevelsCount(pageAddr)) * Long.BYTES;
+    }
+
     /**
      * @param pageAddr Page address.
      * @param inlineObjSupported Supports inline object flag.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
index bee16c3c9d6..dc0acda88a1 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIO.java
@@ -903,6 +903,15 @@ public abstract class PageIO {
      */
     protected abstract void printPage(long addr, int pageSize, GridStringBuilder sb) throws IgniteCheckedException;
 
+    /**
+     * Count of bytes that is currently free in this page and possibly can be used to place additional payload.
+     * @param pageSize Page size.
+     * @param pageAddr Page address.
+     *
+     * @return Free space.
+     */
+    public abstract int getFreeSpace(int pageSize, long pageAddr);
+
     /**
      * @param page Page.
      * @param out Output buffer.
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
index 170d66d5580..821f0b90791 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageMetaIO.java
@@ -274,4 +274,9 @@ public class PageMetaIO extends PageIO {
             .a(",\n\tcandidatePageCount=").a(getCandidatePageCount(addr))
             .a("\n]");
     }
+
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return 0;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
index b1adad790a4..25d58b8759c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PagePartitionCountersIO.java
@@ -210,7 +210,7 @@ public class PagePartitionCountersIO extends PageIO {
      * @param pageSize Page size.
      * @return Maximum number of items which can be stored in buffer.
      */
-    private int getCapacity(int pageSize) {
+    public int getCapacity(int pageSize) {
         return (pageSize - ITEMS_OFF) / ITEM_SIZE;
     }
 
@@ -230,4 +230,9 @@ public class PagePartitionCountersIO extends PageIO {
 
         sb.a("\n\t}\n]");
     }
+
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return (getCapacity(pageSize) - getCount(pageAddr)) * ITEM_SIZE;
+    }
 }
diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
index 97522ad0aa5..4367d33be7c 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/TrackingPageIO.java
@@ -491,4 +491,9 @@ public class TrackingPageIO extends PageIO {
 
         sb.a("}\n\t}\n]");
     }
+
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return 0;
+    }
 }
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/DummyPageIO.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/DummyPageIO.java
index b34353392b7..a0136c74bd6 100644
--- a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/DummyPageIO.java
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/DummyPageIO.java
@@ -46,6 +46,11 @@ public class DummyPageIO extends PageIO implements CompactablePageIO {
         sb.a("\n]");
     }
 
+    /** {@inheritDoc} */
+    @Override public int getFreeSpace(int pageSize, long pageAddr) {
+        return 0;
+    }
+
     /** {@inheritDoc} */
     @Override public void compactPage(ByteBuffer page, ByteBuffer out, int pageSize) {
         copyPage(page, out, pageSize);
diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIOFreeSizeTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIOFreeSizeTest.java
new file mode 100644
index 00000000000..dd5c59ba7e9
--- /dev/null
+++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/cache/persistence/tree/io/PageIOFreeSizeTest.java
@@ -0,0 +1,217 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.processors.cache.persistence.tree.io;
+
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.ignite.IgniteCheckedException;
+import org.apache.ignite.internal.cache.query.index.IndexProcessor;
+import org.apache.ignite.internal.cache.query.index.sorted.IndexRow;
+import org.apache.ignite.internal.cache.query.index.sorted.inline.io.AbstractInlineInnerIO;
+import org.apache.ignite.internal.processors.cache.persistence.freelist.PagesList;
+import org.apache.ignite.internal.processors.cache.persistence.freelist.io.PagesListMetaIO;
+import org.apache.ignite.internal.processors.cache.persistence.freelist.io.PagesListNodeIO;
+import org.apache.ignite.internal.processors.cache.persistence.pagemem.PageMetrics;
+import org.apache.ignite.internal.processors.metric.impl.LongAdderMetric;
+import org.apache.ignite.internal.util.GridUnsafe;
+import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.apache.ignite.internal.util.IgniteUtils.KB;
+
+/** Tests {@link PageIO#getFreeSpace(int, long)} method for different {@link PageIO} implementations. */
+@RunWith(Parameterized.class)
+public class PageIOFreeSizeTest extends GridCommonAbstractTest {
+    /** Page size. */
+    @Parameterized.Parameter
+    public int pageSz;
+
+    /** */
+    @Parameterized.Parameters(name = "pageSz={0}")
+    public static Collection<?> parameters() {
+        List<Object[]> params = new ArrayList<>();
+
+        for (long pageSz : new long[] {4 * KB, 8 * KB, 16 * KB})
+            params.add(new Object[] {(int)pageSz});
+
+        return params;
+    }
+
+    /** Page buffer. */
+    private ByteBuffer buf;
+
+    /** Page address. */
+    private long addr;
+
+    /** */
+    private final PageMetrics mock = new PageMetrics() {
+        final LongAdderMetric totalPages = new LongAdderMetric("a", null);
+
+        final LongAdderMetric idxPages = new LongAdderMetric("b", null);
+
+        @Override public LongAdderMetric totalPages() {
+            return totalPages;
+        }
+
+        @Override public LongAdderMetric indexPages() {
+            return idxPages;
+        }
+
+        @Override public void reset() {
+            // No-op.
+        }
+    };
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTestsStarted() throws Exception {
+        super.beforeTestsStarted();
+
+        IndexProcessor.registerIO();
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void beforeTest() throws Exception {
+        super.beforeTest();
+
+        buf = GridUnsafe.allocateBuffer(pageSz);
+        addr = GridUnsafe.bufferAddress(buf);
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void afterTest() throws Exception {
+        super.afterTest();
+
+        GridUnsafe.freeBuffer(buf);
+    }
+
+    /** */
+    @Test
+    public void testPagesListMetaIO() {
+        PagesListMetaIO io = io(PagesListMetaIO.VERSIONS);
+
+        int emptyPageSz = io.getCapacity(pageSz, addr) * PagesListMetaIO.ITEM_SIZE;
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.addTails(pageSz, addr, 1, new PagesList.Stripe[] {new PagesList.Stripe(1, true)}, 0);
+
+        assertEquals(emptyPageSz - PagesListMetaIO.ITEM_SIZE, io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testPagesListNodeIO() {
+        PagesListNodeIO io = io(PagesListNodeIO.VERSIONS);
+
+        int emptyPageSz = io.getCapacity(pageSz) * Long.BYTES;
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.addPage(addr, 42L, pageSz);
+        io.addPage(addr, 43L, pageSz);
+
+        assertEquals(emptyPageSz - 2 * Long.BYTES, io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testTrackingPageIO() {
+        assertEquals(0, io(TrackingPageIO.VERSIONS).getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testPageMetaIO() {
+        assertEquals(0, io(PageMetaIO.VERSIONS).getFreeSpace(pageSz, addr));
+        assertEquals(0, io(PageMetaIOV2.VERSIONS).getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testPartitionCountersIO() {
+        PagePartitionCountersIO io = io(PagePartitionCountersIO.VERSIONS);
+
+        int emptyPageSz = io.getCapacity(pageSz) * PagePartitionCountersIO.ITEM_SIZE;
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.writeCacheSizes(pageSz, addr, new byte[PagePartitionCountersIO.ITEM_SIZE * 3], 0);
+
+        assertEquals(emptyPageSz - 3 * PagePartitionCountersIO.ITEM_SIZE, io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testBPlusMetaIO() {
+        BPlusMetaIO io = io(BPlusMetaIO.VERSIONS);
+
+        int emptyPageSz = io.getMaxLevels(addr, pageSz) * Long.BYTES;
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.initRoot(addr, 42L, pageSz);
+
+        assertEquals(emptyPageSz - Long.BYTES, io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testBPlusIO() throws IgniteCheckedException {
+        BPlusInnerIO<IndexRow> io = io(AbstractInlineInnerIO.versions(42, false));
+
+        int emptyPageSz = io.getMaxCount(addr, pageSz) * io.getItemSize();
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.insert(addr, 0, null, new byte[42], 42, true);
+
+        // Inner pages contains extra link to next level.
+        assertEquals(emptyPageSz - (42 + Long.BYTES), io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    @Test
+    public void testDataPageIO() throws IgniteCheckedException {
+        DataPageIO io = io(DataPageIO.VERSIONS);
+
+        int emptyPageSz = pageSz - DataPageIO.ITEMS_OFF - DataPageIO.ITEM_SIZE - DataPageIO.PAYLOAD_LEN_SIZE - DataPageIO.LINK_SIZE;
+
+        assertEquals(emptyPageSz, io.getFreeSpace(pageSz, addr));
+
+        io.addRow(addr, new byte[42], pageSz);
+
+        // See AbstractDataPageIO#getPageEntrySize(int, byte)
+        assertEquals(emptyPageSz - 42 - 4 /* data size added. */, io.getFreeSpace(pageSz, addr));
+    }
+
+    /** */
+    private <I extends PageIO> I io(IOVersions<I> versions) {
+        I io = versions.latest();
+
+        GridUnsafe.setMemory(addr, pageSz, (byte)0);
+
+        io.initNewPage(addr, 1, pageSz, mock);
+
+        return io;
+    }
+}
diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite5.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite5.java
index aeb1865904c..d1f332e4468 100644
--- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite5.java
+++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgnitePdsTestSuite5.java
@@ -39,6 +39,7 @@ import org.apache.ignite.internal.processors.cache.persistence.pagemem.PagesWrit
 import org.apache.ignite.internal.processors.cache.persistence.pagemem.SpeedBasedThrottleBreakdownTest;
 import org.apache.ignite.internal.processors.cache.persistence.pagemem.UsedPagesMetricTest;
 import org.apache.ignite.internal.processors.cache.persistence.pagemem.UsedPagesMetricTestPersistence;
+import org.apache.ignite.internal.processors.cache.persistence.tree.io.PageIOFreeSizeTest;
 import org.apache.ignite.internal.processors.cache.persistence.tree.io.TrackingPageIOTest;
 import org.apache.ignite.internal.processors.cache.persistence.wal.CpTriggeredWalDeltaConsistencyTest;
 import org.apache.ignite.internal.processors.cache.persistence.wal.ExplicitWalDeltaConsistencyTest;
@@ -80,6 +81,7 @@ public class IgnitePdsTestSuite5 {
         GridTestUtils.addTestIfNeeded(suite, PageMemoryImplTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, PageIdDistributionTest.class, ignoredTests);
         GridTestUtils.addTestIfNeeded(suite, TrackingPageIOTest.class, ignoredTests);
+        GridTestUtils.addTestIfNeeded(suite, PageIOFreeSizeTest.class, ignoredTests);
 
         // BTree tests with store page memory.
         GridTestUtils.addTestIfNeeded(suite, BPlusTreePageMemoryImplTest.class, ignoredTests);