1 /* 2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.video.internal; 18 19 import androidx.annotation.GuardedBy; 20 import androidx.camera.core.Logger; 21 import androidx.core.util.Pair; 22 import androidx.core.util.Preconditions; 23 24 import org.jspecify.annotations.NonNull; 25 26 import java.io.Closeable; 27 import java.nio.ByteBuffer; 28 import java.util.Locale; 29 import java.util.concurrent.Executor; 30 import java.util.concurrent.RejectedExecutionException; 31 import java.util.concurrent.atomic.AtomicInteger; 32 33 /** 34 * A read-only wrapper for a {@link ByteBuffer} that may be shared between multiple clients. 35 * 36 * <p>Every {@code SharedByteBuffer} must be closed with {@link #close()} when no longer in use. 37 * This will signal to other clients that it may be safe to reclaim or reuse the wrapped ByteBuffer. 38 * Failure to call {@link #close()} may lead to memory leaks or create stalls in processing 39 * pipelines. 40 * 41 * <p>The {@link ByteBuffer} returned by {@link #get()} is safe to read from as long as 42 * {@link #close()} has not been called. Once {@link #close()} has been called, it should be 43 * assumed the {@link ByteBuffer} contains invalid data. 44 */ 45 public final class SharedByteBuffer implements Closeable { 46 47 private static final String TAG = "SharedByteBuffer"; 48 49 private final ByteBuffer mSharedByteBuffer; 50 51 // Unique ID for the original SharedByteBuffer. Transferred to all share() instances. 52 private final int mShareId; 53 54 private final Object mCloseLock = new Object(); 55 56 private final Pair<Executor, Runnable> mFinalCloseAction; 57 58 @GuardedBy("mCloseLock") 59 private final AtomicInteger mSharedRefCount; 60 61 @GuardedBy("mCloseLock") 62 private boolean mClosed = false; 63 64 /** 65 * Creates an initial instance of SharedByteBuffer for sharing a {@link ByteBuffer} between 66 * multiple clients. 67 * 68 * <p>The initial SharedByteBuffer holds the initial reference count. Each subsequent call to 69 * {@link #share()} will increment the reference count. Once all instances generated by 70 * {@link #share()} have been closed, and the initial instance has also been closed, the 71 * provided {@code finalCloseAction} will run. 72 * 73 * @param sharedBuf The buffer to be wrapped. 74 * @param closeActionExecutor The executor used to execute {@code finalCloseAction}. 75 * @param finalCloseAction The action to take once all shared instances have been closed. 76 * This action will only be run once the initial instance and all 77 * instances generated by {@link #share()} have been closed. A common 78 * {@code finalCloseAction} might be to return the ByteBuffer to a 79 * pool so it can be reused. 80 */ newSharedInstance(@onNull ByteBuffer sharedBuf, @NonNull Executor closeActionExecutor, @NonNull Runnable finalCloseAction)81 static @NonNull SharedByteBuffer newSharedInstance(@NonNull ByteBuffer sharedBuf, 82 @NonNull Executor closeActionExecutor, @NonNull Runnable finalCloseAction) { 83 AtomicInteger sharedRefCount = new AtomicInteger(1); 84 int shareId = System.identityHashCode(sharedBuf); 85 ByteBuffer readOnlyBuf = Preconditions.checkNotNull(sharedBuf).asReadOnlyBuffer(); 86 return new SharedByteBuffer(readOnlyBuf, sharedRefCount, 87 new Pair<>(Preconditions.checkNotNull(closeActionExecutor), 88 Preconditions.checkNotNull(finalCloseAction)), shareId); 89 } 90 91 /** 92 * Creates a new instance of a SharedByteBuffer from an existing ByteBuffer and ref count. 93 * 94 * <p>This constructor must always be used with a pre-incremented {@code sharedRefCount}. 95 */ SharedByteBuffer(@onNull ByteBuffer sharedBuf, @NonNull AtomicInteger sharedRefCount, @NonNull Pair<Executor, Runnable> finalCloseAction, int shareId)96 private SharedByteBuffer(@NonNull ByteBuffer sharedBuf, @NonNull AtomicInteger sharedRefCount, 97 @NonNull Pair<Executor, Runnable> finalCloseAction, int shareId) { 98 99 mSharedByteBuffer = sharedBuf; 100 mSharedRefCount = sharedRefCount; 101 mFinalCloseAction = finalCloseAction; 102 mShareId = shareId; 103 104 if (Logger.isDebugEnabled(TAG)) { 105 int refCount = sharedRefCount.get(); 106 if (refCount < 1) { 107 throw new AssertionError(String.format(Locale.US, "Cannot create new " 108 + "instance of SharedByteBuffer with invalid ref count %d. Ref count must " 109 + "be >= 1. [%s]", refCount, toString())); 110 } 111 } 112 } 113 114 /** 115 * Creates a new instance of SharedByteBuffer which references the same underlying ByteBuffer. 116 * 117 * <p>This creates a new read-only instance of the same underlying {@link ByteBuffer} that 118 * can be retrieved with {@link #get()}. While the two instances are independent, the new 119 * instance's initial position, limit, and mark values will match those of this instance's 120 * values at the time {@code share()} is called. 121 */ share()122 @NonNull SharedByteBuffer share() { 123 int newRefCount; 124 AtomicInteger sharedRefCountLocal; 125 synchronized (mCloseLock) { 126 checkNotClosed("share()"); 127 newRefCount = mSharedRefCount.incrementAndGet(); 128 sharedRefCountLocal = mSharedRefCount; 129 } 130 131 if (Logger.isDebugEnabled(TAG)) { 132 if (newRefCount <= 1) { 133 throw new AssertionError("Invalid ref count. share() should always produce " 134 + "a ref count of 2 or more."); 135 } 136 Logger.d(TAG, String.format(Locale.US, "Ref count incremented: %d [%s]", 137 newRefCount, toString())); 138 } 139 140 return new SharedByteBuffer(mSharedByteBuffer.asReadOnlyBuffer(), sharedRefCountLocal, 141 mFinalCloseAction, mShareId); 142 } 143 144 /** 145 * Closes this instance of SharedByteBuffer. 146 * 147 * <p>Once an instance is closed, {@link #get()} can no longer be used to retrieve the 148 * buffer. If any reference to the buffer exists from a previous invocation of {@link #get()} 149 * it should no longer be used. 150 * 151 * <p>Closing a SharedByteBuffer is idempotent. Only the first {@code close()} will have any 152 * effect. Subsequent calls will be no-ops. 153 * 154 */ 155 @Override close()156 public void close() { 157 closeInternal(); 158 } 159 160 /** 161 * Returns a read-only view into the {@link ByteBuffer} instance that is being shared. 162 * 163 * <p>Once an instance is closed with {@link #close()}, this method can no longer be used to 164 * retrieve the buffer. Once closed, the object returned by this method should be assumed 165 * invalid and should no longer be used. 166 * 167 * @return the underlying shared {@link ByteBuffer} 168 * @throws IllegalStateException if this SharedByteBuffer is closed. 169 * @see #close() 170 */ get()171 public @NonNull ByteBuffer get() { 172 synchronized (mCloseLock) { 173 checkNotClosed("get()"); 174 return mSharedByteBuffer; 175 } 176 } 177 178 @GuardedBy("mCloseLock") checkNotClosed(@onNull String caller)179 private void checkNotClosed(@NonNull String caller) { 180 if (mClosed) { 181 throw new IllegalStateException("Cannot call " + caller + " on a closed " 182 + "SharedByteBuffer."); 183 } 184 } 185 186 @Override toString()187 public @NonNull String toString() { 188 return String.format(Locale.US, "SharedByteBuffer[buf: %s, shareId: 0x%x, instanceId:0x%x]", 189 mSharedByteBuffer, mShareId, System.identityHashCode(this)); 190 } 191 192 // Finalizer as a safety net. This will introduce a performance penalty on construction but it 193 // is often critical that all SharedByteBuffer instances are closed, so we eat the penalty. 194 @Override finalize()195 protected void finalize() throws Throwable { 196 try { 197 if (closeInternal()) { 198 Logger.w(TAG, String.format(Locale.US, 199 "SharedByteBuffer closed by finalizer, but should have been closed " 200 + "manually with SharedByteBuffer.close() [%s]", 201 toString())); 202 } 203 } finally { 204 super.finalize(); 205 } 206 207 } 208 209 // Closes the SharedByteBuffer and returns true if the buffer was not already closed. Returns 210 // false if the SharedByteBuffer was already closed. closeInternal()211 private boolean closeInternal() { 212 int newRefCount; 213 synchronized (mCloseLock) { 214 if (mClosed) { 215 // close() is idempotent. Only initial close() should have any affect on ref count. 216 return false; 217 } else { 218 mClosed = true; 219 } 220 newRefCount = mSharedRefCount.decrementAndGet(); 221 } 222 223 if (Logger.isDebugEnabled(TAG)) { 224 if (newRefCount < 0) { 225 throw new AssertionError("Invalid ref count. close() should never produce a " 226 + "ref count below 0"); 227 } 228 // Note there is no guarantee this log will be printed in order with other shared 229 // instances log since there is no mutual exclusion around when logs are printed. 230 // This may make it appear as if ref counts are being decremented out of order, but 231 // that is not the case. 232 Logger.d(TAG, String.format(Locale.US, "Ref count decremented: %d [%s]", newRefCount, 233 toString())); 234 } 235 236 if (newRefCount == 0) { 237 if (Logger.isDebugEnabled(TAG)) { 238 Logger.d(TAG, String.format(Locale.US, "Final reference released. Running final " 239 + "close action. [%s]", toString())); 240 } 241 242 try { 243 Executor executor = Preconditions.checkNotNull(mFinalCloseAction.first); 244 Runnable runnable = Preconditions.checkNotNull(mFinalCloseAction.second); 245 executor.execute(runnable); 246 } catch (RejectedExecutionException e) { 247 Logger.e(TAG, String.format(Locale.US, "Unable to execute final close action. " 248 + "[%s]", toString()), e); 249 } 250 } 251 252 return true; 253 } 254 } 255