Skip to content

Commit 082a7f7

Browse files
committed
#850 Backport Jaybird 6 blob improvements to Jaybird 5
1 parent 85f58da commit 082a7f7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+2503
-1730
lines changed

src/docs/asciidoc/release_notes.adoc

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,18 @@ This fix was backported from Jaybird 6.0.1.
4747
* Fixed: Fetch response with status=0 (FETCH_OK) and count=0 was logged on DEBUG as an unexpected response (https://github.com/FirebirdSQL/jaybird/issues/848[#848])
4848
+
4949
This fix was backported from Jaybird 6.0.1.
50+
* Improvement: backported performance improvements for blob reading and writing from Jaybird 6 (https://github.com/FirebirdSQL/jaybird/issues/850[#850])
51+
+
52+
For details see:
53+
+
54+
--
55+
* <<blob-performance-read>>
56+
* <<blob-performance-write>>
57+
* <<blob-performance-min-buf>>
58+
* <<blob-performance-max-segment>>
59+
* <<blob-buffer-size>>
60+
* <<blob-put-segment-limit>>
61+
--
5062
5163
[#jaybird-5-0-6-changelog]
5264
=== Jaybird 5.0.6
@@ -237,11 +249,12 @@ The major changes and new features in Jaybird 5 are:
237249
* <<jdbc-url-syntax>>
238250
* <<local-protocol-removed>>
239251
* <<stream-blobs-default>>
240-
* <<generated-keys-parser-replaced>> (back-ported to Jaybird 4.0.8)
252+
* <<generated-keys-parser-replaced>> (backported to Jaybird 4.0.8)
241253
* <<server-batch-updates>>
242254
* <<multirow-returning>>
243255
* <<embedded-locator-service-provider>>
244256
* <<table-statistics-manager>>
257+
* <<blob-performance>> (since Jaybird 5.0.7)
245258
246259
Upgrading from Jaybird 4 to 5 should be simple, but please make sure to read <<compatibility-changes>> before using Jaybird 5.
247260
See also <<upgrading-from-jaybird-4-to-jaybird-5>>.
@@ -774,6 +787,38 @@ Its API may change in point releases, or it may be removed or replaced entirely
774787
[#blob-performance]
775788
=== Blob performance improvements
776789

790+
[#blob-performance-read]
791+
==== Reading blobs
792+
793+
Added in: Jaybird 5.0.7, backported from Jaybird 6
794+
795+
Performance of reading blobs has been improved, especially when using `getBytes` on `ResultSet` or `Blob`, or `getString` on `ResultSet` or `Clob`, or reading from a blob input stream with `read(byte[], int, int)` and similar methods with a byte array and requested length greater than 50% of the configured `blobBufferSize`.
796+
797+
Testing on a local network (Wi-Fi) shows an increase in throughput of roughly 50-100% for reading large blobs with the default `blobBufferSize` of 16384.
798+
799+
These throughput improvements were only realised in the pure Java protocol, because there we had the opportunity to avoid all additional allocations by writing directly from the network stream into the destination byte array, and this allows us to ignore the configured `blobBufferSize` and use up to the maximum request size of 65535 bytes instead.
800+
801+
This is not possible for the JNA-based protocols (native/embedded), as the implementation requires a direct byte buffer to bridge to the native API, and thus we can't ignore the `blobBufferSize`.
802+
We were able to realise some other optimizations (in both pure Java and JNA), by avoiding allocation of a number of intermediate objects, but this has only marginal effects on the throughput.
803+
804+
[#blob-performance-write]
805+
==== Writing blobs
806+
807+
Added in: Jaybird 5.0.7, backported from Jaybird 6
808+
809+
Performance of writing blobs was improved, especially when using `setBytes` on `PreparedStatement`, `ResultSet` or `Blob`, or `setString` on `PreparedStatement`, `ResultSet` or `Clob`, or writing to a blob output stream with `write(byte[], int, int)` and similar methods with a byte array larger than the configured `blobBufferSize`.
810+
A smaller improvement was made when using arrays larger than 50% of the `blobBufferSize`.
811+
812+
Testing on a local network (Wi-Fi) shows an increase in throughput of roughly 300-400% for writing large blobs with the default `blobBufferSize` of 16384.
813+
The improvement is not available for all methods of writing blobs, for example using `ResultSet.setBinaryStream` does not see this improvement, as it relies on the `blobBufferSize` for transferring the blob content.
814+
815+
Most of these throughput improvements were only realised in the pure Java protocol, because there we had the opportunity to avoid all additional allocations by writing directly from the source byte array to the network stream, and this allows us to ignore the configured `blobBufferSize` and use up to the maximum segment size of 65535 bytes instead.
816+
817+
For the JNA-based protocols (native/embedded) a smaller throughput improvement was realised, by using the maximum segment size for the first roundtrip if the array write used offset `0`.
818+
If the length is larger than the maximum segment size, or if the offset is non-zero, we need to allocate a buffer (for subsequent segments in case offset is `0`), and thus cannot ignore the `blobBufferSize`.
819+
820+
Similar to the improvements for reading, we were also able to realise some other optimizations (in both pure Java and JNA), by avoiding allocation of a number of intermediate objects, but this has only marginal effects on the throughput.
821+
777822
[#blob-performance-defer-open]
778823
==== Deferred blob open
779824

@@ -789,6 +834,58 @@ This optimization is available for Firebird 2.1 and higher, but formally only su
789834

790835
For native connections, a similar optimization -- but only for reading blobs -- is available when using a Firebird 5.0.2 or higher fbclient, independent of the Jaybird version.
791836

837+
[#blob-performance-min-buf]
838+
==== Minimum `blobBufferSize` 512 bytes
839+
840+
Added in: Jaybird 5.0.7, backported from Jaybird 6
841+
842+
As part of the performance improvements, a minimum `blobBufferSize` of 512 bytes was introduced.
843+
Configuring values less than 512 will be ignored and use 512 instead.
844+
845+
[#blob-performance-max-segment]
846+
==== Maximum segment size raised
847+
848+
Added in: Jaybird 5.0.7, backported from Jaybird 6
849+
850+
For connections to Firebird 3.0 and higher, the maximum segment size was raised from 32765 to 65535 bytes to match the maximum segment size supported by Firebird.
851+
852+
The maximum segment size is the maximum size for sending segments (_put_) to the server.
853+
Due to protocol limitations, retrieving segments from the server (_get_) is two bytes (or multiples of two bytes) shorterfootnote:[For _get_ the maximum segment size is actually the maximum buffer size to receive one or more segments which are prefixed with two bytes for the length].
854+
855+
[#blob-buffer-size]
856+
==== Effectiveness of `blobBufferSize` larger than maximum segment size
857+
858+
Added in: Jaybird 5.0.7, backported from Jaybird 6
859+
860+
Previously, when reading blobs, a `blobBufferSize` larger than the maximum segment size was effectively ignored.
861+
Now, when reading through an input stream, a `blobBufferSize` larger than the maximum segment size can be used.
862+
863+
Jaybird will use one or more roundtrips to fill the buffer.
864+
To avoid inefficient fetches, a minimum of 90% of the buffer size will be filled up to the `blobBufferSize`.
865+
This change is not likely to improve performance, but it may allow for optimizations when reading or transferring data in large chunks.
866+
867+
In general, setting the `blobBufferSize` larger than 65535 bytes will likely not improve performance.
868+
869+
[#blob-put-segment-limit]
870+
==== Internal API changes for `FbBlob`
871+
872+
Added in: Jaybird 5.0.7, backported from Jaybird 6
873+
874+
Three new methods were added to `FbBlob`:
875+
876+
`int get(byte[] b, int off, int len)`::
877+
populates the array `b`, starting at `off`, for the requested `len` bytes from the blob, and returns the actual number of bytes read.
878+
This method will read until `len` bytes have been read, and only return less than `len` when end-of-blob was reached.
879+
880+
`int get(byte[] b, int off, int len, float minFillFactor)`::
881+
populates the array `b`, starting at `off`, for at least `minFillFactor` * `len` bytes (up to `len` bytes) from the blob, and returns the actual number of bytes read.
882+
883+
`void put(byte[] b, int off, int len)`::
884+
sends data from array `b` to the blob, starting at `off`, for the requested `len` bytes.
885+
886+
The documentation of method `FbBlob.putSegment(byte[])` contradicted itself, by requiring implementations to batch larger arrays, but also requiring them to throw an exception for larger arrays, and the actual implementations provided by Jaybird threw an exception.
887+
This contradiction has been removed, and the implementations will now send arrays longer than the maximum segment size to the server in multiple _put_ requests.
888+
792889
[#potentially-breaking-changes]
793890
=== Potentially breaking changes
794891

src/jna-client/org/firebirdsql/gds/ng/jna/JnaBlob.java

Lines changed: 104 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,27 @@
2323
import com.sun.jna.ptr.ShortByReference;
2424
import org.firebirdsql.gds.BlobParameterBuffer;
2525
import org.firebirdsql.gds.ISCConstants;
26+
import org.firebirdsql.gds.JaybirdErrorCodes;
2627
import org.firebirdsql.gds.ng.AbstractFbBlob;
2728
import org.firebirdsql.gds.ng.FbBlob;
2829
import org.firebirdsql.gds.ng.FbExceptionBuilder;
2930
import org.firebirdsql.gds.ng.LockCloseable;
3031
import org.firebirdsql.gds.ng.listeners.DatabaseListener;
3132
import org.firebirdsql.jna.fbclient.FbClientLibrary;
3233
import org.firebirdsql.jna.fbclient.ISC_STATUS;
34+
import org.firebirdsql.util.ByteArrayHelper;
3335

3436
import java.nio.ByteBuffer;
3537
import java.sql.SQLException;
3638

39+
import static org.firebirdsql.gds.ISCConstants.isc_segstr_no_op;
3740
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobGetSegmentNegative;
3841
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobPutSegmentEmpty;
39-
import static org.firebirdsql.gds.JaybirdErrorCodes.jb_blobPutSegmentTooLong;
4042

4143
/**
4244
* Implementation of {@link org.firebirdsql.gds.ng.FbBlob} for native client access.
4345
*
44-
* @author <a href="mailto:[email protected]">Mark Rotteveel</a>
46+
* @author Mark Rotteveel
4547
* @since 3.0
4648
*/
4749
public class JnaBlob extends AbstractFbBlob implements FbBlob, DatabaseListener {
@@ -121,15 +123,15 @@ public final long getBlobId() {
121123
public void open() throws SQLException {
122124
try {
123125
if (isOutput() && getBlobId() != NO_BLOB_ID) {
124-
throw new FbExceptionBuilder().nonTransientException(ISCConstants.isc_segstr_no_op).toSQLException();
126+
throw new FbExceptionBuilder().nonTransientException(isc_segstr_no_op).toSQLException();
125127
}
126128

127129
final BlobParameterBuffer blobParameterBuffer = getBlobParameterBuffer();
128130
final byte[] bpb;
129131
if (blobParameterBuffer != null) {
130132
bpb = blobParameterBuffer.toBytesWithType();
131133
} else {
132-
bpb = new byte[0];
134+
bpb = ByteArrayHelper.emptyByteArray();
133135
}
134136
try (LockCloseable ignored = withLock()) {
135137
checkDatabaseAttached();
@@ -163,35 +165,19 @@ public final boolean isOutput() {
163165

164166
@Override
165167
public byte[] getSegment(int sizeRequested) throws SQLException {
166-
try {
168+
try (LockCloseable ignored = withLock()) {
167169
if (sizeRequested <= 0) {
168-
throw new FbExceptionBuilder().exception(jb_blobGetSegmentNegative)
170+
throw FbExceptionBuilder.forException(jb_blobGetSegmentNegative)
169171
.messageParameter(sizeRequested)
170172
.toSQLException();
171173
}
172-
// TODO Honour request for larger sizes by looping?
173-
sizeRequested = Math.min(sizeRequested, getMaximumSegmentSize());
174-
final ByteBuffer responseBuffer;
175-
final ShortByReference actualLength = new ShortByReference();
176-
try (LockCloseable ignored = withLock()) {
177-
checkDatabaseAttached();
178-
checkTransactionActive();
179-
checkBlobOpen();
180-
responseBuffer = getByteBuffer(sizeRequested);
181-
182-
clientLibrary.isc_get_segment(statusVector, getJnaHandle(), actualLength, (short) sizeRequested,
183-
responseBuffer);
184-
final int status = statusVector[1].intValue();
185-
// status 0 means: more to come, isc_segment means: buffer was too small, rest will be returned on next call
186-
if (status == ISCConstants.isc_segstr_eof) {
187-
setEof();
188-
} else if (!(status == 0 || status == ISCConstants.isc_segment)) {
189-
processStatusVector();
190-
}
191-
throwAndClearDeferredException();
192-
}
193-
final int actualLengthInt = ((int) actualLength.getValue()) & 0xFFFF;
194-
final byte[] segment = new byte[actualLengthInt];
174+
checkDatabaseAttached();
175+
checkTransactionActive();
176+
checkBlobOpen();
177+
ShortByReference actualLength = new ShortByReference();
178+
ByteBuffer responseBuffer = getSegment0(sizeRequested, actualLength);
179+
throwAndClearDeferredException();
180+
byte[] segment = new byte[actualLength.getValue() & 0xFFFF];
195181
responseBuffer.get(segment);
196182
return segment;
197183
} catch (SQLException e) {
@@ -200,25 +186,91 @@ public byte[] getSegment(int sizeRequested) throws SQLException {
200186
}
201187
}
202188

189+
private ByteBuffer getSegment0(int sizeRequested, ShortByReference actualLength) throws SQLException {
190+
sizeRequested = Math.min(sizeRequested, getMaximumSegmentSize());
191+
ByteBuffer responseBuffer = getByteBuffer(sizeRequested);
192+
clientLibrary.isc_get_segment(statusVector, getJnaHandle(), actualLength, (short) sizeRequested,
193+
responseBuffer);
194+
int status = statusVector[1].intValue();
195+
// status 0 means: more to come, isc_segment means: buffer was too small, rest will be returned on next call
196+
if (status == ISCConstants.isc_segstr_eof) {
197+
setEof();
198+
} else if (!(status == 0 || status == ISCConstants.isc_segment)) {
199+
processStatusVector();
200+
}
201+
return responseBuffer;
202+
}
203+
203204
@Override
204-
public void putSegment(byte[] segment) throws SQLException {
205-
try {
206-
if (segment.length == 0) {
207-
throw new FbExceptionBuilder().exception(jb_blobPutSegmentEmpty).toSQLException();
205+
protected int get(final byte[] b, final int off, final int len, final int minLen) throws SQLException {
206+
try (LockCloseable ignored = withLock()) {
207+
validateBufferLength(b, off, len);
208+
if (len == 0) return 0;
209+
if (minLen <= 0 || minLen > len ) {
210+
throw new FbExceptionBuilder().nonTransientException(JaybirdErrorCodes.jb_invalidStringLength)
211+
.messageParameter("minLen", len, minLen)
212+
.toSQLException();
208213
}
209-
// TODO Handle by performing multiple puts? (Wrap in byte buffer, use position to move pointer?)
210-
if (segment.length > getMaximumSegmentSize()) {
211-
throw new FbExceptionBuilder().exception(jb_blobPutSegmentTooLong).toSQLException();
214+
checkDatabaseAttached();
215+
checkTransactionActive();
216+
checkBlobOpen();
217+
218+
ShortByReference actualLength = new ShortByReference();
219+
int count = 0;
220+
while (count < minLen && !isEof()) {
221+
// We honor the configured buffer size unless we somehow already allocated a bigger buffer earlier
222+
ByteBuffer segmentBuffer = getSegment0(
223+
Math.min(len - count, Math.max(getBlobBufferSize(), currentBufferCapacity())),
224+
actualLength);
225+
int dataLength = actualLength.getValue() & 0xFFFF;
226+
segmentBuffer.get(b, off + count, dataLength);
227+
count += dataLength;
212228
}
213-
try (LockCloseable ignored = withLock()) {
214-
checkDatabaseAttached();
215-
checkTransactionActive();
216-
checkBlobOpen();
229+
throwAndClearDeferredException();
230+
return count;
231+
} catch (SQLException e) {
232+
errorOccurred(e);
233+
throw e;
234+
}
235+
}
236+
237+
private int getBlobBufferSize() {
238+
return getDatabase().getConnectionProperties().getBlobBufferSize();
239+
}
240+
241+
@Override
242+
public void put(final byte[] b, final int off, final int len) throws SQLException {
243+
try (LockCloseable ignored = withLock()) {
244+
validateBufferLength(b, off, len);
245+
if (len == 0) {
246+
throw FbExceptionBuilder.forException(jb_blobPutSegmentEmpty).toSQLException();
247+
}
248+
checkDatabaseAttached();
249+
checkTransactionActive();
250+
checkBlobOpen();
217251

218-
clientLibrary.isc_put_segment(statusVector, getJnaHandle(), (short) segment.length, segment);
252+
int count = 0;
253+
if (off == 0) {
254+
// no additional buffer allocation needed, so we can send with max segment size
255+
count = Math.min(len, getMaximumSegmentSize());
256+
clientLibrary.isc_put_segment(statusVector, getJnaHandle(), (short) count, b);
219257
processStatusVector();
220-
throwAndClearDeferredException();
258+
if (count == len) {
259+
// put complete
260+
return;
261+
}
221262
}
263+
264+
byte[] segmentBuffer =
265+
new byte[Math.min(len - count, Math.min(getBlobBufferSize(), getMaximumSegmentSize()))];
266+
while (count < len) {
267+
int segmentLength = Math.min(len - count, segmentBuffer.length);
268+
System.arraycopy(b, off + count, segmentBuffer, 0, segmentLength);
269+
clientLibrary.isc_put_segment(statusVector, getJnaHandle(), (short) segmentLength, segmentBuffer);
270+
processStatusVector();
271+
count += segmentLength;
272+
}
273+
throwAndClearDeferredException();
222274
} catch (SQLException e) {
223275
errorOccurred(e);
224276
throw e;
@@ -299,11 +351,18 @@ private void processStatusVector() throws SQLException {
299351
}
300352

301353
private ByteBuffer getByteBuffer(int requiredSize) {
354+
ByteBuffer byteBuffer = this.byteBuffer;
302355
if (byteBuffer == null || byteBuffer.capacity() < requiredSize) {
303-
byteBuffer = ByteBuffer.allocateDirect(requiredSize);
304-
} else {
305-
byteBuffer.clear();
356+
// Allocate buffer in increments of 512
357+
return this.byteBuffer = ByteBuffer.allocateDirect((1 + (requiredSize - 1) / 512) * 512);
306358
}
359+
byteBuffer.clear();
307360
return byteBuffer;
308361
}
362+
363+
private int currentBufferCapacity() {
364+
ByteBuffer byteBuffer = this.byteBuffer;
365+
return byteBuffer != null ? byteBuffer.capacity() : 0;
366+
}
367+
309368
}

0 commit comments

Comments
 (0)