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.Procedure; 010import arez.SafeFunction; 011import arez.Task; 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 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 private final int _flags; 083 /** 084 * The index of the next ComputableValue created. 085 * This is only used when creating unique names for ComputableValues. 086 */ 087 private int _nextIndex; 088 /** 089 * Flag indicating that the cache is currently being disposed. 090 */ 091 private boolean _disposed; 092 093 /** 094 * Create the Memoize method cache. 095 * 096 * @param context the context in which to create ComputableValue instances. 097 * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true. 098 * @param name a human consumable prefix for computable values. 099 * @param function the memoized function. 100 * @param argCount the number of arguments expected to be passed to memoized function. 101 */ 102 public MemoizeCache( @Nullable final ArezContext context, 103 @Nullable final Component component, 104 @Nullable final String name, 105 @Nonnull final Function<T> function, 106 final int argCount ) 107 { 108 this( context, component, name, function, argCount, 0 ); 109 } 110 111 /** 112 * Create the Memoize method cache. 113 * 114 * @param context the context in which to create ComputableValue instances. 115 * @param component the associated native component if any. This should only be set if {@link Arez#areNativeComponentsEnabled()} returns true. 116 * @param name a human consumable prefix for computable values. 117 * @param function the memoized function. 118 * @param argCount the number of arguments expected to be passed to memoized function. 119 * @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}. 120 */ 121 public MemoizeCache( @Nullable final ArezContext context, 122 @Nullable final Component component, 123 @Nullable final String name, 124 @Nonnull final Function<T> function, 125 final int argCount, 126 @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) final int flags ) 127 { 128 if ( Arez.shouldCheckApiInvariants() ) 129 { 130 apiInvariant( () -> Arez.areZonesEnabled() || null == context, 131 () -> "Arez-174: MemoizeCache passed a context but Arez.areZonesEnabled() is false" ); 132 apiInvariant( () -> Arez.areNamesEnabled() || null == name, 133 () -> "Arez-0159: MemoizeCache passed a name '" + name + "' but Arez.areNamesEnabled() is false" ); 134 apiInvariant( () -> argCount > 0, 135 () -> "Arez-0160: MemoizeCache constructed with invalid argCount: " + argCount + 136 ". Expected positive value." ); 137 final int mask = ComputableValue.Flags.PRIORITY_HIGHEST | 138 ComputableValue.Flags.PRIORITY_HIGH | 139 ComputableValue.Flags.PRIORITY_NORMAL | 140 ComputableValue.Flags.PRIORITY_LOW | 141 ComputableValue.Flags.PRIORITY_LOWEST | 142 ComputableValue.Flags.NO_REPORT_RESULT | 143 ComputableValue.Flags.AREZ_DEPENDENCIES | 144 ComputableValue.Flags.AREZ_OR_NO_DEPENDENCIES | 145 ComputableValue.Flags.AREZ_OR_EXTERNAL_DEPENDENCIES | 146 ComputableValue.Flags.OBSERVE_LOWER_PRIORITY_DEPENDENCIES | 147 ComputableValue.Flags.READ_OUTSIDE_TRANSACTION; 148 149 apiInvariant( () -> ( ~mask & flags ) == 0, 150 () -> "Arez-0211: MemoizeCache passed unsupported flags. Unsupported bits: " + ( ~mask & flags ) ); 151 } 152 _context = Arez.areZonesEnabled() ? Objects.requireNonNull( context ) : null; 153 _component = Arez.areNativeComponentsEnabled() ? component : null; 154 _name = Arez.areNamesEnabled() ? Objects.requireNonNull( name ) : null; 155 _function = Objects.requireNonNull( function ); 156 _argCount = argCount; 157 _flags = flags; 158 } 159 160 /** 161 * Return the result of the memoized function, calculating if necessary. 162 * 163 * @param args the arguments passed to the memoized function. 164 * @return the result of the memoized function. 165 */ 166 public T get( @Nonnull final Object... args ) 167 { 168 if ( Arez.shouldCheckApiInvariants() ) 169 { 170 apiInvariant( this::isNotDisposed, 171 () -> "Arez-0161: MemoizeCache named '" + _name + "' had get() invoked when disposed." ); 172 } 173 return getComputableValue( args ).get(); 174 } 175 176 @Override 177 public boolean isDisposed() 178 { 179 return _disposed; 180 } 181 182 @Override 183 public void dispose() 184 { 185 if ( !_disposed ) 186 { 187 _disposed = true; 188 getContext().safeAction( Arez.areNamesEnabled() ? _name : null, () -> { 189 disposeMap( _cache, _argCount ); 190 _cache.clear(); 191 }, ActionFlags.NO_VERIFY_ACTION_REQUIRED ); 192 } 193 } 194 195 @Nonnull 196 private ArezContext getContext() 197 { 198 return Arez.areZonesEnabled() ? Objects.requireNonNull( _context ) : Arez.context(); 199 } 200 201 /** 202 * Traverse to leaf map elements and dispose all contained ComputableValue instances. 203 */ 204 @SuppressWarnings( "unchecked" ) 205 private void disposeMap( @Nonnull final Map<Object, Object> map, final int depth ) 206 { 207 if ( 1 == depth ) 208 { 209 for ( final Map.Entry<Object, Object> entry : map.entrySet() ) 210 { 211 final ComputableValue<?> computableValue = (ComputableValue<?>) entry.getValue(); 212 computableValue.dispose(); 213 } 214 } 215 else 216 { 217 for ( final Map.Entry<Object, Object> entry : map.entrySet() ) 218 { 219 disposeMap( (Map<Object, Object>) entry.getValue(), depth - 1 ); 220 } 221 } 222 } 223 224 /** 225 * Retrieve the computable value for specified parameters, creating it if necessary. 226 * 227 * @param args the arguments passed to the memoized function. 228 * @return the computable value instance for the specified args. 229 */ 230 @SuppressWarnings( "unchecked" ) 231 @Nonnull 232 public ComputableValue<T> getComputableValue( @Nonnull final Object... args ) 233 { 234 if ( Arez.shouldCheckApiInvariants() ) 235 { 236 apiInvariant( () -> args.length == _argCount, 237 () -> "Arez-0162: MemoizeCache.getComputableValue called with " + args.length + 238 " arguments but expected " + _argCount + " arguments." ); 239 } 240 Map<Object, Object> map = _cache; 241 final int size = args.length - 1; 242 for ( int i = 0; i < size; i++ ) 243 { 244 map = (Map<Object, Object>) map.computeIfAbsent( args[ i ], v -> new HashMap<>() ); 245 } 246 ComputableValue<T> computableValue = 247 (ComputableValue<T>) map.computeIfAbsent( args[ size ], v -> createComputableValue( args ) ); 248 if ( Disposable.isDisposed( computableValue ) ) 249 { 250 computableValue = createComputableValue( args ); 251 map.put( args[ size ], computableValue ); 252 } 253 return computableValue; 254 } 255 256 /** 257 * Create computable value for specified parameters. 258 * 259 * @param args the arguments passed to the memoized function. 260 */ 261 @Nonnull 262 private ComputableValue<T> createComputableValue( @Nonnull final Object... args ) 263 { 264 final Component component = Arez.areNativeComponentsEnabled() ? _component : null; 265 final String name = Arez.areNamesEnabled() ? _name + "." + _nextIndex++ : null; 266 final Procedure onDeactivate = () -> disposeComputableValue( args ); 267 final SafeFunction<T> function = () -> _function.call( args ); 268 return getContext().computable( component, name, function, null, onDeactivate, _flags ); 269 } 270 271 /** 272 * Method invoked to dispose memoized value. 273 * This is called from deactivate hook so there should always by a cached value present 274 * and thus we never check for missing elements in chain. 275 * 276 * @param args the arguments originally passed to the memoized function. 277 */ 278 @SuppressWarnings( "unchecked" ) 279 void disposeComputableValue( @Nonnull final Object... args ) 280 { 281 if ( Arez.shouldCheckInvariants() ) 282 { 283 invariant( () -> args.length == _argCount, 284 () -> "Arez-0163: MemoizeCache.disposeComputableValue called with " + args.length + 285 " argument(s) but expected " + _argCount + " argument(s)." ); 286 } 287 if ( _disposed ) 288 { 289 return; 290 } 291 final Stack<Map<Object, ?>> stack = new Stack<>(); 292 stack.push( _cache ); 293 final int size = args.length - 1; 294 for ( int i = 0; i < size; i++ ) 295 { 296 stack.push( (Map<Object, ?>) stack.peek().get( args[ i ] ) ); 297 } 298 final ComputableValue<T> computableValue = (ComputableValue<T>) stack.peek().remove( args[ size ] ); 299 if ( Arez.shouldCheckInvariants() ) 300 { 301 invariant( () -> null != computableValue, 302 () -> "Arez-0193: MemoizeCache.disposeComputableValue called with args " + Arrays.asList( args ) + 303 " but unable to locate corresponding ComputableValue." ); 304 } 305 assert null != computableValue; 306 getContext().task( Arez.areNamesEnabled() ? computableValue.getName() + ".dispose" : null, 307 computableValue::dispose, 308 Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK ); 309 while ( stack.size() > 1 ) 310 { 311 final Map<Object, ?> map = stack.pop(); 312 if ( map.isEmpty() ) 313 { 314 stack.peek().remove( args[ stack.size() - 1 ] ); 315 } 316 else 317 { 318 return; 319 } 320 } 321 } 322 323 @OmitSymbol 324 Map<Object, Object> getCache() 325 { 326 return _cache; 327 } 328 329 @OmitSymbol 330 int getNextIndex() 331 { 332 return _nextIndex; 333 } 334 335 @OmitSymbol 336 int getFlags() 337 { 338 return _flags; 339 } 340}