001package arez.component.internal; 002 003import arez.ActionFlags; 004import arez.Arez; 005import arez.ArezContext; 006import arez.Component; 007import arez.ComputableValue; 008import arez.Disposable; 009import arez.EqualityComparator; 010import arez.ObjectsEqualsComparator; 011import arez.SafeFunction; 012import arez.Task; 013import arez.component.DisposeNotifier; 014import grim.annotations.OmitSymbol; 015import java.util.Arrays; 016import java.util.HashMap; 017import java.util.Map; 018import java.util.Objects; 019import java.util.Stack; 020import javax.annotation.Nonnull; 021import javax.annotation.Nullable; 022import org.intellij.lang.annotations.MagicConstant; 023import static org.realityforge.braincheck.Guards.*; 024 025/** 026 * The class responsible for caching ComputableValue instances for different input parameters. 027 */ 028public final class MemoizeCache<T> 029 implements Disposable 030{ 031 /** 032 * Functional interface for calculating memoizable value. 033 * 034 * @param <T> The type of the returned value. 035 */ 036 @FunctionalInterface 037 public interface Function<T> 038 { 039 /** 040 * Return calculated memoizable value. 041 * 042 * @param args the functions arguments. 043 * @return the value generated by function. 044 */ 045 T call( @Nonnull final Object... args ); 046 } 047 048 /** 049 * Reference to the system to which this node belongs. 050 */ 051 @OmitSymbol( unless = "arez.enable_zones" ) 052 @Nullable 053 private final ArezContext _context; 054 /** 055 * A human consumable prefix for computable values. It should be non-null if {@link Arez#areNamesEnabled()} returns 056 * true and <code>null</code> otherwise. 057 */ 058 @Nullable 059 @OmitSymbol( unless = "arez.enable_names" ) 060 private final String _name; 061 /** 062 * The component that this memoize cache is contained within. 063 * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but can be null even if this is true. 064 */ 065 @OmitSymbol( unless = "arez.enable_native_components" ) 066 @Nullable 067 private final Component _component; 068 /** 069 * The function memoized. 070 */ 071 @Nonnull 072 private final Function<T> _function; 073 /** 074 * The cache of all the ComputableValue created for each unique combination of parameters. 075 */ 076 private final Map<Object, Object> _cache = new HashMap<>(); 077 /** 078 * The number of arguments passed to memoized function. 079 */ 080 private final int _argCount; 081 /** 082 * The flags passed to the created ComputableValues. 083 */ 084 @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) 085 private final int _flags; 086 /** 087 * Strategy used to compare old and new computed values. 088 */ 089 @Nonnull 090 private final EqualityComparator _equalityComparator; 091 /** 092 * The index of the next ComputableValue created. 093 * This is only used when creating unique names for ComputableValues. 094 */ 095 private int _nextIndex; 096 /** 097 * Flag indicating that the cache is currently being disposed. 098 */ 099 private boolean _disposed; 100 101 /** 102 * Create the Memoize method cache. 103 * 104 * @param context the context in which to create ComputableValue instances. 105 * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true. 106 * @param name a human consumable prefix for computable values. 107 * @param function the memoized function. 108 * @param argCount the number of arguments expected to be passed to memoized function. 109 */ 110 public MemoizeCache( @Nullable final ArezContext context, 111 @Nullable final Component component, 112 @Nullable final String name, 113 @Nonnull final Function<T> function, 114 final int argCount ) 115 { 116 this( context, component, name, function, argCount, 0, new ObjectsEqualsComparator() ); 117 } 118 119 /** 120 * Create the Memoize method cache. 121 * 122 * @param context the context in which to create ComputableValue instances. 123 * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true. 124 * @param name a human consumable prefix for computable values. 125 * @param function the memoized function. 126 * @param argCount the number of arguments expected to be passed to memoized function. 127 * @param flags the flags that are used when creating ComputableValue instances. The only flags supported are flags defined in {@link ComputableValue.Flags} except for {@link ComputableValue.Flags#KEEPALIVE}, {@link ComputableValue.Flags#RUN_NOW} and {@link ComputableValue.Flags#RUN_LATER}. 128 */ 129 public MemoizeCache( @Nullable final ArezContext context, 130 @Nullable final Component component, 131 @Nullable final String name, 132 @Nonnull final Function<T> function, 133 final int argCount, 134 @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) final int flags ) 135 { 136 this( context, component, name, function, argCount, flags, new ObjectsEqualsComparator() ); 137 } 138 139 /** 140 * Create the Memoize method cache. 141 * 142 * @param context the context in which to create ComputableValue instances. 143 * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true. 144 * @param name a human consumable prefix for computable values. 145 * @param function the memoized function. 146 * @param argCount the number of arguments expected to be passed to memoized function. 147 * @param flags the flags that are used when creating ComputableValue instances. The only flags supported are flags defined in {@link ComputableValue.Flags} except for {@link ComputableValue.Flags#KEEPALIVE}, {@link ComputableValue.Flags#RUN_NOW} and {@link ComputableValue.Flags#RUN_LATER}. 148 * @param equalityComparator strategy used to compare old and new computed values. 149 */ 150 public MemoizeCache( @Nullable final ArezContext context, 151 @Nullable final Component component, 152 @Nullable final String name, 153 @Nonnull final Function<T> function, 154 final int argCount, 155 @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) final int flags, 156 @Nonnull final EqualityComparator equalityComparator ) 157 { 158 if ( Arez.shouldCheckApiInvariants() ) 159 { 160 apiInvariant( () -> Arez.areZonesEnabled() || null == context, 161 () -> "Arez-174: MemoizeCache passed a context but Arez.areZonesEnabled() is false" ); 162 apiInvariant( () -> Arez.areNamesEnabled() || null == name, 163 () -> "Arez-0159: MemoizeCache passed a name '" + name + "' but Arez.areNamesEnabled() is false" ); 164 apiInvariant( () -> argCount > 0, 165 () -> "Arez-0160: MemoizeCache constructed with invalid argCount: " + argCount + 166 ". Expected positive value." ); 167 final int mask = ComputableValue.Flags.PRIORITY_HIGHEST | 168 ComputableValue.Flags.PRIORITY_HIGH | 169 ComputableValue.Flags.PRIORITY_NORMAL | 170 ComputableValue.Flags.PRIORITY_LOW | 171 ComputableValue.Flags.PRIORITY_LOWEST | 172 ComputableValue.Flags.NO_REPORT_RESULT | 173 ComputableValue.Flags.AREZ_DEPENDENCIES | 174 ComputableValue.Flags.AREZ_OR_NO_DEPENDENCIES | 175 ComputableValue.Flags.AREZ_OR_EXTERNAL_DEPENDENCIES | 176 ComputableValue.Flags.OBSERVE_LOWER_PRIORITY_DEPENDENCIES | 177 ComputableValue.Flags.READ_OUTSIDE_TRANSACTION; 178 179 apiInvariant( () -> ( ~mask & flags ) == 0, 180 () -> "Arez-0211: MemoizeCache passed unsupported flags. Unsupported bits: " + ( ~mask & flags ) ); 181 } 182 _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null; 183 _component = Arez.areNativeComponentsEnabled() ? component : null; 184 _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null; 185 _function = Objects.requireNonNull( function ); 186 _argCount = argCount; 187 _flags = flags; 188 _equalityComparator = Objects.requireNonNull( equalityComparator ); 189 } 190 191 /** 192 * Return the result of the memoized function, calculating if necessary. 193 * 194 * @param args the arguments passed to the memoized function. 195 * @return the result of the memoized function. 196 */ 197 public T get( @Nonnull final Object... args ) 198 { 199 if ( Arez.shouldCheckApiInvariants() ) 200 { 201 apiInvariant( this::isNotDisposed, 202 () -> "Arez-0161: MemoizeCache named '" + _name + "' had get() invoked when disposed." ); 203 } 204 return getComputableValue( args ).get(); 205 } 206 207 @Override 208 public boolean isDisposed() 209 { 210 return _disposed; 211 } 212 213 @Override 214 public void dispose() 215 { 216 if ( !_disposed ) 217 { 218 _disposed = true; 219 getContext().safeAction( Arez.areNamesEnabled() ? _name : null, () -> { 220 disposeMap( _cache, _argCount ); 221 _cache.clear(); 222 }, ActionFlags.NO_VERIFY_ACTION_REQUIRED ); 223 } 224 } 225 226 @Nonnull 227 private ArezContext getContext() 228 { 229 return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context(); 230 } 231 232 /** 233 * Traverse to leaf map elements and dispose all contained ComputableValue instances. 234 */ 235 @SuppressWarnings( "unchecked" ) 236 private void disposeMap( @Nonnull final Map<Object, Object> map, final int depth ) 237 { 238 if ( 1 == depth ) 239 { 240 for ( final Map.Entry<Object, Object> entry : map.entrySet() ) 241 { 242 final ComputableValue<?> computableValue = (ComputableValue<?>) entry.getValue(); 243 computableValue.dispose(); 244 } 245 } 246 else 247 { 248 for ( final Map.Entry<Object, Object> entry : map.entrySet() ) 249 { 250 disposeMap( (Map<Object, Object>) entry.getValue(), depth - 1 ); 251 } 252 } 253 } 254 255 /** 256 * Retrieve the computable value for specified parameters, creating it if necessary. 257 * 258 * @param args the arguments passed to the memoized function. 259 * @return the computable value instance for the specified args. 260 */ 261 @SuppressWarnings( "unchecked" ) 262 @Nonnull 263 public ComputableValue<T> getComputableValue( @Nonnull final Object... args ) 264 { 265 if ( Arez.shouldCheckApiInvariants() ) 266 { 267 apiInvariant( () -> args.length == _argCount, 268 () -> "Arez-0162: MemoizeCache.getComputableValue called with " + args.length + 269 " arguments but expected " + _argCount + " arguments." ); 270 } 271 Map<Object, Object> map = _cache; 272 final int size = args.length - 1; 273 for ( int i = 0; i < size; i++ ) 274 { 275 map = (Map<Object, Object>) map.computeIfAbsent( args[ i ], v -> new HashMap<>() ); 276 } 277 ComputableValue<T> computableValue = 278 (ComputableValue<T>) map.computeIfAbsent( args[ size ], v -> createComputableValue( args ) ); 279 if ( Disposable.isDisposed( computableValue ) ) 280 { 281 computableValue = createComputableValue( args ); 282 map.put( args[ size ], computableValue ); 283 } 284 return computableValue; 285 } 286 287 /** 288 * Create computable value for specified parameters. 289 * 290 * @param args the arguments passed to the memoized function. 291 */ 292 @Nonnull 293 private ComputableValue<T> createComputableValue( @Nonnull final Object... args ) 294 { 295 final Component component = Arez.areNativeComponentsEnabled() ? _component : null; 296 final int id = _nextIndex++; 297 final String name = Arez.areNamesEnabled() ? _name + "." + id : null; 298 final SafeFunction<T> function = () -> { 299 Arez.context().registerHook( "$MC$", null, () -> disposeComputableValue( args ) ); 300 return _function.call( args ); 301 }; 302 final ComputableValue<T> computable = getContext().computable( component, name, function, _flags, _equalityComparator ); 303 for ( final Object arg : args ) 304 { 305 if ( arg instanceof DisposeNotifier ) 306 { 307 DisposeNotifier.asDisposeNotifier( arg ).addOnDisposeListener( computable, computable::dispose, false ); 308 } 309 } 310 return computable; 311 } 312 313 /** 314 * Method invoked to dispose memoized value. 315 * This is called from deactivate hook so there should always by a cached value present 316 * and thus we never check for missing elements in chain. 317 * 318 * @param args the arguments originally passed to the memoized function. 319 */ 320 @SuppressWarnings( "unchecked" ) 321 void disposeComputableValue( @Nonnull final Object... args ) 322 { 323 if ( Arez.shouldCheckInvariants() ) 324 { 325 invariant( () -> args.length == _argCount, 326 () -> "Arez-0163: MemoizeCache.disposeComputableValue called with " + args.length + 327 " argument(s) but expected " + _argCount + " argument(s)." ); 328 } 329 if ( _disposed ) 330 { 331 return; 332 } 333 final Stack<Map<Object, ?>> stack = new Stack<>(); 334 stack.push( _cache ); 335 final int size = args.length - 1; 336 for ( int i = 0; i < size; i++ ) 337 { 338 stack.push( (Map<Object, ?>) stack.peek().get( args[ i ] ) ); 339 } 340 final ComputableValue<T> computableValue = (ComputableValue<T>) stack.peek().remove( args[ size ] ); 341 if ( Arez.shouldCheckInvariants() ) 342 { 343 invariant( () -> null != computableValue, 344 () -> "Arez-0193: MemoizeCache.disposeComputableValue called with args " + Arrays.asList( args ) + 345 " but unable to locate corresponding ComputableValue." ); 346 } 347 assert null != computableValue; 348 getContext().task( Arez.areNamesEnabled() ? computableValue.getName() + ".dispose" : null, 349 computableValue::dispose, 350 Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK ); 351 for ( final Object arg : args ) 352 { 353 if ( arg instanceof DisposeNotifier ) 354 { 355 DisposeNotifier.asDisposeNotifier( arg ).removeOnDisposeListener( computableValue, false ); 356 } 357 } 358 while ( stack.size() > 1 ) 359 { 360 final Map<Object, ?> map = stack.pop(); 361 if ( map.isEmpty() ) 362 { 363 stack.peek().remove( args[ stack.size() - 1 ] ); 364 } 365 else 366 { 367 return; 368 } 369 } 370 } 371 372 @OmitSymbol 373 Map<Object, Object> getCache() 374 { 375 return _cache; 376 } 377 378 @OmitSymbol 379 int getNextIndex() 380 { 381 return _nextIndex; 382 } 383 384 @OmitSymbol 385 int getFlags() 386 { 387 return _flags; 388 } 389 390 @OmitSymbol 391 @Nonnull 392 EqualityComparator getEqualityComparator() 393 { 394 return _equalityComparator; 395 } 396}