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