Skip to content

Commit cb221b3

Browse files
committed
tests
1 parent d69dcb7 commit cb221b3

File tree

7 files changed

+975
-689
lines changed

7 files changed

+975
-689
lines changed

.github/workflows/test.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v3
15+
16+
- name: Set up JDK 11
17+
uses: actions/setup-java@v3
18+
with:
19+
java-version: '11'
20+
distribution: 'temurin'
21+
cache: maven
22+
23+
- name: Run tests
24+
run: mvn -B test
25+
26+
- name: Build with Maven
27+
run: mvn -B package --file pom.xml

src/main/java/io/github/elimelt/pmqueue/Message.java

Lines changed: 156 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,107 +5,168 @@
55
import java.io.ObjectOutputStream;
66
import java.io.Serializable;
77
import java.lang.ref.SoftReference;
8-
import java.util.Arrays;
98
import sun.misc.Unsafe;
109
import java.lang.reflect.Field;
1110

11+
/**
12+
* A high-performance, immutable message container optimized for memory
13+
* efficiency and fast access.
14+
* This class uses direct memory operations via {@link sun.misc.Unsafe} for
15+
* improved performance
16+
* and implements custom serialization for better control over the serialization
17+
* process.
18+
*
19+
* <p>
20+
* The message contains:
21+
* <ul>
22+
* <li>A byte array containing the message data
23+
* <li>A timestamp recording when the message was created
24+
* <li>A message type identifier
25+
* </ul>
26+
*
27+
* <p>
28+
* This class implements optimizations including:
29+
* <ul>
30+
* <li>Direct memory access using Unsafe for field operations
31+
* <li>Cached hash code using soft references to allow GC if memory is tight
32+
* <li>Custom serialization implementation for performance
33+
* </ul>
34+
*
35+
* @see MessageSerializer
36+
*/
37+
@SuppressWarnings("deprecation")
1238
public class Message implements Serializable {
13-
private static final long serialVersionUID = 1L;
14-
15-
// Use SoftReference for the cached hash to allow GC if memory is tight
16-
private transient SoftReference<Integer> hashCache;
17-
18-
// Direct byte array reference for minimal overhead
19-
private final byte[] data;
20-
private final long timestamp;
21-
private final int messageType;
22-
23-
// Cache array length to avoid field access
24-
private final int length;
25-
26-
// Unsafe instance for direct memory operations
27-
private static final Unsafe unsafe;
28-
29-
// Field offsets for direct memory access
30-
private static final long dataOffset;
31-
private static final long timestampOffset;
32-
private static final long messageTypeOffset;
33-
34-
static {
35-
try {
36-
Field f = Unsafe.class.getDeclaredField("theUnsafe");
37-
f.setAccessible(true);
38-
unsafe = (Unsafe) f.get(null);
39-
40-
dataOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("data"));
41-
timestampOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("timestamp"));
42-
messageTypeOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("messageType"));
43-
} catch (Exception e) {
44-
throw new Error(e);
45-
}
46-
}
47-
48-
public Message(byte[] data, int messageType) {
49-
// Avoid double array length access
50-
int dataLength = data.length;
51-
this.data = new byte[dataLength];
52-
// Direct memory copy instead of Arrays.copyOf
53-
unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
54-
this.data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
55-
dataLength);
56-
this.length = dataLength;
57-
this.timestamp = System.currentTimeMillis();
58-
this.messageType = messageType;
59-
}
60-
61-
public byte[] getData() {
62-
byte[] copy = new byte[length];
63-
// Direct memory copy for better performance
64-
unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
65-
copy, Unsafe.ARRAY_BYTE_BASE_OFFSET,
66-
length);
67-
return copy;
68-
}
69-
70-
public long getTimestamp() {
71-
// Direct memory access instead of field access
72-
return unsafe.getLong(this, timestampOffset);
73-
}
74-
75-
public int getMessageType() {
76-
// Direct memory access instead of field access
77-
return unsafe.getInt(this, messageTypeOffset);
39+
private static final long serialVersionUID = 1L;
40+
41+
private transient SoftReference<Integer> hashCache;
42+
private final byte[] data;
43+
private final long timestamp;
44+
private final int messageType;
45+
private final int length;
46+
47+
private static final Unsafe unsafe;
48+
@SuppressWarnings("unused")
49+
private static final long dataOffset;
50+
private static final long timestampOffset;
51+
private static final long messageTypeOffset;
52+
53+
static {
54+
try {
55+
Field f = Unsafe.class.getDeclaredField("theUnsafe");
56+
f.setAccessible(true);
57+
unsafe = (Unsafe) f.get(null);
58+
59+
dataOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("data"));
60+
timestampOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("timestamp"));
61+
messageTypeOffset = unsafe.objectFieldOffset(Message.class.getDeclaredField("messageType"));
62+
} catch (Exception e) {
63+
throw new Error(e);
7864
}
79-
80-
@Override
81-
public int hashCode() {
82-
Integer cachedHash = hashCache != null ? hashCache.get() : null;
83-
if (cachedHash != null) {
84-
return cachedHash;
85-
}
86-
87-
// FNV-1a hash algorithm - faster than Arrays.hashCode
88-
int hash = 0x811c9dc5;
89-
for (byte b : data) {
90-
hash ^= b;
91-
hash *= 0x01000193;
92-
}
93-
hash = hash * 31 + (int)(timestamp ^ (timestamp >>> 32));
94-
hash = hash * 31 + messageType;
95-
96-
hashCache = new SoftReference<>(hash);
97-
return hash;
98-
}
99-
100-
private void writeObject(ObjectOutputStream out) throws IOException {
101-
// Direct field access for better performance
102-
out.writeLong(unsafe.getLong(this, timestampOffset));
103-
out.writeInt(unsafe.getInt(this, messageTypeOffset));
104-
out.writeInt(length);
105-
out.write(data, 0, length);
65+
}
66+
67+
/**
68+
* Creates a new Message with the specified data and message type.
69+
* The message's timestamp is automatically set to the current system time.
70+
* A defensive copy of the input data is made to ensure immutability.
71+
*
72+
* @param data the byte array containing the message data
73+
* @param messageType an integer identifying the type of message
74+
* @throws NullPointerException if data is null
75+
*/
76+
public Message(byte[] data, int messageType) {
77+
int dataLength = data.length;
78+
this.data = new byte[dataLength];
79+
unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
80+
this.data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
81+
dataLength);
82+
this.length = dataLength;
83+
this.timestamp = System.currentTimeMillis();
84+
this.messageType = messageType;
85+
}
86+
87+
/**
88+
* Returns a copy of the message data.
89+
* A new array is created and returned each time to preserve immutability.
90+
*
91+
* @return a copy of the message data as a byte array
92+
*/
93+
public byte[] getData() {
94+
byte[] copy = new byte[length];
95+
unsafe.copyMemory(data, Unsafe.ARRAY_BYTE_BASE_OFFSET,
96+
copy, Unsafe.ARRAY_BYTE_BASE_OFFSET,
97+
length);
98+
return copy;
99+
}
100+
101+
/**
102+
* Returns the timestamp when this message was created.
103+
*
104+
* @return the message creation timestamp as milliseconds since epoch
105+
*/
106+
public long getTimestamp() {
107+
return unsafe.getLong(this, timestampOffset);
108+
}
109+
110+
/**
111+
* Returns the message type identifier.
112+
*
113+
* @return the integer message type
114+
*/
115+
public int getMessageType() {
116+
return unsafe.getInt(this, messageTypeOffset);
117+
}
118+
119+
/**
120+
* Computes and caches the hash code for this message using the FNV-1a
121+
* algorithm.
122+
* The hash is computed based on the message data, timestamp, and message type.
123+
* The computed hash is cached using a {@link SoftReference} to allow garbage
124+
* collection
125+
* if memory is tight.
126+
*
127+
* @return the hash code for this message
128+
*/
129+
@Override
130+
public int hashCode() {
131+
Integer cachedHash = hashCache != null ? hashCache.get() : null;
132+
if (cachedHash != null) {
133+
return cachedHash;
106134
}
107135

108-
private void readObject(ObjectInputStream in) throws IOException {
109-
throw new IOException("Use MessageSerializer instead");
136+
int hash = 0x811c9dc5;
137+
for (byte b : data) {
138+
hash ^= b;
139+
hash *= 0x01000193;
110140
}
141+
hash = hash * 31 + (int) (timestamp ^ (timestamp >>> 32));
142+
hash = hash * 31 + messageType;
143+
144+
hashCache = new SoftReference<>(hash);
145+
return hash;
146+
}
147+
148+
/**
149+
* Custom serialization implementation for better performance.
150+
* Writes the message fields directly to the output stream.
151+
*
152+
* @param out the output stream to write to
153+
* @throws IOException if an I/O error occurs
154+
*/
155+
private void writeObject(ObjectOutputStream out) throws IOException {
156+
out.writeLong(unsafe.getLong(this, timestampOffset));
157+
out.writeInt(unsafe.getInt(this, messageTypeOffset));
158+
out.writeInt(length);
159+
out.write(data, 0, length);
160+
}
161+
162+
/**
163+
* Disabled default deserialization.
164+
* Use {@link MessageSerializer} instead for proper deserialization.
165+
*
166+
* @param in the input stream to read from
167+
* @throws IOException always, to prevent default deserialization
168+
*/
169+
private void readObject(ObjectInputStream in) throws IOException {
170+
throw new IOException("Use MessageSerializer instead");
171+
}
111172
}

0 commit comments

Comments
 (0)