Project

General

Profile

6720_runtime_feedback.patch

Teodor Gorghe, 06/10/2026 05:36 AM

Download (214 KB)

View differences:

new/src/com/goldencode/p2j/jmx/FwdServerJMX.java 2026-06-10 08:13:45 +0000
248 248
      OrmDataSetParam,
249 249
      /** Instrumentation for <code>DataSet.createDynamicDataSet</code>. */
250 250
      OrmDynamicDataSetParam,
251
      
252
      /** Lazily hydrated an new record (not rehydrated from cache) */
253
      HydratorNewRecord,
254
      /** Lazily hydrated a record (including rehydration from cache) */
255
      HydratorLazyRecord,
256
      /** Fully or partially hydrated a record, but not lazy */
257
      HydratorNonLazyRecord,
258
      /** The hydrator token matched the result-set token */
259
      HydratorHit,
260
      /** The hydrator token mismatched the result-set token */
261
      HydratorMiss,
262
      /** The hydrator fallbacked to refresh and succeeded */
263
      HyratorSuccessfullRefresh,
264
      /** The hydrator fallbacked to refresh and failed */
265
      HyratorUnsuccessfullRefresh,
266
      /** Created a {@code ScrollableResults} instance with lazy results */
267
      HydratorLazyScrollable,
268
      /** Created a {@code ScrollableResults} instance without lazy results */
269
      HydratorNonLazyScrollable,
270
      /** Executed uniqueResult instance with lazy results */
271
      HydratorLazyUnique,
272
      /** Executed uniqueResult instance without lazy results */
273
      HydratorNonLazyUnique,
251 274

  
252 275
      /** Instrumentation for <code>OutputTableCopier.finished</code> */
253 276
      OutputTableCopier,
......
489 512
   public static enum Counter
490 513
   implements OptionalCounter
491 514
   {
515
      TempTotalFields,
516
      PersistentTotalFields,
517
      TempHydratedRecordsWhenSessionCloses,
518
      PersistentHydratedRecordsWhenSessionCloses,
519
      TempHydratedFieldsWhenSessionCloses,
520
      PersistentHydratedFieldsWhenSessionCloses,
521
      PersistentHydratedFieldsWithLZS,
522
      PersistentHydratedFieldsWithLoader,
523
      TempHydratedFieldsWithLZS,
524
      TempHydratedFieldsWithLoader,
525
      
526
      
527
      /** Lazily hydrated an new record (not rehydrated from cache) */
528
      HydratorNewRecord,
529
      /** Lazily hydrated a record (including rehydration from cache) */
530
      HydratorLazyRecord,
531
      /** Fully or partially hydrated a record, but not lazy */
532
      HydratorNonLazyRecord,
533
      /** The hydrator token matched the result-set token */
534
      HydratorHit,
535
      /** The hydrator token mismatched the result-set token */
536
      HydratorMiss,
537
      /** The hydrator fallbacked to refresh and succeeded */
538
      HyratorSuccessfullRefresh,
539
      /** The hydrator fallbacked to refresh and failed */
540
      HyratorUnsuccessfullRefresh,
541
      /** Created a {@code ScrollableResults} instance with lazy results */
542
      HydratorLazyScrollable,
543
      /** Created a {@code ScrollableResults} instance without lazy results */
544
      HydratorNonLazyScrollable,
545
      /** Executed uniqueResult instance with lazy results */
546
      HydratorLazyUnique,
547
      /** Executed uniqueResult instance without lazy results */
548
      HydratorNonLazyUnique,
549
      /** A call-site's field-usage profile locked in the EAGER hydration verdict */
550
      HydrationVerdictEager,
551
      /** A call-site's field-usage profile locked in the LAZY hydration verdict */
552
      HydrationVerdictLazy,
553

  
492 554
      /** Incomning network traffic */
493 555
      NetworkReads,
494 556
      /** Outgoing network traffic */
new/src/com/goldencode/p2j/persist/Persistence.java 2026-06-10 08:15:57 +0000
751 751
import java.util.function.*;
752 752
import java.util.logging.*;
753 753
import com.goldencode.cache.*;
754
import com.goldencode.p2j.jmx.*;
754 755
import com.goldencode.p2j.persist.deploy.*;
755 756
import com.goldencode.p2j.persist.dialect.*;
756 757
import com.goldencode.p2j.persist.dirty.DirtyShareSupport;
......
925 926
    * name.
926 927
    */
927 928
   private final ConcurrentHashMap<String, Tenant> tenantData = new ConcurrentHashMap<>();
928
   
929

  
930
   /**
931
    * Bounded per-database map of runtime field-usage profiles, keyed by FQL. Used by the
932
    * lazy-feedback hydration layer to decide eager vs lazy hydration per call-site. Hosted on the
933
    * per-database {@code Persistence} singleton (not the per-session context) so that all sessions
934
    * against this database share the learned verdicts, while different databases stay isolated. The
935
    * map is capped (see {@code FieldUsageProfile.maxProfiles()}): dynamic queries generate unbounded
936
    * distinct FQL strings, so once the cap is hit new call-sites get no profile and hydrate eagerly.
937
    */
938
   private final ConcurrentHashMap<String, FieldUsageProfile> usageProfiles = new ConcurrentHashMap<>();
939

  
929 940
   /**
930 941
    * A container class which stores tenant-specific objects. Although the persistence tracks all these
931 942
    * {@code Tenant}, at any given moment, only one of is active in each user context.
......
1036 1047
   {
1037 1048
      return PersistenceFactory.getInstance(name);
1038 1049
   }
1039
   
1050

  
1051
   /**
1052
    * Resolve (creating if necessary) the runtime field-usage profile for the given FQL call-site.
1053
    * Used by the lazy-feedback hydration layer to decide eager vs lazy hydration per call-site.
1054
    *
1055
    * @param   fql
1056
    *          The FQL identifying the call-site.
1057
    *
1058
    * @return  The shared profile for the call-site, or {@code null} when the feature is disabled or
1059
    *          the per-database profile cap has been reached (in which case the call-site hydrates
1060
    *          eagerly, the safe default).
1061
    */
1062
   public FieldUsageProfile getOrCreateProfile(String fql)
1063
   {
1064
      if (!FieldUsageProfile.isEnabled() || fql == null)
1065
      {
1066
         return null;
1067
      }
1068

  
1069
      FieldUsageProfile profile = usageProfiles.get(fql);
1070
      if (profile != null)
1071
      {
1072
         return profile;
1073
      }
1074

  
1075
      // hard create-cap: dynamic queries produce unbounded distinct FQL, so refuse new profiles once
1076
      // full rather than evicting (a missing profile simply means EAGER). The size check races with
1077
      // concurrent creators, but only ever overshoots the cap by the number of racing threads.
1078
      if (usageProfiles.size() >= FieldUsageProfile.maxProfiles())
1079
      {
1080
         return null;
1081
      }
1082

  
1083
      return usageProfiles.computeIfAbsent(fql, FieldUsageProfile::new);
1084
   }
1085

  
1040 1086
   /**
1041 1087
    * Initialize various infrastructure components of the persistence framework:
1042 1088
    * <ul>
......
1947 1993
      {
1948 1994
         local.closeSession(true);
1949 1995
         ErrorEntry ee = exc.getErrorEntry();
1950
         ErrorManager.recordOrShowError(
1951
                  new int[] {ee.num}, 
1952
                  new String[] {ee.text}, 
1953
                  true, 
1954
                  ee.prefix, 
1955
                  false,
1956
                  false,
1957
                  false,
1958
                  true);
1996
         ErrorManager.recordOrShowError(new int[] {ee.num}, 
1997
                                        new String[] {ee.text},
1998
                                        true,
1999
                                        ee.prefix,
2000
                                        false,
2001
                                        false,
2002
                                        false,
2003
                                        true);
1959 2004
         handleException(ErrorManager.buildErrorText(ee.num, ee.text, ee.prefix, false), exc);
1960 2005
      }
1961 2006
      catch (PersistenceException exc)
......
5248 5293
      /** FQL statement */
5249 5294
      private final String fql;
5250 5295
      
5296
      /** Database dialect */
5297
      private final Dialect dialect;
5298
      
5251 5299
      /** Does the query was generated with maximum results option? */
5252 5300
      private final boolean hasMaxResults;
5253 5301
      
......
5268 5316
       * 
5269 5317
       * @param   hql
5270 5318
       *          FQL statement
5319
       * @param   dialect
5320
       *          Database dialect.
5271 5321
       * @param   maxResults
5272 5322
       *          Maximum results.
5273 5323
       * @param   startOffset
5274 5324
       *          Starting row offset.
5275 5325
       */
5276
      QueryKey(String hql, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
5326
      QueryKey(String hql, Dialect dialect, int maxResults, int startOffset, DataRelation relation, boolean lazyMode)
5277 5327
      {
5278 5328
         this.fql = hql;
5329
         this.dialect = dialect;
5279 5330
         this.hasMaxResults = maxResults > 0;
5280 5331
         this.hasStartOffset = startOffset > 0;
5281 5332
         this.whereStr = relation == null ? "" : relation.getWhereString().toJavaType();
5282 5333
         this.lazyMode = lazyMode;
5283 5334
         
5284 5335
         int result = 17;
5285
         result = 37 * result + hql.hashCode();
5336
         result = 37 * result + fql.hashCode();
5337
         result = 37 * result + dialect.hashCode();
5286 5338
         result = 37 * result + (hasMaxResults ? 1 : 0) + (hasStartOffset ? 2 : 0);
5287 5339
         result = 37 * result + whereStr.hashCode();
5288 5340
         result = 37 * result + (lazyMode ? 1 : 0);
......
5308 5360
         QueryKey that = (QueryKey) o;
5309 5361
         
5310 5362
         return this.hash == that.hash                     &&
5363
                this.dialect.equals(that.dialect)          &&
5311 5364
                this.hasMaxResults == that.hasMaxResults   &&
5312 5365
                this.hasStartOffset == that.hasStartOffset &&
5313 5366
                this.fql.equals(that.fql) &&
......
5334 5387
      @Override
5335 5388
      public String toString()
5336 5389
      {
5337
         return fql + "[" + hasMaxResults + ":" + hasStartOffset + "];" + whereStr;
5390
         return fql + "[" + hasMaxResults + ":" + hasStartOffset + "] (" + dialect + ") " + whereStr;
5338 5391
      }
5339 5392
   }
5340 5393
   
......
5561 5614
         Query query = null;
5562 5615
         fql = fql.trim(); // just in case
5563 5616
         
5564
         // TODO: use the dialect also? I.e: is it possible to have different SQL for same FQL
5565
         //       if different dialects are used?
5566
         QueryKey key = new QueryKey(fql, maxResults, startOffset, relation, lazyMode);
5617
         QueryKey key = new QueryKey(fql, dialect, maxResults, startOffset, relation, lazyMode);
5567 5618
         
5568
         synchronized (staticQueryCache)
5619
         query = staticQueryCache.get(key);
5620
         if (query == null)
5569 5621
         {
5570
            query = staticQueryCache.get(key);
5571
            if (query == null)
5622
            Consumer<Query> c = (q) ->
5572 5623
            {
5573
               query = Session.createQuery(q ->
5574
               {
5575
                  synchronized (staticQueryCache)
5576
                  {
5577
                     // NOTE: the FQL string is not converted to SQL at this time. The conversion
5578
                     //       is performed once, lazily, when the first navigation is required by a
5579
                     //       call of list(), scroll(), or uniqueResult() on the new query
5580
                     
5581
                     staticQueryCache.put(key, q);
5582
                  }
5583
               },
5584
               fql);
5585
            }
5624
               // NOTE: the FQL string is not converted to SQL at this time. The conversion
5625
               //       is performed once, lazily, when the first navigation is required by a
5626
               //       call of list(), scroll(), or uniqueResult() on the new query
5627
               
5628
               staticQueryCache.put(key, q);
5629
            };
5630
            
5631
            query = Session.createQuery(c, fql);
5586 5632
         }
5587 5633
         
5588 5634
         // update [maxResults] and [startOffset] because the cached query might have set 
......
5979 6025
                  try
5980 6026
                  {
5981 6027
                     session.associate(record);
6028
                     record.refreshHydrator(session);
5982 6029
                  }
5983 6030
                  catch (StaleRecordException exc)
5984 6031
                  {
......
6142 6189
         try
6143 6190
         {
6144 6191
            closingSession = true;
6145
            
6192
            hydrateActiveRecords();
6146 6193
            if (isTransactionOpen())
6147 6194
            {
6148 6195
               if (LOG.isLoggable(Level.WARNING))
......
6221 6268
         }
6222 6269
      }
6223 6270
      
6271
      private void hydrateActiveRecords()
6272
      {
6273
         Iterator<RecordBuffer> iter = bufferManager.activeBuffers(Persistence.this);
6274
         
6275
         Record record;
6276
         
6277
         while (iter.hasNext())
6278
         {
6279
            record = iter.next().getCurrentRecord();
6280
            if (record.primaryKey() < 0)
6281
            {
6282
               continue;
6283
            }
6284
            
6285
            record.hydrateRemainingFields();
6286
         }
6287
      }
6288
      
6224 6289
      /**
6225 6290
       * Notify all registered session listeners that the current ORM session is about to
6226 6291
       * close or the current, implicit transaction is being committed.  Listeners which were set
new/src/com/goldencode/p2j/persist/Record.java 2026-06-10 06:43:53 +0000
102 102
import java.util.*;
103 103
import java.util.function.*;
104 104

  
105
import com.goldencode.p2j.jmx.*;
105 106
import com.goldencode.p2j.oo.lang.BaseObject;
106 107
import com.goldencode.p2j.persist.orm.*;
107 108
import com.goldencode.p2j.util.*;
......
115 116
extends BaseRecord
116 117
implements PropertiesDescriptor
117 118
{
119
   private static final SimpleCounter HYDRATE_RECORDS_WHEN_SESSION_CLOSE =
120
      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedRecordsWhenSessionCloses);
121
   
122
   private static final SimpleCounter TOTAL_FIELDS =
123
      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentTotalFields);
124
   
125
   private static final SimpleCounter HYDRATE_FIELD_WHEN_SESSION_CLOSES =
126
      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWhenSessionCloses);
127

  
128
   private static final SimpleCounter HYDRATED_FIELDS_LZS =
129
      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWithLZS);
130
   
131
   private static final SimpleCounter HYDRATED_FIELDS_LOADER =
132
      SimpleCounter.getInstance(FwdServerJMX.Counter.PersistentHydratedFieldsWithLoader);
133
   
118 134
   /**
119 135
    * Default constructor needed by DMOs extending this class.
120 136
    */
......
163 179
   {
164 180
      checkIncomplete(offset);
165 181

  
166
      Integer datum = (Integer) data[offset];
182
      Integer datum = (Integer) getDatum(offset);
167 183
      
168 184
      return datum != null ? new integer(datum) : new integer();
169 185
   }
......
272 288
   {
273 289
      checkIncomplete(offset);
274 290

  
275
      OffsetDateTime datum = (OffsetDateTime) data[offset];
291
      OffsetDateTime datum = (OffsetDateTime) getDatum(offset);
276 292
      
277 293
      return datum != null ? new datetimetz(datum) : new datetimetz();
278 294
   }
......
362 378
      }
363 379
   }
364 380
   
381
   @Override
382
   protected void countHydrateRecordWhenSessionCloses(long count)
383
   {
384
      HYDRATE_RECORDS_WHEN_SESSION_CLOSE.update(count);
385
   }
386
   
387
   @Override
388
   protected void countTotalFields(long count)
389
   {
390
      TOTAL_FIELDS.update(count);
391
   }
392
   
393
   @Override
394
   protected void countHydratedFieldsWithLoader(long count)
395
   {
396
      HYDRATED_FIELDS_LOADER.update(count);
397
   }
398
   
399
   @Override
400
   protected void countHydratedFieldsWithLZS(long count)
401
   {
402
      HYDRATED_FIELDS_LZS.update(count);
403
   }
404
   
405
   @Override
406
   protected void countHydratedFieldWhenSessionCloses(long count)
407
   {
408
      HYDRATE_FIELD_WHEN_SESSION_CLOSES.update(count);
409
   }
410
   
365 411
   /**
366 412
    * Get an {@code datetime} value for the datum at the given offset in the property array.
367 413
    * <p>
......
378 424
   {
379 425
      checkIncomplete(offset);
380 426

  
381
      Timestamp datum = (Timestamp) data[offset];
427
      Timestamp datum = (Timestamp) getDatum(offset);
382 428
      
383 429
      return datum != null ? new datetime(datum) : new datetime();
384 430
   }
......
483 529
   {
484 530
      checkIncomplete(offset);
485 531

  
486
      Date datum = (Date) data[offset];
532
      Date datum = (Date) getDatum(offset);
487 533
      
488 534
      return datum != null ? new date(datum) : new date();
489 535
   }
......
588 634
   {
589 635
      checkIncomplete(offset);
590 636

  
591
      Boolean datum = (Boolean) data[offset];
637
      Boolean datum = (Boolean) getDatum(offset);
592 638
      
593 639
      return datum != null ? new logical(datum) : new logical();
594 640
   }
......
691 737
    */
692 738
   public final int64 _getInt64(int offset)
693 739
   {
694
      Long datum = (Long) data[offset];
740
      checkIncomplete(offset);
741
      
742
      Long datum = (Long) getDatum(offset);
695 743
      
696 744
      return datum != null ? new int64(datum) : new int64();
697 745
   }
......
800 848
   {
801 849
      checkIncomplete(offset);
802 850

  
803
      BigDecimal datum = (BigDecimal) data[offset];
851
      BigDecimal datum = (BigDecimal) getDatum(offset);
804 852
      
805 853
      return datum != null ? new decimal(datum) : new decimal();
806 854
   }
......
917 965
   public final character _getCharacter(int offset)
918 966
   {
919 967
      checkIncomplete(offset);
920

  
921
      String datum = (String) data[offset];
968
      
969
      String datum = (String) getDatum(offset);
922 970
      
923 971
      if (datum != null)
924 972
      {
......
1030 1078
   {
1031 1079
      checkIncomplete(offset);
1032 1080

  
1033
      Long datum = (Long) data[offset];
1081
      Long datum = (Long) getDatum(offset);
1034 1082
      
1035 1083
      return datum != null ? new rowid(datum) : new rowid();
1036 1084
   }
......
1137 1185
   {
1138 1186
      checkIncomplete(offset);
1139 1187

  
1140
      Long datum = (Long) data[offset];
1188
      Long datum = (Long) getDatum(offset);
1141 1189
      
1142 1190
      return datum != null ? handle.fromResourceId(datum, false) : new handle();
1143 1191
   }
......
1244 1292
   {
1245 1293
      checkIncomplete(offset);
1246 1294

  
1247
      Long datum = (Long) data[offset];
1295
      Long datum = (Long) getDatum(offset);
1248 1296
      
1249 1297
      return datum != null ? new recid(datum) : new recid();
1250 1298
   }
......
1350 1398
   {
1351 1399
      checkIncomplete(offset);
1352 1400

  
1353
      byte[] datum = (byte[]) data[offset];
1401
      byte[] datum = (byte[]) getDatum(offset);
1354 1402
      
1355 1403
      return datum != null ? new blob(datum) : new blob();
1356 1404
   }
......
1457 1505
   {
1458 1506
      checkIncomplete(offset);
1459 1507

  
1460
      return new clob((String) data[offset], getCodePage(offset));
1508
      return new clob((String) getDatum(offset), getCodePage(offset));
1461 1509
   }
1462 1510
   
1463 1511
   /**
......
1560 1608
   {
1561 1609
      checkIncomplete(offset);
1562 1610

  
1563
      String datum = (String) data[offset];
1611
      String datum = (String) getDatum(offset);
1564 1612
      
1565 1613
      return datum != null ? comhandle.fromString(datum) : new comhandle();
1566 1614
   }
......
1665 1713
   {
1666 1714
      checkIncomplete(offset);
1667 1715

  
1668
      Long datum = (Long) data[offset];
1716
      Long datum = (Long) getDatum(offset);
1669 1717
      
1670 1718
      // the instance must be typed to Progress.Lang.Object
1671 1719
      object<?> res = new object<>(BaseObject.class);
......
1781 1829
   {
1782 1830
      checkIncomplete(offset);
1783 1831

  
1784
      byte[] datum = (byte[]) data[offset];
1832
      byte[] datum = (byte[]) getDatum(offset);
1785 1833
      
1786 1834
      return datum != null ? new raw(datum) : raw.instantiateUnknownRaw();
1787 1835
   }
......
1956 2004
      RecordMeta meta = _recordMeta();
1957 2005
      if (oneLine)
1958 2006
      {
1959
         int last = data.length;
2007
         int last = getLength();
1960 2008
         int start = (this instanceof TempTableRecord) ? 5 : 0;
1961 2009
         sb.append(meta.legacyName).append('/').append(meta.tables[0]).append(':').append(id).append(" {");
1962 2010
         for (int i = start; i < last; i++)
......
1966 2014
               sb.append(", ");
1967 2015
            }
1968 2016
            
1969
            boolean isChar = data[i] instanceof String;
2017
            boolean isChar = getSimpleDatum(i) instanceof String;
1970 2018
            if (isChar)
1971 2019
            {
1972 2020
               sb.append("\"");
......
1974 2022
            
1975 2023
            boolean incomplete = readFields != null && !readFields.get(i);
1976 2024
            
1977
            sb.append(data[i] == null ? incomplete ? "n/a" : "?" : data[i]);
2025
            Object datum = getSimpleDatum(i);
2026
            sb.append(datum == null ? incomplete ? "n/a" : "?" : datum);
1978 2027
            if (isChar)
1979 2028
            {
1980 2029
               sb.append("\"");
......
1997 2046
            int extent = pms[i].getExtent();
1998 2047
            if (extent == 0)
1999 2048
            {
2000
               sb.append(data[i]);
2049
               sb.append(getSimpleDatum(i));
2001 2050
            }
2002 2051
            else
2003 2052
            {
2004 2053
               for (int k = 0; k < extent; k++)
2005 2054
               {
2006 2055
                  sb.append(k == 0 ? "[" : "");
2007
                  sb.append(data[i + k]);
2056
                  sb.append(getSimpleDatum(i + k));
2008 2057
               }
2009 2058
               
2010 2059
               i += extent - 1;
......
2042 2091
         Object val = properties.get(prop.getLegacyName());
2043 2092
         val = f.apply(prop, val);
2044 2093

  
2094
         Object[] data = getData();
2045 2095
         if (prop.getExtent() > 0)
2046 2096
         {
2047 2097
            for (int j = 0; j < prop.getExtent(); j++, i++)
......
2050 2100
               
2051 2101
               BaseDataType fval = (BaseDataType) Array.get(val, j);
2052 2102
               prop.getDataHandler().setField(data, fval, i, prop);
2103
               setLivePropsAtOffset(i);
2053 2104
            }
2054 2105
         }
2055 2106
         else
2056 2107
         {
2057 2108
            prop.getDataHandler().setField(data, (BaseDataType) val, i, prop);
2109
            setLivePropsAtOffset(i);
2058 2110
         }
2059 2111
      }
2060 2112
   }
......
2127 2179
   }
2128 2180
   
2129 2181
   /**
2130
    * Check if two {@link Record} objects are equal from the point of view of the {@code data} array.
2131
    * <p>
2132
    * <strong>To be noted</strong>, that this method accesses the {@code data} arrays directly without using 
2133
    * any getter. This provides faster access, however should be changed if not all fields are hydrated and
2134
    * access is mandatory from a getter.
2135
    * 
2136
    * @param  otherRecord
2137
    *         The instance to compare against.
2138
    *        
2139
    * @return True if the {@code data} arrays are equal, false - otherwise
2140
    */
2141
   public boolean equalData(Record otherRecord)
2142
   {
2143
      return Arrays.equals(this.data, otherRecord.data);
2144
   }
2145
   
2146
   /**
2147 2182
    * Converts this {@code Record} into a POJO, if POJO class is available.
2148 2183
    * To be implemented by DMO implementations.
2149 2184
    * 
new/src/com/goldencode/p2j/persist/RecordBuffer.java 2026-06-10 08:17:33 +0000
12658 12658
            // don't count usages of template records and don't register with persistence
12659 12659
            if (oldRecord.primaryKey() != null && oldRecord.primaryKey() > 0)
12660 12660
            {
12661
               // the buffer is leaving this record: report the field demand observed during the visit
12662
               // to the runtime-feedback hydration layer (no-op unless the record was probing)
12663
               oldRecord.reportDemandSample();
12664

  
12661 12665
               decrementDMOUseCount(oldRecord, false);
12662 12666
            }
12663 12667
         }
new/src/com/goldencode/p2j/persist/TempRecord.java 2026-06-10 06:43:53 +0000
17 17
** 004 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
18 18
**                  default methods cause the Java metaspace to spike one order of magnitude.
19 19
** 005 AS  20241009 Refactored activeOffset into activeOffsets(Bitset).
20
** 006 AL2 20231102 Added computeIndexForHydrator for lazy hydration.
20 21
*/
21 22

  
22 23
/*
......
74 75

  
75 76
package com.goldencode.p2j.persist;
76 77

  
78
import com.goldencode.p2j.jmx.*;
77 79
import com.goldencode.p2j.persist.orm.*;
78 80
import com.goldencode.p2j.util.object;
79 81

  
......
87 89
extends Record
88 90
implements TempTableRecord
89 91
{
92
   private static final SimpleCounter HYDRATE_RECORDS_WHEN_SESSION_CLOSE =
93
      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedRecordsWhenSessionCloses);
94
   
95
   private static final SimpleCounter TOTAL_FIELDS =
96
      SimpleCounter.getInstance(FwdServerJMX.Counter.TempTotalFields);
97
   
98
   private static final SimpleCounter HYDRATE_FIELD_WHEN_SESSION_CLOSES =
99
      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWhenSessionCloses);
100
   
101
   private static final SimpleCounter HYDRATED_FIELDS_LZS =
102
      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWithLZS);
103
   
104
   private static final SimpleCounter HYDRATED_FIELDS_LOADER =
105
      SimpleCounter.getInstance(FwdServerJMX.Counter.TempHydratedFieldsWithLoader);
106
   
90 107
   /** The property/column name of "__error-flag__" field. */
91 108
   public static final String _ERROR_FLAG = "_errorFlag";
92 109
   
......
197 214
   @Override
198 215
   public final Long _peerRowid()
199 216
   {
200
      return (Long) data[_PEER_ROWID_DATA_INDEX];
217
      return (Long) getDatum(_PEER_ROWID_DATA_INDEX);
201 218
   }
202 219
   
203 220
   /**
......
237 254
   @Override
238 255
   public final Integer _rowState()
239 256
   {
240
      return (Integer) data[_ROW_STATE_DATA_INDEX];
257
      return (Integer) getDatum(_ROW_STATE_DATA_INDEX);
241 258
   }
242 259
   
243 260
   /**
......
278 295
   @Override
279 296
   public final Long _originRowid()
280 297
   {
281
      return (Long) data[_ORIGIN_ROWID_DATA_INDEX];
298
      return (Long) getDatum(_ORIGIN_ROWID_DATA_INDEX);
282 299
   }
283 300
   
284 301
   /**
......
318 335
   @Override
319 336
   public final Long _datasourceRowid()
320 337
   {
321
      return (Long) data[_DATASOURCE_ROWID_INDEX];
338
      return (Long) getDatum(_DATASOURCE_ROWID_INDEX);
322 339
   }
323 340
   
324 341
   /**
......
356 373
   @Override
357 374
   public final Integer _errorFlags()
358 375
   {
359
      return (Integer) data[_ERROR_FLAG_DATA_INDEX];
376
      return (Integer) getDatum(_ERROR_FLAG_DATA_INDEX);
360 377
   }
361 378
   
362 379
   /**
......
392 409
   @Override
393 410
   public final String _errorString()
394 411
   {
395
      return (String) data[_ERROR_STRING_DATA_INDEX];
412
      return (String) getDatum(_ERROR_STRING_DATA_INDEX);
396 413
   }
397 414
   
398 415
   /**
......
507 524
      Class<?> implIFace = _recordMeta().getDmoMeta().getAnnotatedInterface();
508 525
      TemporaryBuffer.updateObjectRefCount(implIFace, oldVal, newVal);
509 526
   }
527
   
528
   @Override
529
   protected int getHydratorRowOffset()
530
   {
531
      return 2;
532
   }
533
   
534
   @Override
535
   protected void countHydrateRecordWhenSessionCloses(long count)
536
   {
537
      HYDRATE_RECORDS_WHEN_SESSION_CLOSE.update(count);
538
   }
539
   
540
   @Override
541
   protected void countTotalFields(long count)
542
   {
543
      TOTAL_FIELDS.update(count);
544
   }
545
   
546
   @Override
547
   protected void countHydratedFieldsWithLZS(long count)
548
   {
549
      HYDRATED_FIELDS_LZS.update(count);
550
   }
551
   
552
   @Override
553
   protected void countHydratedFieldsWithLoader(long count)
554
   {
555
      HYDRATED_FIELDS_LOADER.update(count);
556
   }
557
   
558
   @Override
559
   protected void countHydratedFieldWhenSessionCloses(long count)
560
   {
561
      HYDRATE_FIELD_WHEN_SESSION_CLOSES.update(count);
562
   }
510 563
}
new/src/com/goldencode/p2j/persist/dialect/Dialect.java 2026-06-10 08:51:59 +0000
213 213
import java.util.logging.*;
214 214
import com.goldencode.p2j.cfg.*;
215 215
import com.goldencode.p2j.convert.*;
216
import com.goldencode.p2j.directory.*;
216 217
import com.goldencode.p2j.persist.*;
217 218
import com.goldencode.p2j.persist.deploy.*;
218 219
import com.goldencode.p2j.persist.orm.*;
......
264 265
   
265 266
   /** Logger */
266 267
   private static final CentralLogger log = CentralLogger.get(Dialect.class.getName());
267
   
268

  
269
   /** Directory path for the lazy-hydration master switch. */
270
   private static final String CFG_LAZY_HYDRATION = "persistence/lazy-hydration/enabled";
271

  
272
   /**
273
    * Master switch for lazy (just-in-time) hydration of DMOs, read once from configuration. When
274
    * {@code false}, hydration is always eager and the runtime-feedback layer is moot (no call-site is
275
    * ever wrapped lazily). Defaults to enabled.
276
    */
277
   private static final boolean lazyHydrationEnabled =
278
      DirectoryManager.getInstance().getBoolean(Directory.ID_RELATIVE_SERVER, CFG_LAZY_HYDRATION, false);
279

  
268 280
   /** Map of known/loaded dialect instances */
269 281
   private static final Map<String, Dialect> knownDialects = new ConcurrentHashMap<>();
270 282
   
......
605 617
   }
606 618
   
607 619
   /**
608
    * Create the error descriptor from the SQLException message.
609
    *
620
    * Create the error descriptor from the {@code SQLException} message.
621
    * 
610 622
    * @param   message
611 623
    *          The SQLException message.
612 624
    *
......
620 632
   /**
621 633
    * Check if Common Table Expressions should be used for converted CONTAINS operator
622 634
    * 
623
    * @return <code>true</code> if Common Table Expressions should be used for converted CONTAINS operator
635
    * @return  <code>true</code> if Common Table Expressions should be used for converted CONTAINS operator
624 636
    */
625 637
   public boolean useCTE4Contains()
626 638
   {
......
718 730
   public abstract void generateWordTablesDDLImpl(String dbName,
719 731
                                                  PrintStream out,
720 732
                                                  String eoln,
721
                                                  Collection<WordTable>  wordTables);
733
                                                  Collection<WordTable> wordTables);
722 734
   
723 735
   /**
724 736
    * Create a data object, the purpose of which is specific to a particular
......
2189 2201
   {
2190 2202
      return null;
2191 2203
   }
2204

  
2205
   /**
2206
    * Indicate whether this dialect is likely to benefit from lazy hydration of DMOs when reading a record
2207
    * from the database. This serves as a hint, but may be overridden by configuration or runtime conditions.
2208
    *
2209
    * @return  {@code true} unless disabled via the {@code persistence/lazy-hydration/enabled}
2210
    *          configuration or overridden by subclasses.
2211
    */
2212
   public boolean useLazyHydration()
2213
   {
2214
      return lazyHydrationEnabled;
2215
   }
2192 2216
   
2193 2217
   /**
2194 2218
    * Use the provided database connection to extract the collation used by the database and report it
new/src/com/goldencode/p2j/persist/dirty/DefaultDirtyShareManager.java 2026-06-10 06:43:53 +0000
834 834
               indexUpdateStates.remove(ident);
835 835
            }
836 836
            
837
            String hql = "delete from " + entity + 
838
                         " where " + DatabaseManager.PRIMARY_KEY + " = ?";
837
            String fql = "delete from " + entity + " where " + Session.PK + " = ?";
839 838
            Session session = null;
840 839
            boolean inTx = false;
841 840
            
......
843 842
            {
844 843
               session = new Session(dirtyDatabase);
845 844
               inTx = session.beginTransaction();
846
               Query query = Session.createQuery(hql);
845
               Query query = Session.createQuery(fql);
847 846
               query.setParameter(0, id);
848 847
               query.executeUpdate(session);
849 848
               session.commit();
......
1223 1222
   }
1224 1223
   
1225 1224
   /**
1226
    * Execute an HQL query and return the results as a list. If the query returns multiple results
1225
    * Execute an FQL query and return the results as a list. If the query returns multiple results
1227 1226
    * per row, each element in the list will be an array of {@code Object}s.
1228 1227
    * <p>
1229 1228
    * No locking is attempted.
new/src/com/goldencode/p2j/persist/orm/AbstractRowStructure.java 2026-06-10 06:43:53 +0000
12 12
** 003 LS  20241105 Added hasOnlyPK().
13 13
** 004 OM  20240701 Replaced + concatenation with append in StringBuilder.
14 14
**     SP  20250321 Removed usage of prop.index.
15
** 005 AL2 20231214 Fixed hydrate extents to avoid honoring recOffset if set on -1.
15 16
*/
16 17

  
17 18
/*
......
177 178
    * @throws  PersistenceException
178 179
    *          thrown when hydrating extents fails (as it requires extra SQL queries to be run).
179 180
    */
180
   protected void hydrateExtents(Session session, BaseRecord r, int recOffset)
181
   @Override
182
   public void hydrateExtents(Session session, BaseRecord r, int recOffset)
181 183
   throws PersistenceException
182 184
   {
183 185
      // TODO: collect extent properties and do a new query from SQL, see Loader.readExtentData()
......
188 190
         PropertyMeta[] propsMeta = recordMeta.getPropertyMeta(false);
189 191
         String[] loadSqls = recordMeta.loadSql;
190 192
         int[] loadSqlsIndexes = recordMeta.loadSqlIndexes;
191
         boolean incomplete = isIncomplete();
193
         boolean arbitrary = isIncomplete() || recOffset == -1;
192 194
         
193 195
         // only use the extent queries, skip the loadSqls[0] which contains the simple properties
194 196
         for (int i = 1; i < loadSqls.length; i++) // skip "plain" columns 
195 197
         {            
196
            if (incomplete)
198
            if (arbitrary)
197 199
            {
198 200
               // resolve the offset in the 'props' array where this sql starts.  
199 201
               // all non-expanded extent fields are loaded even for incomplete records.
new/src/com/goldencode/p2j/persist/orm/AdaptiveRowStructure.java 2026-06-10 06:43:53 +0000
10 10
**     AL2 20231212 Use dmoMeta directly, without getter.
11 11
** 002 OM  20240715 Fixed copy/paste bug.
12 12
** 003 LS  20250714 Added 'getNeededFields' implementation.
13
** 004 AL2 20231214 Added implementation for hydrateAt.
13 14
*/
14 15

  
15 16
/*
......
69 70

  
70 71
import java.sql.ResultSet;
71 72
import java.sql.SQLException;
72
import java.util.BitSet;
73
import java.util.Iterator;
73
import java.util.*;
74 74

  
75 75
import com.goldencode.p2j.persist.*;
76 76

  
......
228 228
      return delegate.getNeededFields();
229 229
   }
230 230

  
231

  
232
   /**
233
    * Hydrate only one property in the base-record. This is largely used by the lazy hydrator,
234
    * allowing it to hydrate only the property that is accessed now. The other hydration method 
235
    * ({@link #hydrate}) eagerly hydrates all properties, is a slow process and may do useless
236
    * hydrations on properties that are not accessed.
237
    * 
238
    * @param   resultSet
239
    *          The ResultSet from which the hydration should be done
240
    * @param   rsOffset
241
    *          The result set offset from which the reading should be done. This is relevant as
242
    *          the result set may contain a joined row, but we are interested only in a specific
243
    *          record that may start at a specific offset.
244
    * @param   r
245
    *          The record that should be hydrated. 
246
    * @param   propOffset
247
    *          The property that should be hydrated. The caller is responsible in assuring that
248
    *          this property wasn't hydrated before.
249
    * 
250
    * @return  {@code true} if this worked due to the fact that it was a scalar value. {@code false}
251
    *          in case the property was a non-expanded 
252
    *          
253
    * @throws  PersistenceException
254
    *          This is thrown when reading the scalar property fails.
255
    */
256
   @Override
257
   public boolean hydrateAt(ResultSet resultSet, int rsOffset, BaseRecord r, int dataIndex)
258
   throws PersistenceException
259
   {
260
      return delegate.hydrateAt(resultSet, rsOffset, r, dataIndex);
261
   }
262
   
263
   /**
264
    * Retrieve the fields that were loaded from the database.
265
    *
266
    * @return A BitSet object, where true means that the field associated with that position in the {@code
267
    * data} array has been loaded from database, and false - otherwise.
268
    */
269
   @Override
270
   public BitSet getLoadedFields()
271
   {
272
      return delegate.getLoadedFields();
273
   }
274
   
231 275
   /**
232 276
    * Invalidation logic that will pick up everything that was appended until now and moved into
233 277
    * an un-ordered implementation. This routine should be avoided as much as possible in the 
new/src/com/goldencode/p2j/persist/orm/BaseRecord.java 2026-06-10 08:17:13 +0000
2 2
** Module   : BaseRecord.java
3 3
** Abstract : The abstract base class for all business data model object classes.
4 4
**
5
** Copyright (c) 2019-2025, Golden Code Development Corporation.
5
** Copyright (c) 2019-2026, Golden Code Development Corporation.
6 6
**
7 7
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
8 8
** 001 ECF 20191001 Created initial version with basic data and accessors.
......
112 112

  
113 113
package com.goldencode.p2j.persist.orm;
114 114

  
115
import com.goldencode.p2j.jmx.*;
115 116
import com.goldencode.p2j.persist.*;
116 117
import com.goldencode.p2j.rest.serializers.*;
117 118
import com.goldencode.p2j.schema.*;
118 119
import com.goldencode.p2j.util.*;
119 120
import com.goldencode.p2j.util.logging.*;
120

  
121 121
import java.lang.reflect.*;
122 122
import java.sql.*;
123 123
import java.util.*;
......
162 162
   /** Surrogate primary key of the record */
163 163
   protected Long id = null;
164 164
   
165
   /** The data represented by this record, in it's low-level (database friendly) form */
166
   protected final Object[] data;
167
   
168 165
   /** For an incomplete record, this holds the fields which were read or assigned in this record. */
169 166
   protected BitSet readFields = null;
170 167
   
......
178 175
    */
179 176
   protected long references = 0;
180 177
   
178
   /** The data represented by this record, in it's low-level (database friendly) form */
179
   private final Object[] data;
180
   
181 181
   /** Map of properties touched, each bit corresponds with a property in the data array */
182 182
   private final BitSet dirtyProps;
183 183
   
......
193 193
   /** Record identifier (lock variant, which uses table name) for this record (created lazily) */
194 194
   private RecordIdentifier<String> recid = null;
195 195
   
196
   /** Hydrator used to extract data from a result set positioned on a particular target row */
197
   private Hydrator hydrator = null;
198
   
199
   /** Map of "live" properties (read or assigned), each bit corresponds with a property in the data array */
200
   // TODO: resolve redundancy with readFields
201
   private BitSet liveProps;
202
   
203
   /** The number of bits set in {@link #liveProps} */
204
   private int livePropsCardinality;
205

  
206
   /**
207
    * Process-wide fast-path gate for the runtime-feedback hydration probe. {@code true} while the
208
    * feature is enabled and at least one {@link FieldUsageProfile} is still UNKNOWN. When {@code
209
    * false}, {@link #getDatum} pays only this single volatile read per field access and never touches
210
    * the per-record probe state, so steady-state hydration cost is unchanged. Maintained by
211
    * {@link FieldUsageProfile} as profiles are created and as verdicts lock in.
212
    */
213
   static volatile boolean PROBING_ACTIVE = false;
214

  
215
   /**
216
    * The field-usage profile this record is probing for, or {@code null} if not probing. Distinct
217
    * from {@link #hydrator}, so it survives the hydrator being nulled out in {@link #getDatum}.
218
    */
219
   private FieldUsageProfile usageProfile;
220

  
221
   /**
222
    * While probing, the set of field offsets read via {@link #getDatum} during the current buffer
223
    * visit; {@code null} except while a probe is armed (see {@link #armUsageProbe}).
224
    */
225
   private BitSet accessed;
226

  
196 227
   /** The currently active buffer when state is changing */
197 228
   private RecordBuffer activeBuffer = null;
198 229
   
......
298 329
         }
299 330
         
300 331
         // TODO: LOB data is mutable, needs to be duplicated explicitly
301
         boolean change = to.data[i] != from.data[i];
332
         Object toDatum = to.getDatum(i);
333
         Object fromDatum = from.getDatum(i);
334
         boolean change = toDatum != fromDatum; 
335

  
302 336
         if (change)
303 337
         {
304
            to.data[i] = from.data[i];
338
            to.setSimpleDatum(i, fromDatum);
305 339
            
306 340
            // update the null property bitset and mark the property as changed to be able to save it
307
            to.nullProps.set(i, to.data[i] == null);
341
            to.nullProps.set(i, fromDatum == null);
308 342
            to.dirtyProps.set(i);
309 343
         }
310 344
      }
......
488 522
   public void copy(BaseRecord from)
489 523
   {
490 524
      // TODO: BLOB data is mutable, needs to be duplicated explicitly
491
      int len = this.data.length;
525
      int len = this.getLength();
492 526
      this.id = from.id;
493
      System.arraycopy(from.data, 0, this.data, 0, len);
527
      copyArray(from.getData());
494 528
      dirtyProps.set(0, len); // TODO: nullProps?
529
      
530
      hydrator = from.hydrator;
531
      liveProps = from.liveProps != null ? (BitSet) from.liveProps.clone() : null;
532
      livePropsCardinality = from.livePropsCardinality;
533
   }
534
   
535
   /**
536
    * Check if two {@link BaseRecord} objects are equal from the point of view of the {@code data} array.
537
    * <p>
538
    * <strong>To be noted</strong>, that this method accesses the {@code data} arrays directly without using 
539
    * any getter. This provides faster access, however should be changed if not all fields are hydrated and
540
    * access is mandatory from a getter.
541
    *
542
    * @param  otherRecord
543
    *         The instance to compare against.
544
    *
545
    * @return True if the {@code data} arrays are equal, false - otherwise
546
    */
547
   public boolean equalData(BaseRecord otherRecord)
548
   {
549
      if (this.getLength() != otherRecord.getLength())
550
      {
551
         return false;
552
      }
553
      for  (int i = 0; i < this.getLength(); i++)
554
      {
555
         if (this.areAllPropertiesLive() && otherRecord.areAllPropertiesLive())
556
         {
557
            return Arrays.equals(this.getData(), otherRecord.getData());
558
         }
559
      }
560
      
561
      for  (int i = 0; i < this.getLength(); i++)
562
      {
563
         if (!Objects.equals(getDatum(i), otherRecord.getDatum(i)))
564
         {
565
            return false;
566
         }
567
      }
568
      
569
      return true;
570
   }
571
   
572
   
573
   /**
574
    * Copy an array of objects into data.
575
    * 
576
    * @param   from
577
    *          The source to copy from.
578
    */
579
   public void copyArray(Object[] from)
580
   {
581
      System.arraycopy(from, 0, data, 0, getLength());
495 582
   }
496 583
   
497 584
   /**
......
695 782
      //       previous state and roll this back as well
696 783
      for (int i = activeOffsets.nextSetBit(0); i != -1; i = activeOffsets.nextSetBit(i + 1))
697 784
      {
698
         data[i] = activePropPreviousValues[i];
785
         setSimpleDatum(i, activePropPreviousValues[i]);
699 786
      }
700 787
   }
701 788
   
......
728 815
      buf.append(':').append(primaryKey()).append(sep)
729 816
         .append(state).append(sep)
730 817
         .append("references: ").append(references).append(sep)
818
         .append("read: ").append(readFields).append(sep)
819
         .append("live: ").append(liveProps).append(sep)
731 820
         .append("dirty: ").append(dirtyProps).append(sep)
732 821
         .append("unvalidated: ").append(unvalidated);
733 822
      
734 823
      if (includeData)
735 824
      {
736
         int len = data.length;
825
         int len = getLength();
737 826
         buf.append(sep).append("data: {");
738 827
         for (int i = 0; i < len; i++)
739 828
         {
......
742 831
               buf.append(", ");
743 832
            }
744 833
            
745
            buf.append(data[i]);
834
            buf.append(getSimpleDatum(i));
746 835
         }
747 836
         
748 837
         buf.append('}');
......
772 861
   {
773 862
      Objects.requireNonNull(pm);
774 863
      
775
      return data;
864
      return getData();
776 865
   }
777 866
   
778 867
   /**
......
791 880
   public Object[] getData(RecordBuffer rb)
792 881
   {
793 882
      Objects.requireNonNull(rb);
794
      
883

  
884
      return getData();
885
   }
886
   
887
   /**
888
    * Get the data property.
889
    * 
890
    * @return   The value of the data property.
891
    */
892
   public Object[] getData()
893
   {
795 894
      return data;
796 895
   }
896
       
897
   /**
898
    * Get the size of the data property.
899
    * 
900
    * @return  Size of the data property.
901
    */
902
   public int getLength()
903
   {
904
      return data.length;
905
   }
797 906
   
798 907
   /**
799 908
    * Indicate whether any of this record's validatable properties are dirty and have not been validated.
......
860 969
   public Object[] getActiveUpdateDiff(int ormIndex, int extentIndex)
861 970
   {
862 971
      int actualIndex = ormIndex + extentIndex;
863
      if (dirtyProps.get(actualIndex) && !Objects.equals(activePropPreviousValues[actualIndex], 
864
                                                        data[actualIndex]))
972
      Object activeOffsetDatum = getDatum(actualIndex);
973
      
974
      if (dirtyProps.get(actualIndex) &&
975
         !Objects.equals(activePropPreviousValues[actualIndex], activeOffsetDatum))
865 976
      {
866 977
         Object[] res = new Object[2];
867 978
         res[0] = activePropPreviousValues[actualIndex];
868
         res[1] = data[actualIndex];
979
         res[1] = activeOffsetDatum;
869 980
         return res;
870 981
      }
871 982
      return null;
......
890 1001
   public BitSet _getReadFields()
891 1002
   {
892 1003
      return readFields;
893
   }
894

  
1004
   }  
1005
   
1006
   /**
1007
    * This should be called when the record is re-attached to a new session. This way, we refresh the 
1008
    * underlying session in hydrator (if any) to properly refresh the record when needed. If this is 
1009
    * not used, we may face {@link LazyHydrationException} due to stale sessions in the hydrator. 
1010
    * 
1011
    * First time a hydrator is created, it will take the session from the generator result-set, thus,
1012
    * there is no need to call this method in that case.
1013
    * 
1014
    * @param   session
1015
    *          The session to which this record is re-attached.
1016
    */
1017
   public void refreshHydrator(Session session)
1018
   {
1019
      if (hydrator != null)
1020
      {
1021
         hydrator.refresh(session);
1022
      }
1023
   }
1024
   
1025
   /**
1026
    * Sets the bit in {@link BaseRecord#liveProps} to true, meaning that the field at that index has been 
1027
    * read or written.
1028
    * 
1029
    * @param offset
1030
    *        Zero-based offset of the datum in the data array, which corresponds to the index in 
1031
    *        {@link BaseRecord#liveProps}
1032
    */
1033
   protected void setLivePropsAtOffset(int offset)
1034
   {
1035
      if (liveProps != null)
1036
      {
1037
         if (!liveProps.get(offset))
1038
         {
1039
            livePropsCardinality++;
1040
         }
1041
         
1042
         liveProps.set(offset);
1043
      }
1044
   }
1045
   
1046
   protected void setAllLiveProps()
1047
   {
1048
      if (liveProps == null)
1049
      {
1050
         return;
1051
      }
1052
      for (int i = 0; i < data.length; i++)
1053
      {
1054
         liveProps.set(i);
1055
      }
1056
   }
1057
   
895 1058
   /**
896 1059
    * Set a single datum into the data array, and update the record and property state accordingly.
897 1060
    * 
......
901 1064
    *          Value to be set into the data array.
902 1065
    */
903 1066
   protected void setDatum(int offset, Object datum)
904
   {
905
      if (readFields != null)
906
      {
907
         readFields.set(offset);
908
      }
909

  
1067
   {      
910 1068
      if (activeBuffer != null)
911 1069
      {
912 1070
         activeOffsets.set(offset);
......
915 1073
      // update the bitset of dirty (touched) properties
916 1074
      dirtyProps.set(offset);
917 1075
      
1076
      Object offsetDatum = getDatum(offset);
1077
      
918 1078
      // temporarily save the old value (even if it is the same as the new value)
919
      activePropPreviousValues[offset] = data[offset];
1079
      activePropPreviousValues[offset] = offsetDatum;
1080

  
1081
      if (readFields != null)
1082
      {
1083
         readFields.set(offset);
1084
      }
920 1085
      
921 1086
      // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the 
922 1087
      // same, regardless of the scale value.
923
      if (!(Objects.equals(data[offset], datum) || 
1088
      if (!(Objects.equals(offsetDatum, datum) || 
924 1089
            (datum instanceof Comparable && 
925
             data[offset] != null        && 
926
             ((Comparable) datum).compareTo(data[offset]) == 0)))
1090
             offsetDatum != null        && 
1091
             ((Comparable) datum).compareTo(offsetDatum) == 0)))
927 1092
      {
928 1093
         // ensure we have an exclusive lock on the record
929 1094
         if (!lockForUpdate())
......
942 1107
         }
943 1108
         
944 1109
         // update the value in the data array
945
         data[offset] = datum;
1110
         setSimpleDatum(offset, datum);
946 1111
      }
947 1112
   }
948 1113
   
......
980 1145
            continue;
981 1146
         }
982 1147
         
983
         if (fromProp.getType()   != toProp.getType() || 
984
             fromProp.getExtent() != toProp.getExtent())
1148
         if (fromProp.getType() != toProp.getType() || fromProp.getExtent() != toProp.getExtent())
985 1149
         {
986 1150
            return false;
987 1151
         }
......
1012 1176
         
1013 1177
         // for BigDecimal, equals checks the scale - we need compareTo, to check if the values are really the 
1014 1178
         // same, regardless of the scale value.
1015
         if (!(Objects.equals(data[toOffset], datum) || 
1179
         Object offsetDatum = getDatum(toOffset);
1180
         if (!(Objects.equals(offsetDatum, datum) || 
1016 1181
              (datum instanceof Comparable && 
1017
               data[toOffset] != null && 
1018
               ((Comparable) datum).compareTo(data[toOffset]) == 0)))
1182
               offsetDatum != null && 
1183
               ((Comparable) datum).compareTo(offsetDatum) == 0)))
1019 1184
         {
1020 1185
            if (updatedOffsets == null)
1021 1186
            {
......
1056 1221
      {
1057 1222
         int offset = updatedOffsets.get(idx);
1058 1223
         Object datum = updatedDatums.get(idx);
1059
         data[offset] = datum; 
1224
         setSimpleDatum(offset, datum);
1060 1225
      }
1061 1226
      
1062 1227
      return true;
1063 1228
   }
1064 1229
   
1065 1230
   /**
1066
    * Get the field's value on the specified offset.
1067
    * 
1068
    * @param    offset
1069
    *           Zero-based offset of the datum in the data array.
1070
    * 
1071
    * @return   The field's value.
1231
    * Get the field's value at the specified offset, with {@code null} indicating unknown value.
1232
    * <p>
1233
    * N.B.: if lazy hydration can be active for this record, callers must catch and handle the unchecked
1234
    * {@code LazyHydrationException}.
1235
    * 
1236
    * @param   offset
1237
    *          Zero-based offset of the datum in the data array.
1238
    * 
1239
    * @return  The field's value, with {@code null} indicating unknown value.
1240
    * 
1241
    * @throws  LazyHydrationException
1242
    *          if this record is enabled for lazy hydration, but the attached hydrator was no longer valid.
1243
    *          Although this is an unchecked exception to allow the DMO to be generated with simple getter
1244
    *          methods, callers which invoke this method when lazy hydration could be active MUST catch and
1245
    *          handle this exception.
1072 1246
    */
1073 1247
   protected Object getDatum(int offset)
1248
   throws LazyHydrationException
1074 1249
   {
1250
      // runtime-feedback probe: record that this field was demanded during the current visit. Placed
1251
      // before the hydrator block because eager records have hydrator == null and would otherwise be
1252
      // invisible. Gated on a single volatile read so steady state (feature off / all verdicts
1253
      // locked) costs nothing more.
1254
      if (PROBING_ACTIVE)
1255
      {
1256
         BitSet acc = accessed;
1257
         if (acc != null)
1258
         {
1259
            acc.set(offset);
1260
         }
1261
      }
1262

  
1263
      if (hydrator != null && liveProps != null && !liveProps.get(offset))
1264
      {
1265
         boolean success = hydrator.hydrate(this, offset, getHydratorRowOffset(), true);
1266
         if (success)
1267
         {
1268
            countHydratedFieldsWithLZS(1);
1269
         }
1270
         if (!success)
1271
         {
1272
            BitSet unhydratedFields = getUnhydratedFields();
1273
            hydrator.refresh(this, id, unhydratedFields);
1274
            countHydratedFieldsWithLoader(unhydratedFields.cardinality());
1275
         }
1276
         if (!success || livePropsCardinality == data.length)
1277
         {
1278
            hydrator = null;
1279
            liveProps = null;
1280
            livePropsCardinality = data.length;
1281
         }
1282
      }
1283
      
1075 1284
      return data[offset];
1076 1285
   }
1286

  
1287
   /**
1288
    * Switch the process-wide probing fast-path gate on or off. Called by {@link FieldUsageProfile}
1289
    * as profiles are created and as their verdicts lock in.
1290
    *
1291
    * @param   active
1292
    *          The new value of {@link #PROBING_ACTIVE}.
1293
    */
1294
   static void setProbingActive(boolean active)
1295
   {
1296
      PROBING_ACTIVE = active;
1297
   }
1298

  
1299
   /**
1300
    * Arm the runtime-feedback hydration probe for the given profile: while the profile is still
1301
    * learning, this record records which fields are read (via {@link #getDatum}) and reports the
1302
    * sample when the buffer leaves it (see {@link #reportDemandSample}). Hydration itself stays eager
1303
    * &mdash; only the demand is observed. No-op when the profile is {@code null} or already locked.
1304
    *
1305
    * @param   profile
1306
    *          The call-site profile to probe for, or {@code null}.
1307
    */
1308
   void armUsageProbe(FieldUsageProfile profile)
1309
   {
1310
      if (profile == null || !profile.isUnknown())
1311
      {
1312
         return;
... This diff was truncated because it exceeds the maximum size that can be displayed.