3939import com .arcadedb .serializer .json .JSONArray ;
4040import com .arcadedb .serializer .json .JSONObject ;
4141import com .arcadedb .utility .FileUtils ;
42+ import com .arcadedb .utility .IntIntHashMap ;
4243
4344import java .io .*;
4445import java .util .*;
@@ -88,7 +89,11 @@ public class LocalBucket extends PaginatedComponent implements Bucket {
8889 protected final int contentHeaderSize ;
8990 private final int maxRecordsInPage = DEF_MAX_RECORDS_IN_PAGE ;
9091 private final AtomicLong cachedRecordCount = new AtomicLong (-1 );
91- private final TreeMap <Integer , Integer > freeSpaceInPages = new TreeMap <>();
92+ // pageId → free-space-bytes. TreeMap ordering is unused (verified by grep), so a primitive
93+ // open-addressing map saves memory and avoids Integer boxing on every read/write/remove on
94+ // the page-allocation hot path. Bounded by MAX_PAGES_GATHER_STATS (100). Single-threaded
95+ // access is enforced by `synchronized (freeSpaceInPages)` blocks at every callsite.
96+ private final IntIntHashMap freeSpaceInPages = new IntIntHashMap ();
9297 private final REUSE_SPACE_MODE reuseSpaceMode ;
9398 private long timeOfLastStats = 0L ;
9499 private long changesFromLastStats = 0L ;
@@ -1875,34 +1880,55 @@ private PageAnalysis findAvailableSpace(final int currentPageId, final int space
18751880 private PageAnalysis findAvailableSpaceFromStatistics (final int currentPageId , final int spaceNeeded ,
18761881 final boolean multiPageRecord )
18771882 throws IOException {
1883+ // Snapshot keys/values into local arrays so we can safely mutate freeSpaceInPages
1884+ // (put/remove) inside the loop body. The snapshot is bounded by MAX_PAGES_GATHER_STATS (100).
1885+ //
1886+ // The snapshot is sorted by pageId to preserve the deterministic "fill lowest pageId first"
1887+ // allocation behavior that the previous TreeMap-backed implementation provided implicitly via
1888+ // its ordered iteration. Tests like RandomDeleteTest depend on this so that re-inserts after
1889+ // bulk deletes land at the same RIDs as the original inserts.
1890+ final int snapSize = freeSpaceInPages .size ();
1891+ final int [] snapPageIds = new int [snapSize ];
1892+ final int [] snapPageStats = new int [snapSize ];
1893+ final int [] cursor = { 0 };
1894+ freeSpaceInPages .forEach ((k , v ) -> {
1895+ snapPageIds [cursor [0 ]] = k ;
1896+ snapPageStats [cursor [0 ]] = v ;
1897+ cursor [0 ]++;
1898+ });
1899+ sortByPageIdAscending (snapPageIds , snapPageStats , snapSize );
1900+
18781901 PageAnalysis bestPageAnalysis = null ;
1879- List <Integer > pagesToRemove = null ;
1880- for (Map .Entry <Integer , Integer > entry : freeSpaceInPages .entrySet ()) {
1881- final int pageId = entry .getKey ();
1902+ int [] pagesToRemove = null ;
1903+ int pagesToRemoveCount = 0 ;
1904+ for (int s = 0 ; s < snapSize ; s ++) {
1905+ final int pageId = snapPageIds [s ];
18821906 if (pageId == currentPageId )
18831907 // ALREADY EVALUATED
18841908 continue ;
18851909
1886- final Integer pageStats = entry . getValue () ;
1910+ final int pageStats = snapPageStats [ s ] ;
18871911
18881912 if (pageStats >= spaceNeeded ) {
18891913 // CHECK IF THE SPACE AVAILABLE IS REAL
18901914 final PageAnalysis pageAnalysis = getAvailableSpaceInPage (pageId , spaceNeeded , multiPageRecord );
18911915
18921916 if (pageAnalysis .totalRecordsInPage >= maxRecordsInPage ) {
1893- pagesToRemove = pagesToRemove == null ? new ArrayList <>() : pagesToRemove ;
1894- pagesToRemove .add (pageId );
1917+ if (pagesToRemove == null )
1918+ pagesToRemove = new int [snapSize ];
1919+ pagesToRemove [pagesToRemoveCount ++] = pageId ;
18951920 continue ;
18961921 }
18971922
18981923 if (!pageAnalysis .createNewPage ) {
18991924 if (pageAnalysis .spaceAvailableInCurrentPage != pageStats ) {
19001925 // LOW COST UPDATE OF STATISTICS
19011926 if (pageAnalysis .spaceAvailableInCurrentPage < MINIMUM_SPACE_LEFT_IN_PAGE ) {
1902- pagesToRemove = pagesToRemove == null ? new ArrayList <>() : pagesToRemove ;
1903- pagesToRemove .add (pageId );
1927+ if (pagesToRemove == null )
1928+ pagesToRemove = new int [snapSize ];
1929+ pagesToRemove [pagesToRemoveCount ++] = pageId ;
19041930 } else
1905- entry . setValue ( pageAnalysis .spaceAvailableInCurrentPage );
1931+ freeSpaceInPages . put ( pageId , pageAnalysis .spaceAvailableInCurrentPage );
19061932 }
19071933
19081934 if (multiPageRecord && pageAnalysis .page .pageId .getPageNumber () == 0 && pageAnalysis .availablePositionIndex == 0 )
@@ -1916,8 +1942,8 @@ private PageAnalysis findAvailableSpaceFromStatistics(final int currentPageId, f
19161942 }
19171943
19181944 if (pagesToRemove != null )
1919- for (Integer pageId : pagesToRemove )
1920- freeSpaceInPages .remove (pageId );
1945+ for (int i = 0 ; i < pagesToRemoveCount ; i ++ )
1946+ freeSpaceInPages .remove (pagesToRemove [ i ], - 1 );
19211947
19221948 return bestPageAnalysis ;
19231949 }
@@ -1974,35 +2000,36 @@ private void updatePageStatistics(final int pageId, final int availableSpace, fi
19742000
19752001 synchronized (freeSpaceInPages ) {
19762002 if (availableSpace + delta == 0 )
1977- freeSpaceInPages .remove (pageId );
2003+ freeSpaceInPages .remove (pageId , - 1 );
19782004 else {
19792005 final int usableSpaceInPage = getPageSize () - contentHeaderSize ;
19802006
1981- final Integer freeSpaceInPage = freeSpaceInPages .get (pageId );
1982- final int prevSpace = availableSpace == 0 && freeSpaceInPage != null ? freeSpaceInPage : availableSpace ;
2007+ final boolean hasEntry = freeSpaceInPages .containsKey (pageId );
2008+ final int existingFreeSpace = hasEntry ? freeSpaceInPages .get (pageId , 0 ) : 0 ;
2009+ final int prevSpace = availableSpace == 0 && hasEntry ? existingFreeSpace : availableSpace ;
19832010 final int newSpace = prevSpace + delta ;
19842011
1985- if (freeSpaceInPage != null ) {
2012+ if (hasEntry ) {
19862013 if (newSpace <= MINIMUM_SPACE_LEFT_IN_PAGE || (freeSpaceInPages .size () >= MAX_PAGES_GATHER_STATS
19872014 && newSpace * 100 / usableSpaceInPage < GATHER_STATS_MIN_SPACE_PERC ))
1988- freeSpaceInPages .remove (pageId );
2015+ freeSpaceInPages .remove (pageId , - 1 );
19892016 else
19902017 freeSpaceInPages .put (pageId , newSpace );
19912018 } else if (newSpace * 100 / usableSpaceInPage >= GATHER_STATS_MIN_SPACE_PERC ) {
19922019 if (freeSpaceInPages .size () >= MAX_PAGES_GATHER_STATS ) {
19932020 // REMOVE THE SMALLEST PAGE
1994- int lowestPageId = - 1 ;
1995- int lowestPageSpace = - 1 ;
2021+ final int [] lowestPageId = { - 1 } ;
2022+ final int [] lowestPageSpace = { - 1 } ;
19962023
1997- for ( Map . Entry < Integer , Integer > entry : freeSpaceInPages .entrySet ()) {
1998- if (lowestPageId < 0 || entry . getValue () < lowestPageSpace ) {
1999- lowestPageId = entry . getKey () ;
2000- lowestPageSpace = entry . getValue () ;
2024+ freeSpaceInPages .forEach (( k , v ) -> {
2025+ if (lowestPageId [ 0 ] < 0 || v < lowestPageSpace [ 0 ] ) {
2026+ lowestPageId [ 0 ] = k ;
2027+ lowestPageSpace [ 0 ] = v ;
20012028 }
2002- }
2029+ });
20032030
2004- if (lowestPageId > -1 && lowestPageSpace < newSpace ) {
2005- freeSpaceInPages .remove (lowestPageId );
2031+ if (lowestPageId [ 0 ] > -1 && lowestPageSpace [ 0 ] < newSpace ) {
2032+ freeSpaceInPages .remove (lowestPageId [ 0 ], - 1 );
20062033 freeSpaceInPages .put (pageId , newSpace );
20072034 }
20082035 } else
@@ -2022,6 +2049,26 @@ public void setPageStatistics(final JSONArray pages) {
20222049 }
20232050 }
20242051
2052+ /**
2053+ * Insertion sort on two parallel arrays, ordering by ascending {@code pageIds[i]}. Bounded to
2054+ * MAX_PAGES_GATHER_STATS (100) entries so insertion sort is the right choice (low constant,
2055+ * great cache behavior, in-place).
2056+ */
2057+ private static void sortByPageIdAscending (final int [] pageIds , final int [] pageStats , final int n ) {
2058+ for (int i = 1 ; i < n ; i ++) {
2059+ final int kPage = pageIds [i ];
2060+ final int kStats = pageStats [i ];
2061+ int j = i - 1 ;
2062+ while (j >= 0 && pageIds [j ] > kPage ) {
2063+ pageIds [j + 1 ] = pageIds [j ];
2064+ pageStats [j + 1 ] = pageStats [j ];
2065+ j --;
2066+ }
2067+ pageIds [j + 1 ] = kPage ;
2068+ pageStats [j + 1 ] = kStats ;
2069+ }
2070+ }
2071+
20252072 public JSONObject getStatistics () {
20262073 final JSONObject json = new JSONObject ();
20272074
@@ -2031,10 +2078,7 @@ public JSONObject getStatistics() {
20312078
20322079 final JSONArray pages = new JSONArray ();
20332080 synchronized (freeSpaceInPages ) {
2034- for (Map .Entry <Integer , Integer > entry : freeSpaceInPages .entrySet ())
2035- pages .put (//
2036- new JSONObject ().put ("id" , entry .getKey ()).put ("free" , entry .getValue ())//
2037- );
2081+ freeSpaceInPages .forEach ((k , v ) -> pages .put (new JSONObject ().put ("id" , k ).put ("free" , v )));
20382082 }
20392083 json .put ("pages" , pages );
20402084
0 commit comments