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 grim.annotations.OmitSymbol; 012import java.util.Arrays; 013import java.util.HashMap; 014import java.util.Map; 015import java.util.Objects; 016import java.util.Stack; 017import javax.annotation.Nonnull; 018import javax.annotation.Nullable; 019import org.intellij.lang.annotations.MagicConstant; 020import static org.realityforge.braincheck.Guards.*; 021 022/** 023 * The class responsible for caching ComputableValue instances for different input parameters. 024 */ 025public final class MemoizeCache<T> 026 implements Disposable 027{ 028 /** 029 * Functional interface for calculating memoizable value. 030 * 031 * @param <T> The type of the returned value. 032 */ 033 @FunctionalInterface 034 public interface Function<T> 035 { 036 /** 037 * Return calculated memoizable value. 038 * 039 * @param args the functions arguments. 040 * @return the value generated by function. 041 */ 042 T call( @Nonnull final Object... args ); 043 } 044 045 /** 046 * Reference to the system to which this node belongs. 047 */ 048 @OmitSymbol( unless = "arez.enable_zones" ) 049 @Nullable 050 private final ArezContext _context; 051 /** 052 * A human consumable prefix for computable values. It should be non-null if {@link Arez#areNamesEnabled()} returns 053 * true and <code>null</code> otherwise. 054 */ 055 @Nullable 056 @OmitSymbol( unless = "arez.enable_names" ) 057 private final String _name; 058 /** 059 * The component that this memoize cache is contained within. 060 * This should only be set if {@link Arez#areNativeComponentsEnabled()} is true but can be null even if this is true. 061 */ 062 @OmitSymbol( unless = "arez.enable_native_components" ) 063 @Nullable 064 private final Component _component; 065 /** 066 * The function memoized. 067 */ 068 @Nonnull 069 private final Function<T> _function; 070 /** 071 * The cache of all the ComputableValue created for each unique combination of parameters. 072 */ 073 private final Map<Object, Object> _cache = new HashMap<>(); 074 /** 075 * The number of arguments passed to memoized function. 076 */ 077 private final int _argCount; 078 /** 079 * The flags passed to the created ComputableValues. 080 */ 081 @MagicConstant( flagsFromClass = ComputableValue.Flags.class ) 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 SafeFunction<T> function = () -> { 267 Arez.context().registerHook( "$MC$", null, () -> disposeComputableValue( args ) ); 268 return _function.call( args ); 269 }; 270 return getContext().computable( component, name, function, _flags ); 271 } 272 273 /** 274 * Method invoked to dispose memoized value. 275 * This is called from deactivate hook so there should always by a cached value present 276 * and thus we never check for missing elements in chain. 277 * 278 * @param args the arguments originally passed to the memoized function. 279 */ 280 @SuppressWarnings( "unchecked" ) 281 void disposeComputableValue( @Nonnull final Object... args ) 282 { 283 if ( Arez.shouldCheckInvariants() ) 284 { 285 invariant( () -> args.length == _argCount, 286 () -> "Arez-0163: MemoizeCache.disposeComputableValue called with " + args.length + 287 " argument(s) but expected " + _argCount + " argument(s)." ); 288 } 289 if ( _disposed ) 290 { 291 return; 292 } 293 final Stack<Map<Object, ?>> stack = new Stack<>(); 294 stack.push( _cache ); 295 final int size = args.length - 1; 296 for ( int i = 0; i < size; i++ ) 297 { 298 stack.push( (Map<Object, ?>) stack.peek().get( args[ i ] ) ); 299 } 300 final ComputableValue<T> computableValue = (ComputableValue<T>) stack.peek().remove( args[ size ] ); 301 if ( Arez.shouldCheckInvariants() ) 302 { 303 invariant( () -> null != computableValue, 304 () -> "Arez-0193: MemoizeCache.disposeComputableValue called with args " + Arrays.asList( args ) + 305 " but unable to locate corresponding ComputableValue." ); 306 } 307 assert null != computableValue; 308 getContext().task( Arez.areNamesEnabled() ? computableValue.getName() + ".dispose" : null, 309 computableValue::dispose, 310 Task.Flags.PRIORITY_HIGHEST | Task.Flags.DISPOSE_ON_COMPLETE | Task.Flags.NO_WRAP_TASK ); 311 while ( stack.size() > 1 ) 312 { 313 final Map<Object, ?> map = stack.pop(); 314 if ( map.isEmpty() ) 315 { 316 stack.peek().remove( args[ stack.size() - 1 ] ); 317 } 318 else 319 { 320 return; 321 } 322 } 323 } 324 325 @OmitSymbol 326 Map<Object, Object> getCache() 327 { 328 return _cache; 329 } 330 331 @OmitSymbol 332 int getNextIndex() 333 { 334 return _nextIndex; 335 } 336 337 @OmitSymbol 338 int getFlags() 339 { 340 return _flags; 341 } 342}