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}