blob: 7f7bbd11ad59b4acbf1896e4d71c46c6e104b9b7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.ignite.internal.binary;
import org.apache.ignite.internal.util.typedef.internal.S;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
/**
* Schema describing binary object content. We rely on the following assumptions:
* - When amount of fields in the object is low, it is better to inline these values into int fields thus allowing
* for quick comparisons performed within already fetched L1 cache line.
* - When there are more fields, we store them inside a hash map.
*/
public class BinarySchema implements Externalizable {
/** */
private static final long serialVersionUID = 0L;
/** Order returned if field is not found. */
public static final int ORDER_NOT_FOUND = -1;
/** Minimum sensible size. */
private static final int MAP_MIN_SIZE = 32;
/** Empty cell. */
private static final int MAP_EMPTY = 0;
/** Schema ID. */
private int schemaId;
/** IDs depending on order. */
private int[] ids;
/** Interned names of associated fields. */
private String[] names;
/** ID-to-order data. */
private int[] idToOrderData;
/** ID-to-order mask. */
private int idToOrderMask;
/** ID 1. */
private int id0;
/** ID 2. */
private int id1;
/** ID 3. */
private int id2;
/** ID 4. */
private int id3;
/**
* {@link Externalizable} support.
*/
public BinarySchema() {
// No-op.
}
/**
* Constructor.
*
* @param schemaId Schema ID.
* @param fieldIds Field IDs.
*/
public BinarySchema(int schemaId, List<Integer> fieldIds) {
assert fieldIds != null;
this.schemaId = schemaId;
initialize(fieldIds);
}
/**
* @return Schema ID.
*/
public int schemaId() {
return schemaId;
}
/**
* Try speculatively confirming order for the given field name.
*
* @param expOrder Expected order.
* @param expName Expected name.
* @return Field ID.
*/
@SuppressWarnings("StringEquality")
public Confirmation confirmOrder(int expOrder, String expName) {
assert expName != null;
if (expOrder < names.length) {
String name = names[expOrder];
// Note that we use only reference equality assuming that field names are interned literals.
if (name == expName)
return Confirmation.CONFIRMED;
if (name == null)
return Confirmation.CLARIFY;
}
return Confirmation.REJECTED;
}
/**
* Add field name.
*
* @param order Order.
* @param name Name.
*/
public void clarifyFieldName(int order, String name) {
assert name != null;
assert order < names.length;
names[order] = name.intern();
}
/**
* Get field ID by order in footer.
*
* @param order Order.
* @return Field ID.
*/
public int fieldId(int order) {
return order < ids.length ? ids[order] : 0;
}
/**
* Get field order in footer by field ID.
*
* @param id Field ID.
* @return Offset or {@code 0} if there is no such field.
*/
public int order(int id) {
if (idToOrderData == null) {
if (id == id0)
return 0;
if (id == id1)
return 1;
if (id == id2)
return 2;
if (id == id3)
return 3;
return ORDER_NOT_FOUND;
}
else {
int idx = (id & idToOrderMask) << 1;
int curId = idToOrderData[idx];
if (id == curId) // Hit!
return idToOrderData[idx + 1];
else if (curId == MAP_EMPTY) // No such ID!
return ORDER_NOT_FOUND;
else {
// Unlikely collision scenario.
for (int i = 2; i < idToOrderData.length; i += 2) {
int newIdx = (idx + i) % idToOrderData.length;
assert newIdx < idToOrderData.length - 1;
curId = idToOrderData[newIdx];
if (id == curId)
return idToOrderData[newIdx + 1];
else if (curId == MAP_EMPTY)
return ORDER_NOT_FOUND;
}
return ORDER_NOT_FOUND;
}
}
}
/** {@inheritDoc} */
@Override public int hashCode() {
return schemaId;
}
/** {@inheritDoc} */
@Override public boolean equals(Object o) {
return o != null && o instanceof BinarySchema && schemaId == ((BinarySchema)o).schemaId;
}
/** {@inheritDoc} */
@Override public String toString() {
return S.toString(BinarySchema.class, this,
"ids", Arrays.toString(ids),
"names", Arrays.toString(names),
"idToOrderData", Arrays.toString(idToOrderData));
}
/** {@inheritDoc} */
@Override public void writeExternal(ObjectOutput out) throws IOException {
writeTo(out);
}
/**
* The object implements the writeTo method to save its contents
* by calling the methods of DataOutput for its primitive values and strings or
* calling the writeTo method for other objects.
*
* @param out the stream to write the object to.
* @throws IOException Includes any I/O exceptions that may occur.
*/
public void writeTo(DataOutput out) throws IOException {
out.writeInt(schemaId);
out.writeInt(ids.length);
for (Integer id : ids)
out.writeInt(id);
}
/** {@inheritDoc} */
@Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
readFrom(in);
}
/**
* The object implements the readFrom method to restore its
* contents by calling the methods of DataInput for primitive
* types and strings or calling readFrom for other objects. The
* readFrom method must read the values in the same sequence
* and with the same types as were written by writeTo.
*
* @param in the stream to read data from in order to restore the object
* @throws IOException if I/O errors occur
*/
public void readFrom(DataInput in) throws IOException {
schemaId = in.readInt();
int idsCnt = in.readInt();
List<Integer> fieldIds = new ArrayList<>(idsCnt);
for (int i = 0; i < idsCnt; i++)
fieldIds.add(in.readInt());
initialize(fieldIds);
}
/**
* Gets field ids array.
*
* @return Field ids.
*/
public int[] fieldIds() {
return ids;
}
/**
* Parse values.
*
* @param vals Values.
* @param size Proposed result size.
* @return Parse result.
*/
private static ParseResult parse(int[] vals, int size) {
int mask = maskForPowerOfTwo(size);
int totalSize = size * 2;
int[] data = new int[totalSize];
int collisions = 0;
for (int order = 0; order < vals.length; order++) {
int id = vals[order];
assert id != 0;
int idIdx = (id & mask) << 1;
if (data[idIdx] == 0) {
// Found empty slot.
data[idIdx] = id;
data[idIdx + 1] = order;
}
else {
// Collision!
collisions++;
boolean placeFound = false;
for (int i = 2; i < totalSize; i += 2) {
int newIdIdx = (idIdx + i) % totalSize;
if (data[newIdIdx] == 0) {
data[newIdIdx] = id;
data[newIdIdx + 1] = order;
placeFound = true;
break;
}
}
assert placeFound : "Should always have a place for entry!";
}
}
return new ParseResult(data, collisions);
}
/**
* Get next power of two which greater or equal to the given number.
* This implementation is not meant to be very efficient, so it is expected to be used relatively rare.
*
* @param val Number
* @return Nearest pow2.
*/
private static int nextPowerOfTwo(int val) {
int res = 1;
while (res < val)
res = res << 1;
if (res < 0)
throw new IllegalArgumentException("Value is too big to find positive pow2: " + val);
return res;
}
/**
* Calculate mask for the given value which is a power of two.
*
* @param val Value.
* @return Mask.
*/
private static int maskForPowerOfTwo(int val) {
int mask = 0;
int comparand = 1;
while (comparand < val) {
mask |= comparand;
comparand <<= 1;
}
return mask;
}
/**
* Initialization routine.
*
* @param fieldIds Field IDs.
*/
private void initialize(List<Integer> fieldIds) {
ids = new int[fieldIds.size()];
for (int i = 0; i < fieldIds.size(); i++)
ids[i] = fieldIds.get(i);
names = new String[fieldIds.size()];
if (fieldIds.size() <= 4) {
Iterator<Integer> iter = fieldIds.iterator();
id0 = iter.hasNext() ? iter.next() : 0;
id1 = iter.hasNext() ? iter.next() : 0;
id2 = iter.hasNext() ? iter.next() : 0;
id3 = iter.hasNext() ? iter.next() : 0;
}
else {
id0 = id1 = id2 = id3 = 0;
initializeMap(ids);
}
}
/**
* Initialize the map.
*
* @param vals Values.
*/
private void initializeMap(int[] vals) {
int size = Math.max(nextPowerOfTwo(vals.length) << 2, MAP_MIN_SIZE);
assert size > 0;
ParseResult finalRes;
ParseResult res1 = parse(vals, size);
if (res1.collisions == 0)
finalRes = res1;
else {
ParseResult res2 = parse(vals, size * 2);
// Failed to decrease aom
if (res2.collisions == 0)
finalRes = res2;
else
finalRes = parse(vals, size * 4);
}
idToOrderData = finalRes.data;
idToOrderMask = maskForPowerOfTwo(idToOrderData.length / 2);
}
/**
* Schema builder.
*/
public static class Builder {
/** Schema ID. */
private int schemaId = BinaryUtils.schemaInitialId();
/** Fields. */
private final ArrayList<Integer> fields = new ArrayList<>();
/**
* Create new schema builder.
*
* @return Schema builder.
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Private constructor.
*/
private Builder() {
// No-op.
}
/**
* Add field.
*
* @param fieldId Field ID.
*/
public void addField(int fieldId) {
fields.add(fieldId);
schemaId = BinaryUtils.updateSchemaId(schemaId, fieldId);
}
/**
* Build schema.
*
* @return Schema.
*/
public BinarySchema build() {
return new BinarySchema(schemaId, fields);
}
}
/**
* Order confirmation result.
*/
public enum Confirmation {
/** Confirmed. */
CONFIRMED,
/** Denied. */
REJECTED,
/** Field name clarification is needed. */
CLARIFY
}
/**
* Result of map parsing.
*/
private static class ParseResult {
/** Data. */
private int[] data;
/** Collisions. */
private int collisions;
/**
* Constructor.
*
* @param data Data.
* @param collisions Collisions.
*/
private ParseResult(int[] data, int collisions) {
this.data = data;
this.collisions = collisions;
}
}
}