• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bernardladenthin / BitcoinAddressFinder / #315

23 May 2025 03:19PM UTC coverage: 66.005% (-1.9%) from 67.925%
#315

push

bernardladenthin
Refactor OpenCL resource handling and improve consistency across API

- Introduced AutoCloseable for OpenCL resources (OpenClTask, OpenCLContext)
- Replaced manual release() methods with try-with-resources using close()
- Added isClosed() for defensive resource lifecycle checks
- Refactored buffer handling to separate host and device pointers
- Renamed createSecrets() param for clarity (overallWorkSize instead of batchSizeInBits)
- Updated all tests and affected logic to match new API and lifecycle
- Enhanced OpenCLGridResult to avoid unnecessary buffer duplication
- Improved benchmark documentation in README with CPU column
- Added optional LMDB in-memory caching in configuration

11 of 118 new or added lines in 10 files covered. (9.32%)

3 existing lines in 2 files now uncovered.

1231 of 1865 relevant lines covered (66.01%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/java/net/ladenthin/bitcoinaddressfinder/OpenClTask.java
1
// @formatter:off
2
/**
3
 * Copyright 2020 Bernard Ladenthin bernard.ladenthin@gmail.com
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *    http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 *
17
 */
18
// @formatter:on
19
package net.ladenthin.bitcoinaddressfinder;
20

21
import com.google.common.annotations.VisibleForTesting;
22
import java.math.BigInteger;
23
import java.nio.ByteBuffer;
24
import java.nio.ByteOrder;
25
import net.ladenthin.bitcoinaddressfinder.configuration.CProducer;
26
import static org.jocl.CL.CL_MEM_READ_ONLY;
27
import static org.jocl.CL.CL_MEM_USE_HOST_PTR;
28
import static org.jocl.CL.CL_MEM_WRITE_ONLY;
29
import static org.jocl.CL.CL_TRUE;
30
import static org.jocl.CL.clCreateBuffer;
31
import static org.jocl.CL.clEnqueueNDRangeKernel;
32
import static org.jocl.CL.clEnqueueReadBuffer;
33
import static org.jocl.CL.clEnqueueWriteBuffer;
34
import static org.jocl.CL.clFinish;
35
import static org.jocl.CL.clReleaseMemObject;
36
import static org.jocl.CL.clSetKernelArg;
37
import org.jocl.Pointer;
38
import org.jocl.Sizeof;
39
import org.jocl.cl_command_queue;
40
import org.jocl.cl_context;
41
import org.jocl.cl_kernel;
42
import org.jocl.cl_mem;
43
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
45

46
public class OpenClTask implements ReleaseCLObject {
47

48
    protected Logger logger = LoggerFactory.getLogger(this.getClass());
×
49
    
NEW
50
    private final int PRIVATE_KEY_SOURCE_SIZE_IN_BYTES = PublicKeyBytes.PRIVATE_KEY_MAX_NUM_BYTES;
×
51
    
52
    private final CProducer cProducer;
53

54
    private final cl_context context;
55
    
56
    private final SourceArgument privateKeySourceArgument;
57
    
58
    private final BitHelper bitHelper;
59
    private final ByteBufferUtility byteBufferUtility;
60
    private final BigInteger maxPrivateKeyForBatchSize;
61
    
NEW
62
    private boolean closed = false;
×
63

64
    public abstract static class CLByteBufferPointerArgument implements ReleaseCLObject {
65
        /**
66
         * Controls how memory is allocated for the OpenCL output buffer.
67
         *
68
         * If set to {@link org.jocl.CL#CL_MEM_USE_HOST_PTR}, the OpenCL buffer is created using a host pointer,
69
         * meaning the host's {@link ByteBuffer} is directly used by the device (zero-copy if supported).
70
         * This may reduce memory copy overhead on some platforms, but:
71
         * <ul>
72
         *     <li>It requires the buffer to remain valid and pinned in memory.</li>
73
         *     <li>On some OpenCL implementations or devices (e.g. discrete GPUs), it may cause slower access due to lack of true zero-copy support.</li>
74
         *     <li>Debugging and compatibility issues can arise if host memory alignment or page-locking requirements aren't met.</li>
75
         * </ul>
76
         *
77
         * If set to {@link org.jocl.CL#CL_MEM_WRITE_ONLY}, the buffer is created with no reference to host memory,
78
         * and OpenCL manages the memory internally. This is typically safer and potentially faster on discrete GPUs,
79
         * although it requires an explicit copy back to the host after kernel execution.
80
         *
81
         * In most cases, {@link org.jocl.CL#CL_MEM_WRITE_ONLY} (i.e. setting this flag to {@code false}) is more robust and portable.
82
         */
83
        protected static final boolean USE_HOST_PTR = false;
84

85
        protected final ByteBuffer byteBuffer;
86
        protected final Pointer hostMemoryPointer;
87
        protected final cl_mem mem;
88
        protected final Pointer clMemPointer;
NEW
89
        private boolean closed = false;
×
90

NEW
91
        public CLByteBufferPointerArgument(ByteBuffer byteBuffer, Pointer hostMemoryPointer, cl_mem mem, Pointer clMemPointer) {
×
NEW
92
            this.byteBuffer = byteBuffer;
×
NEW
93
            this.hostMemoryPointer = hostMemoryPointer;
×
NEW
94
            this.mem = mem;
×
NEW
95
            this.clMemPointer = clMemPointer;
×
NEW
96
        }
×
97

98
        public ByteBuffer getByteBuffer() {
NEW
99
            return byteBuffer;
×
100
        }
101

102
        /** Used for reading/writing data to the host via clEnqueueRead/WriteBuffer. */
103
        public Pointer getHostMemoryPointer() {
NEW
104
            return hostMemoryPointer;
×
105
        }
106

107
        /** Used to pass the buffer to the kernel via clSetKernelArg. */
108
        public Pointer getClMemPointer() {
NEW
109
            return clMemPointer;
×
110
        }
111

112
        public cl_mem getMem() {
NEW
113
            return mem;
×
114
        }
115

116
        @Override
117
        public boolean isClosed() {
NEW
118
            return closed;
×
119
        }
120

121
        @Override
122
        public void close() {
NEW
123
            if (!closed) {
×
NEW
124
                clReleaseMemObject(mem);
×
NEW
125
                closed = true;
×
126
            }
NEW
127
        }
×
128
    }
129

130
    public static class DestinationArgument extends CLByteBufferPointerArgument {
131
        
132
        private DestinationArgument(ByteBuffer byteBuffer, Pointer hostMemoryPointer, cl_mem mem, Pointer clMemPointer) {
NEW
133
            super(byteBuffer, hostMemoryPointer, mem, clMemPointer);
×
NEW
134
        }
×
135

136
        public static DestinationArgument create(cl_context context, long sizeInBytes) {
NEW
137
            final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(ByteBufferUtility.ensureByteBufferCapacityFitsInt(sizeInBytes));
×
NEW
138
            final Pointer hostMemoryPointer = Pointer.to(byteBuffer);
×
139
            final cl_mem mem;
140

141
            if (USE_HOST_PTR) {
142
                mem = clCreateBuffer(context, CL_MEM_USE_HOST_PTR, sizeInBytes, hostMemoryPointer, null);
143
            } else {
NEW
144
                mem = clCreateBuffer(context, CL_MEM_WRITE_ONLY, sizeInBytes, null, null);
×
145
            }
NEW
146
            final Pointer clMemPointer = Pointer.to(mem);
×
147

NEW
148
            return new DestinationArgument(byteBuffer, hostMemoryPointer, mem, clMemPointer);
×
149
        }
150
        
151
    }
152
    
153
    public static class SourceArgument extends CLByteBufferPointerArgument {
154

155
        private SourceArgument(ByteBuffer byteBuffer, Pointer hostMemoryPointer, cl_mem mem, Pointer clMemPointer) {
NEW
156
            super(byteBuffer, hostMemoryPointer, mem, clMemPointer);
×
NEW
157
        }
×
158

159
        public static SourceArgument create(cl_context context, long sizeInBytes) {
NEW
160
            final ByteBuffer byteBuffer = ByteBuffer.allocateDirect(ByteBufferUtility.ensureByteBufferCapacityFitsInt(sizeInBytes));
×
NEW
161
            final Pointer hostMemoryPointer = Pointer.to(byteBuffer);
×
NEW
162
            final cl_mem mem = clCreateBuffer(context, CL_MEM_READ_ONLY | CL_MEM_USE_HOST_PTR, sizeInBytes, hostMemoryPointer, null);
×
NEW
163
            final Pointer clMemPointer = Pointer.to(mem);
×
NEW
164
            return new SourceArgument(byteBuffer, hostMemoryPointer, mem, clMemPointer);
×
165
        }
166
    }
167

168
    // Only available after init
169
    public OpenClTask(cl_context context, CProducer cProducer, BitHelper bitHelper, ByteBufferUtility byteBufferUtility) {
×
170
        this.context = context;
×
171
        this.cProducer = cProducer;
×
172
        this.bitHelper = bitHelper;
×
173
        this.byteBufferUtility = byteBufferUtility;
×
NEW
174
        this.maxPrivateKeyForBatchSize = KeyUtility.getMaxPrivateKeyForBatchSize(cProducer.batchSizeInBits);
×
NEW
175
        this.privateKeySourceArgument = SourceArgument.create(context, PRIVATE_KEY_SOURCE_SIZE_IN_BYTES);
×
UNCOV
176
    }
×
177

178
    public long getDstSizeInBytes() {
NEW
179
        return (long) PublicKeyBytes.CHUNK_SIZE_NUM_BYTES * cProducer.getOverallWorkSize(bitHelper);
×
180
    }
181

182
    /**
183
    * Writes the base private key to the source buffer in the format expected by the OpenCL kernel.
184
    * <p>
185
    * The method ensures that the provided private key is valid for the current batch size. If it exceeds
186
    * the allowed range, a {@link PrivateKeyTooLargeException} is thrown.
187
    * <p>
188
    * Internally, the private key is first converted to a byte array in Big-Endian format (as returned
189
    * by {@link BigInteger#toByteArray()}). Because the OpenCL kernel expects the private key as a
190
    * {@code __global const u32 *k} array in <strong>Little-Endian</strong> word order, the byte array
191
    * is then converted from Big-Endian to Little-Endian before being written to the OpenCL input buffer.
192
    * <p>
193
    * This matches the behavior of the OpenCL kernel {@code generateKeysKernel_grid}, which reads the key
194
    * using {@code copy_u32_array(k_littleEndian_local, k, ...)} assuming Little-Endian input and applies
195
    * the work-item ID to the least-significant word.
196
    *
197
    * @param privateKeyBase the base private key used as input to the OpenCL kernel
198
    * @throws PrivateKeyTooLargeException if the key is too large for the current batch size
199
    */
200
    public void setSrcPrivateKeyChunk(BigInteger privateKeyBase) {
201
        if (KeyUtility.isInvalidWithBatchSize(privateKeyBase, maxPrivateKeyForBatchSize)) {
×
202
            throw new PrivateKeyTooLargeException(privateKeyBase, maxPrivateKeyForBatchSize, cProducer.batchSizeInBits);
×
203
        }
204

205
        // BigInteger.toByteArray() always returns a big-endian (MSB-first) representation, 
206
        // meaning the most significant byte (MSB) comes first.
207
        // Therefore, the source format is always Big Endian.
NEW
208
        final byte[] byteArray = byteBufferUtility.bigIntegerToBytes(privateKeyBase);
×
209
        EndiannessConverter endiannessConverter = new EndiannessConverter(ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN, byteBufferUtility);
×
210
        endiannessConverter.convertEndian(byteArray);
×
NEW
211
        byteBufferUtility.putToByteBuffer(privateKeySourceArgument.getByteBuffer(), byteArray);
×
212
    }
×
213
    
214
    @VisibleForTesting
215
    public SourceArgument getPrivateKeySourceArgument() {
NEW
216
        return privateKeySourceArgument;
×
217
    }
218

219
    public ByteBuffer executeKernel(cl_kernel kernel, cl_command_queue commandQueue) {
220
        final long dstSizeInBytes = getDstSizeInBytes();
×
221
        // Allocate a new destination buffer so that cloning after kernel execution is unnecessary
NEW
222
        try (final DestinationArgument destinationArgument = DestinationArgument.create(context, dstSizeInBytes) ) {
×
223
            // Set the arguments for the kernel
NEW
224
            clSetKernelArg(kernel, 0, Sizeof.cl_mem, destinationArgument.getClMemPointer());
×
NEW
225
            clSetKernelArg(kernel, 1, Sizeof.cl_mem, privateKeySourceArgument.getClMemPointer());
×
226

227
            // Set the work-item dimensions
NEW
228
            final long global_work_size[] = new long[]{cProducer.getOverallWorkSize(bitHelper)};
×
NEW
229
            final long localWorkSize[] = null; // new long[]{1}; // enabling the system to choose the work-group size.
×
NEW
230
            final int workDim = 1;
×
231

232
            {
233
                // write src buffer
NEW
234
                clEnqueueWriteBuffer(commandQueue,
×
NEW
235
                        privateKeySourceArgument.getMem(),
×
236
                        CL_TRUE,
237
                        0,
238
                        PRIVATE_KEY_SOURCE_SIZE_IN_BYTES,
NEW
239
                        privateKeySourceArgument.getHostMemoryPointer(),
×
240
                        0,
241
                        null,
242
                        null
243
                );
NEW
244
                clFinish(commandQueue);
×
245
            }
246
            {
247
                // execute the kernel
NEW
248
                final long beforeExecute = System.currentTimeMillis();
×
NEW
249
                clEnqueueNDRangeKernel(
×
250
                        commandQueue,
251
                        kernel,
252
                        workDim,
253
                        null,
254
                        global_work_size,
255
                        localWorkSize,
256
                        0,
257
                        null,
258
                        null
259
                );
NEW
260
                clFinish(commandQueue);
×
261

NEW
262
                final long afterExecute = System.currentTimeMillis();
×
263

NEW
264
                if (logger.isTraceEnabled()) {
×
NEW
265
                    logger.trace("Executed OpenCL kernel in " + (afterExecute - beforeExecute) + "ms");
×
266
                }
267
            }
268
            {
269
                // read the dst buffer
NEW
270
                final long beforeRead = System.currentTimeMillis();
×
271

NEW
272
                clEnqueueReadBuffer(commandQueue,
×
NEW
273
                        destinationArgument.getMem(),
×
274
                        CL_TRUE,
275
                        0,
276
                        dstSizeInBytes,
NEW
277
                        destinationArgument.getHostMemoryPointer(),
×
278
                        0,
279
                        null,
280
                        null
281
                );
NEW
282
                clFinish(commandQueue);
×
NEW
283
                destinationArgument.close();
×
284

NEW
285
                final long afterRead = System.currentTimeMillis();
×
NEW
286
                if (logger.isTraceEnabled()) {
×
NEW
287
                    logger.trace("Read OpenCL data "+((dstSizeInBytes / 1024) / 1024) + "Mb in " + (afterRead - beforeRead) + "ms");
×
288
                }
289
            }
NEW
290
            return destinationArgument.getByteBuffer();
×
291
        }
292
    }
293

294
    @Override
295
    public boolean isClosed() {
NEW
296
        return closed;
×
297
    }
298
    
299
    @Override
300
    public void close() {
NEW
301
        if(!closed) {
×
NEW
302
            privateKeySourceArgument.close();
×
303
            // hint: destinationArgument will be released immediately
304
        }
UNCOV
305
    }
×
306

307
    /**
308
     * https://stackoverflow.com/questions/3366925/deep-copy-duplicate-of-javas-bytebuffer/4074089
309
     */
310
    private static ByteBuffer cloneByteBuffer(final ByteBuffer original) {
311
        // Create clone with same capacity as original.
312
        final ByteBuffer clone = (original.isDirect())
×
313
                ? ByteBuffer.allocateDirect(original.capacity())
×
314
                : ByteBuffer.allocate(original.capacity());
×
315

316
        // Create a read-only copy of the original.
317
        // This allows reading from the original without modifying it.
318
        final ByteBuffer readOnlyCopy = original.asReadOnlyBuffer();
×
319

320
        // Flip and read from the original.
321
        readOnlyCopy.flip();
×
322
        clone.put(readOnlyCopy);
×
323

324
        return clone;
×
325
    }
326

327
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc