001/*******************************************************************************
002 * Copyright (c) 2024, 2026, Olivier Ayache.  All rights reserved.
003 *
004 * This file is part of AVPKit.
005 *
006 * AVPKit is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Lesser General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * AVPKit is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with AVPKit.  If not, see <http://www.gnu.org/licenses/>.
018 *******************************************************************************/
019
020package com.avpkit.ferry;
021
022import java.util.HashSet;
023import java.util.Set;
024import java.util.concurrent.locks.ReentrantLock;
025
026import org.slf4j.Logger;
027import org.slf4j.LoggerFactory;
028
029/**
030 * Internal Only.
031 * <p>
032 * This object allocates large memory chunks and returns them to native code.
033 * The native memory then pins raw bytes based on the byte[]s returned here. The
034 * net effect is that Java ends up thinking it actually allocated the memory,
035 * but since a {@link RefCounted} object will also maintain a reference to this
036 * allocator, you can use this to detect instances of 'leaked' references in
037 * your Java code.
038 * </p>
039 * <p>
040 * This function is called DIRECTLY from native code; names of methods MUST NOT
041 * CHANGE.
042 * </p>
043 * 
044 * @author aclarke
045 * 
046 */
047
048public final class JNIMemoryAllocator
049{
050  private static final Logger log = LoggerFactory
051      .getLogger(JNIMemoryAllocator.class);
052
053  // don't allocate this unless we malloc; most classes don't even
054  // touch this memory manager with a 100 foot pole
055  final private Set<byte[]> mBuffers =
056    new HashSet<byte[]>();
057  final private ReentrantLock mLock = 
058    new ReentrantLock();
059  final static private int MAX_ALLOCATION_ATTEMPTS = 5;
060  final static private double FALLBACK_TIME_DECAY = 1.5;
061  final static private boolean SHOULD_RETRY_FAILED_ALLOCS = true;
062
063  private void addToBuffer(byte[] mem)
064  {
065    mLock.lock();
066    try
067    {
068      if (!mBuffers.add(mem))
069      {
070        assert false : "buffers already added";
071      }
072    }
073    finally
074    {
075      mLock.unlock();
076    }
077  }
078
079  private void removeFromBuffer(byte[] mem)
080  {
081    mLock.lock();
082    try
083    {
084      if (!mBuffers.remove(mem))
085      {
086        assert false : "buffer not in memory";
087      }
088    }
089    finally
090    {
091      mLock.unlock();
092    }
093  }
094  
095  /**
096   * Not for use outside the package
097   */
098  JNIMemoryAllocator()
099  {
100    
101  }
102  
103  /**
104   * Internal Only.  Allocate a new block of bytes. Called from native code.
105   * <p>
106   * Will retry many times if it can't get memory, backing off
107   * in timeouts to get there.
108   * </p>
109   * <p>
110   * Callers must eventually call {@link #free(byte[])} when done
111   * with the bytes or a leak will result.
112   * </p>
113   * @param size
114   *          # of bytes requested
115   * @return An array of size or more bytes long, or null on failure.
116   */
117
118  public byte[] malloc(int size)
119  {
120    byte[] retval = null;
121    // first check the parachute
122    JNIMemoryParachute.getParachute().packChute();
123    try
124    {
125      if (SHOULD_RETRY_FAILED_ALLOCS)
126      {
127        int allocationAttempts = 0;
128        int backoffTimeout = 10; // start at 10 milliseconds
129        while (true)
130        {
131          try
132          {
133            // log.debug("attempting malloc of size: {}", size);
134            retval = new byte[size];
135            // log.debug("malloced block of size: {}", size);
136            // we succeed, so break out
137            break;
138          }
139          catch (final OutOfMemoryError e)
140          {
141            // try clearing our queue now and do it again. Why?
142            // because the first failure may have allowed us to
143            // catch a RefCounted no longer in use, and the second
144            // attempt may have freed that memory.
145
146            // do a JNI collect before the alloc
147            ++allocationAttempts;
148            if (allocationAttempts >= MAX_ALLOCATION_ATTEMPTS)
149            {
150              // try pulling our rip cord
151              JNIMemoryParachute.getParachute().pullCord();
152              // do one last "hope gc" to free our own memory
153              JNIReference.getMgr().gcInternal();
154              // and throw the error back to the native code
155              throw e;
156            }
157
158            log.debug("retrying ({}) allocation of {} bytes",
159                allocationAttempts, size);
160            try
161            {
162              // give the finalizer a chance
163              if (allocationAttempts <= 1)
164              {
165                // first just yield
166                Thread.yield();
167              }
168              else
169              {
170                Thread.sleep(backoffTimeout);
171                // and slowly get longer...
172                backoffTimeout = (int) (backoffTimeout * FALLBACK_TIME_DECAY);
173              }
174            }
175            catch (InterruptedException e1)
176            {
177              // reset the interruption so underlying
178              // code can also interrupt
179              Thread.currentThread().interrupt();
180              // and throw the error condition
181              throw e;
182            }
183            // do a JNI collect before the alloc
184            JNIReference.getMgr().gcInternal();
185          }
186        }
187      }
188      else
189      {
190        retval = new byte[size];
191      }
192      addToBuffer(retval);
193      retval[retval.length - 1] = 0;
194      
195//      log.debug("malloc: {}({}:{})", new Object[]
196//      {
197//          retval.hashCode(), retval.length, size
198//      });
199      
200    }
201    catch (Throwable t)
202    {
203      // do not let an exception leak out since we go back to native code.
204      retval = null;
205    }
206    return retval;
207  }
208
209  /**
210   * Free memory allocated by the {@link #malloc(int)} method.
211   * Called from native code.
212   * 
213   * @param mem
214   *          the byes to be freed.
215   */
216
217  public void free(byte[] mem)
218  {
219    removeFromBuffer(mem);
220//      log.debug("free:   {}({})", mem.hashCode(), mem.length);
221  }
222
223  /**
224   * Internal Only.  Native method that tells a native objects (represented by the nativeObj
225   * long pointer val) that this JNIMemoryAllocator is being used to allocate
226   * it's large blocks of memory.
227   * <p>
228   * Follow that? No. That's OK... you really don't want to know.
229   * </p>
230   * 
231   * @param nativeObj
232   *          A C pointer (a la swig).
233   * @param mgr
234   *          Us.
235   */
236
237  public native static void setAllocator(long nativeObj, JNIMemoryAllocator mgr);
238
239  /**
240   * Internal Only.  Get the allocator for the underlying native pointer.
241   * 
242   * @param nativeObj
243   *          The native pointer.
244   * @return The allocator to use, or null.
245   */
246
247  public native static JNIMemoryAllocator getAllocator(long nativeObj);
248
249}