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