Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ public int getIntLE(int index) {
return (get(index + 3) & 0xFF) << 24 | (get(index + 2) & 0xFF) << 16 | (get(index + 1) & 0xFF) << 8 | get(index) & 0xFF;
}

@Override
public long getLongLE(int index) {
return (long) (get(index + 7) & 0xFF) << 56 | (long) (get(index + 6) & 0xFF) << 48 | (long) (get(index + 5) & 0xFF) << 40
| (long) (get(index + 4) & 0xFF) << 32 | (long) (get(index + 3) & 0xFF) << 24 | (get(index + 2) & 0xFF) << 16 | (get(index + 1)
& 0xFF) << 8 | get(index) & 0xFF;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if we want to do it in this PR or in a follow up, but we'll want this same bit twiddling for the long version, and might as well make this reusable for that case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I will add getLongLE method to this class and the interface and a unit test for it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pushed: 94c75f3

}

@Override
public double getDoubleLE(int index) {
return Double.longBitsToDouble(getLongLE(index));
}

@Override
public int indexOf(byte marker, int from) {
final int to = length();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,4 +136,9 @@ public void writeTo(OutputStream os) throws IOException {
public int getIntLE(int index) {
return ByteUtils.readIntLE(bytes, offset + index);
}

@Override
public double getDoubleLE(int index) {
return ByteUtils.readDoubleLE(bytes, offset + index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,16 @@ static BytesReference fromByteArray(ByteArray byteArray, int length) {
*/
int getIntLE(int index);

/**
* Returns the long read from the 8 bytes (LE) starting at the given index.
*/
long getLongLE(int index);

/**
* Returns the double read from the 8 bytes (LE) starting at the given index.
*/
double getDoubleLE(int index);

/**
* Finds the index of the first occurrence of the given marker between within the given bounds.
* @param marker marker byte to search
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,16 @@ public int getIntLE(int index) {
}
return super.getIntLE(index);
}

@Override
public double getDoubleLE(int index) {
int i = getOffsetIndex(index);
int idx = index - offsets[i];
int end = idx + 8;
BytesReference wholeDoublesLivesHere = references[i];
if (end <= wholeDoublesLivesHere.length()) {
return wholeDoublesLivesHere.getDoubleLE(idx);
}
return super.getDoubleLE(index);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ public int getIntLE(int index) {
return delegate.getIntLE(index);
}

@Override
public long getLongLE(int index) {
assert hasReferences();
return delegate.getLongLE(index);
}

@Override
public double getDoubleLE(int index) {
assert hasReferences();
return delegate.getDoubleLE(index);
}

@Override
public int indexOf(byte marker, int from) {
assert hasReferences();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,13 @@ public void set(long index, byte[] buf, int offset, int len) {
assert index >= 0 && index < size();
System.arraycopy(buf, offset << 3, array, (int) index << 3, len << 3);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
int size = (int) size();
out.writeVInt(size * 8);
out.write(array, 0, size * Double.BYTES);
}
}

private static class ByteArrayAsFloatArrayWrapper extends AbstractArrayWrapper implements FloatArray {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

import org.apache.lucene.util.ArrayUtil;
import org.apache.lucene.util.RamUsageEstimator;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;
Expand All @@ -24,6 +26,12 @@
*/
final class BigDoubleArray extends AbstractBigArray implements DoubleArray {

static {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need this here as well? Maybe we can just make a static method for this somewhere at least since we really only use it once? Or put this in a bootstrap check? Seems strange to duplicate this check doesn't it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or put this in a bootstrap check?

I like this idea. I will attempt this in a followup pr.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened this pr for this change: #91801

if (ByteOrder.nativeOrder() != ByteOrder.LITTLE_ENDIAN) {
throw new Error("The deserialization assumes this class is written with little-endian numbers.");
}
}

private static final BigDoubleArray ESTIMATOR = new BigDoubleArray(0, BigArrays.NON_RECYCLING_INSTANCE, false);

static final VarHandle VH_PLATFORM_NATIVE_DOUBLE = MethodHandles.byteArrayViewVarHandle(double[].class, ByteOrder.nativeOrder());
Expand Down Expand Up @@ -123,4 +131,21 @@ public static long estimateRamBytes(final long size) {
public void set(long index, byte[] buf, int offset, int len) {
set(index, buf, offset, len, pages, 3);
}

@Override
public void writeTo(StreamOutput out) throws IOException {
int size = (int) this.size;
out.writeVInt(size * Double.BYTES);
int lastPageEnd = size % DOUBLE_PAGE_SIZE;
if (lastPageEnd == 0) {
for (byte[] page : pages) {
out.write(page);
}
return;
}
for (int i = 0; i < pages.length - 1; i++) {
out.write(pages[i]);
}
out.write(pages[pages.length - 1], 0, lastPageEnd * Double.BYTES);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should share this code with the int/long/whatever other types. It's nearly the same code. 🤷 sounds like a good follow up change.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,19 @@

package org.elasticsearch.common.util;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.Writeable;

import java.io.IOException;

/**
* Abstraction of an array of double values.
*/
public interface DoubleArray extends BigArray {
public interface DoubleArray extends BigArray, Writeable {

static DoubleArray readFrom(StreamInput in) throws IOException {
return new ReleasableDoubleArray(in);
}

/**
* Get an element given its index.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.common.util;

import org.apache.lucene.util.RamUsageEstimator;
import org.elasticsearch.common.bytes.ReleasableBytesReference;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;

class ReleasableDoubleArray implements DoubleArray {
private static final long SHALLOW_SIZE = RamUsageEstimator.shallowSizeOfInstance(ReleasableDoubleArray.class);

private final ReleasableBytesReference ref;

ReleasableDoubleArray(StreamInput in) throws IOException {
ref = in.readReleasableBytesReference();
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeBytesReference(ref);
}

@Override
public long size() {
return ref.length() / Long.BYTES;
}

@Override
public double get(long index) {
if (index > Integer.MAX_VALUE / Long.BYTES) {
// We can't serialize messages longer than 2gb anyway
throw new ArrayIndexOutOfBoundsException();
}
return ref.getDoubleLE((int) index * Long.BYTES);
}

@Override
public double set(long index, double value) {
throw new UnsupportedOperationException();
}

@Override
public double increment(long index, double inc) {
throw new UnsupportedOperationException();
}

@Override
public void fill(long fromIndex, long toIndex, double value) {
throw new UnsupportedOperationException();
}

@Override
public void set(long index, byte[] buf, int offset, int len) {
throw new UnsupportedOperationException();
}

@Override
public long ramBytesUsed() {
/*
* If we return the size of the buffer that we've sliced
* we're likely to double count things.
*/
return SHALLOW_SIZE;
}

@Override
public void close() {
ref.decRef();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is obvious, but I'm not used to looking at this part of the code - where's the corresponding incRef for this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll love this. It's in the ctor - in readReleasableBytesReference. The way this links into the rest of the world is that ReleasableBytesReference#streamInput returns a specialized input that incs the ref when you call readReleasableBytesReference.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,34 @@ public void testGetIntLE() {
* the number of bytes in an int. Get it? I'm not sure I do either....
*/
}

public void testGetLongLE() {
// first 8 bytes = 888, second 8 bytes = Long.MAX_VALUE
// tag::noformat
byte[] array = new byte[] {
0x78, 0x3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
-0x1, -0x1, -0x1, -0x1, -0x1, -0x1, -0x1, 0x7F
};
// end::noformat
BytesReference ref = new BytesArray(array, 0, array.length);
assertThat(ref.getLongLE(0), equalTo(888L));
assertThat(ref.getLongLE(8), equalTo(Long.MAX_VALUE));
Exception e = expectThrows(ArrayIndexOutOfBoundsException.class, () -> ref.getLongLE(9));
assertThat(e.getMessage(), equalTo("Index 16 out of bounds for length 16"));
}

public void testGetDoubleLE() {
// first 8 bytes = 1.2, second 8 bytes = 1.4
// tag::noformat
byte[] array = new byte[] {
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, -0xD, 0x3F,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, -0xA, 0x3F
};
// end::noformat
BytesReference ref = new BytesArray(array, 0, array.length);
assertThat(ref.getDoubleLE(0), equalTo(1.2));
assertThat(ref.getDoubleLE(8), equalTo(1.4));
Exception e = expectThrows(ArrayIndexOutOfBoundsException.class, () -> ref.getDoubleLE(9));
assertThat(e.getMessage(), equalTo("Index 9 out of bounds for length 9"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,34 @@ public void testGetIntLE() {
assertThat(comp.getIntLE(2), equalTo(0x02010012));
assertThat(comp.getIntLE(3), equalTo(0x03020100));
assertThat(comp.getIntLE(4), equalTo(0x04030201));
Exception e = expectThrows(ArrayIndexOutOfBoundsException.class, () -> comp.getIntLE(5));
assertThat(e.getMessage(), equalTo("Index 4 out of bounds for length 4"));
// The jvm can optimize throwing ArrayIndexOutOfBoundsException by reusing the same exception,
// but these reused exceptions have no message or stack trace. This sometimes happens when running this test case.
// We can assert the exception message if -XX:-OmitStackTraceInFastThrow is set in gradle test task.
expectThrows(ArrayIndexOutOfBoundsException.class, () -> comp.getIntLE(5));
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional the testGetDoubleLE() test sometimes causes the jvm to reuse the same AIOOB exception. These reused exceptions have no message and no stacktrace.

This reproduces when running:

./gradlew ':server:test' --tests "org.elasticsearch.common.bytes.CompositeBytesReferenceTests" -Dtests.iters=8

And also failed in PR CI.

Running with OmitStackTraceInFastThrow disabled (is enabled by default) stops the exception reuse and test case doesn't fail without this modification:

./gradlew ':server:test' --tests "org.elasticsearch.common.bytes.CompositeBytesReferenceTests" -Dtests.iters=8 -Dtests.jvm.argline="-XX:-OmitStackTraceInFastThrow"

We either need to ensure -XX:-OmitStackTraceInFastThrow is set when running gradle test task or adjust the test, which I have done now. I don't think asserting the exception message is that important?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is, nah. For what it's worth, painless does set this because it really does want to assert messages. And we set it in production because it can make debugging some issues impossible.

}

public void testGetDoubleLE() {
// first double = 1.2, second double = 1.4, third double = 1.6
// tag::noformat
byte[] data = new byte[] {
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, -0xD, 0x3F,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, -0xA, 0x3F,
-0x66, -0x67, -0x67, -0x67, -0x67, -0x67, -0x7, 0x3F};
// end::noformat

List<BytesReference> refs = new ArrayList<>();
int bytesPerChunk = randomFrom(4, 16);
for (int offset = 0; offset < data.length; offset += bytesPerChunk) {
int length = Math.min(bytesPerChunk, data.length - offset);
refs.add(new BytesArray(data, offset, length));
}
BytesReference comp = CompositeBytesReference.of(refs.toArray(BytesReference[]::new));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

assertThat(comp.getDoubleLE(0), equalTo(1.2));
assertThat(comp.getDoubleLE(8), equalTo(1.4));
assertThat(comp.getDoubleLE(16), equalTo(1.6));
// The jvm can optimize throwing ArrayIndexOutOfBoundsException by reusing the same exception,
// but these reused exceptions have no message or stack trace. This sometimes happens when running this test case.
// We can assert the exception message if -XX:-OmitStackTraceInFastThrow is set in gradle test task.
expectThrows(IndexOutOfBoundsException.class, () -> comp.getDoubleLE(17));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.common.util.DoubleArray;
import org.elasticsearch.common.util.IntArray;
import org.junit.After;

Expand Down Expand Up @@ -80,4 +81,23 @@ public void testBigIntArrayLivesAfterReleasableIsDecremented() throws IOExceptio
assertThat(ref.hasReferences(), equalTo(false));
}

public void testBigDoubleArrayLivesAfterReleasableIsDecremented() throws IOException {
DoubleArray testData = BigArrays.NON_RECYCLING_INSTANCE.newDoubleArray(1, false);
testData.set(0, 1);

BytesStreamOutput out = new BytesStreamOutput();
testData.writeTo(out);

ReleasableBytesReference ref = ReleasableBytesReference.wrap(out.bytes());

try (DoubleArray in = DoubleArray.readFrom(ref.streamInput())) {
ref.decRef();
assertThat(ref.hasReferences(), equalTo(true));

assertThat(in.size(), equalTo(testData.size()));
assertThat(in.get(0), equalTo(1.0));
}
assertThat(ref.hasReferences(), equalTo(false));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.common.util.DoubleArray;
import org.elasticsearch.common.util.IntArray;
import org.elasticsearch.common.util.PageCacheRecycler;
import org.elasticsearch.common.util.set.Sets;
Expand Down Expand Up @@ -245,6 +246,31 @@ private void assertBigIntArray(int size) throws IOException {
}
}

public void testSmallBigDoubleArray() throws IOException {
assertBigDoubleArray(between(0, PageCacheRecycler.DOUBLE_PAGE_SIZE));
}

public void testLargeBigDoubleArray() throws IOException {
assertBigDoubleArray(between(PageCacheRecycler.DOUBLE_PAGE_SIZE, 10000));
}

private void assertBigDoubleArray(int size) throws IOException {
DoubleArray testData = BigArrays.NON_RECYCLING_INSTANCE.newDoubleArray(size, false);
for (int i = 0; i < size; i++) {
testData.set(i, randomDouble());
}

BytesStreamOutput out = new BytesStreamOutput();
testData.writeTo(out);

try (DoubleArray in = DoubleArray.readFrom(getStreamInput(out.bytes()))) {
assertThat(in.size(), equalTo(testData.size()));
for (int i = 0; i < size; i++) {
assertThat(in.get(i), equalTo(testData.get(i)));
}
}
}

public void testCollection() throws IOException {
class FooBar implements Writeable {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,11 @@ public void set(long index, byte[] buf, int offset, int len) {
public Collection<Accountable> getChildResources() {
return Collections.singleton(Accountables.namedAccountable("delegate", in));
}

@Override
public void writeTo(StreamOutput out) throws IOException {
in.writeTo(out);
}
}

private class ObjectArrayWrapper<T> extends AbstractArrayWrapper implements ObjectArray<T> {
Expand Down