Version 4 (#183)
This commit is contained in:
parent
5dffd8e9b3
commit
baed56bcdb
26
calculate_average_shipilev.sh
Executable file
26
calculate_average_shipilev.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Copyright 2023 The original authors
|
||||
#
|
||||
# Licensed 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.
|
||||
#
|
||||
|
||||
JAVA_OPTS="-XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC -Xms64m -Xmx64m -XX:+AlwaysPreTouch -XX:+UseTransparentHugePages
|
||||
-XX:-TieredCompilation -XX:CICompilerCount=2 -XX:-UseCountedLoopSafepoints -XX:+TrustFinalNonStaticFields
|
||||
--add-opens java.base/java.nio=ALL-UNNAMED --add-exports java.base/jdk.internal.ref=ALL-UNNAMED
|
||||
-XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=quiet
|
||||
-XX:CompileCommand=dontinline,dev.morling.onebrc.CalculateAverage_shipilev\$ParsingTask::seqCompute
|
||||
-XX:CompileCommand=dontinline,dev.morling.onebrc.CalculateAverage_shipilev\$MeasurementsMap::updateSlow
|
||||
-XX:CompileCommand=inline,dev.morling.onebrc.CalculateAverage_shipilev::nameMatches
|
||||
-XX:CompileThreshold=2048"
|
||||
java $JAVA_OPTS --class-path target/average-1.0.0-SNAPSHOT.jar dev.morling.onebrc.CalculateAverage_shipilev
|
649
src/main/java/dev/morling/onebrc/CalculateAverage_shipilev.java
Normal file
649
src/main/java/dev/morling/onebrc/CalculateAverage_shipilev.java
Normal file
@ -0,0 +1,649 @@
|
||||
/*
|
||||
* Copyright 2023 The original authors
|
||||
*
|
||||
* Licensed 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 dev.morling.onebrc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InaccessibleObjectException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class CalculateAverage_shipilev {
|
||||
|
||||
// This might not be the fastest implementation one can do.
|
||||
// When working on this implementation, I set the bar as follows.
|
||||
//
|
||||
// This implementation uses vanilla and standard Java as much as possible,
|
||||
// without relying on Unsafe tricks and preview features. If and when
|
||||
// those are used, they should be guarded by a feature flag. This would
|
||||
// allow running vanilla implementation if anything goes off the rails.
|
||||
//
|
||||
// This implementation also covers the realistic scenario: the I/O is
|
||||
// actually slow and jittery. To that end, making sure we can feed
|
||||
// the parsing code under slow I/O is as important as getting the
|
||||
// parsing fast. Current evaluation env keeps the input data on RAM disk,
|
||||
// which hides this important part.
|
||||
|
||||
// ========================= Tunables =========================
|
||||
|
||||
// Workload data file.
|
||||
private static final String FILE = "./measurements.txt";
|
||||
|
||||
// Max distance to search for line separator when scanning for line
|
||||
// boundaries. 100 bytes name should fit into this power-of-two buffer.
|
||||
// Should probably never change.
|
||||
private static final int MAX_LINE_LENGTH = 128;
|
||||
|
||||
// Fixed size of the measurements map. Must be the power of two. Should
|
||||
// be large enough to accomodate all the station names. Rules say there are
|
||||
// 10K station names max, so anything >> 16K works well.
|
||||
private static final int MAP_SIZE = 1 << 15;
|
||||
|
||||
// The largest mmap-ed chunk. This can be be Integer.MAX_VALUE, but
|
||||
// it is normally tuned down to seed the workers with smaller mmap regions
|
||||
// more efficiently.
|
||||
private static final int MMAP_CHUNK_SIZE = Integer.MAX_VALUE / 32;
|
||||
|
||||
// The largest slice as unit of work, processed serially by a worker.
|
||||
// Set it too low and there would be more tasks and less batching, but
|
||||
// more parallelism. Set it too high, and the reverse would be true.
|
||||
private static final int UNIT_SLICE_SIZE = 4 * 1024 * 1024;
|
||||
|
||||
// Employ direct unmapping techniques to alleviate the cost of system
|
||||
// unmmapping on process termination. This matters for very short runs
|
||||
// on highly parallel machines. This unfortunately calls into private
|
||||
// methods of buffers themselves. If not available on target JVM, the
|
||||
// feature would automatically turn off.
|
||||
private static final boolean DIRECT_UNMMAPS = true;
|
||||
|
||||
// ========================= Storage =========================
|
||||
|
||||
// Thread-local measurement maps, each thread gets one.
|
||||
// Even though crude, avoid lambdas here to alleviate startup costs.
|
||||
private static final ThreadLocal<MeasurementsMap> MAPS = ThreadLocal.withInitial(new Supplier<>() {
|
||||
@Override
|
||||
public MeasurementsMap get() {
|
||||
MeasurementsMap m = new MeasurementsMap();
|
||||
ALL_MAPS.add(m);
|
||||
return m;
|
||||
}
|
||||
});
|
||||
|
||||
// After worker threads finish, the data is available here. One just needs
|
||||
// to merge it a little.
|
||||
private static final ConcurrentLinkedQueue<MeasurementsMap> ALL_MAPS = new ConcurrentLinkedQueue<>();
|
||||
|
||||
// Releasable mmaped buffers that workers are done with. These can be un-mapped
|
||||
// in background. Part of the protocol to shutdown the background activity is to
|
||||
// issue the poison pill.
|
||||
private static final LinkedBlockingQueue<ByteBuffer> RELEASABLE_BUFFERS = new LinkedBlockingQueue<>();
|
||||
private static final ByteBuffer RELEASABLE_BUFFER_POISON_PILL = ByteBuffer.allocate(1);
|
||||
|
||||
// ========================= MEATY GRITTY PARTS: PARSE AND AGGREGATE =========================
|
||||
|
||||
// Little helper method to compare the array with given bytebuffer range.
|
||||
public static boolean nameMatches(Bucket bucket, ByteBuffer cand, int begin, int end) {
|
||||
byte[] orig = bucket.name;
|
||||
int origLen = orig.length;
|
||||
int candLen = end - begin;
|
||||
if (origLen != candLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the tails first, to simplify the matches.
|
||||
if (origLen >= 8) {
|
||||
if (bucket.tail1 != cand.getLong(end - 8)) {
|
||||
return false;
|
||||
}
|
||||
if (origLen >= 16) {
|
||||
if (bucket.tail2 != cand.getLong(end - 16)) {
|
||||
return false;
|
||||
}
|
||||
origLen -= 16;
|
||||
}
|
||||
else {
|
||||
origLen -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the rest.
|
||||
for (int i = 0; i < origLen; i++) {
|
||||
if (orig[i] != cand.get(begin + i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static final class Bucket {
|
||||
// Raw station name, its hash, and tails.
|
||||
public final byte[] name;
|
||||
public final int hash;
|
||||
public final long tail1, tail2;
|
||||
|
||||
// Temperature values, in 10x scale.
|
||||
public long sum;
|
||||
public int count;
|
||||
public int min;
|
||||
public int max;
|
||||
|
||||
public Bucket(byte[] name, long tail1, long tail2, int hash, int temp) {
|
||||
this.name = name;
|
||||
this.tail1 = tail1;
|
||||
this.tail2 = tail2;
|
||||
this.hash = hash;
|
||||
this.sum = temp;
|
||||
this.count = 1;
|
||||
this.min = temp;
|
||||
this.max = temp;
|
||||
}
|
||||
|
||||
public void merge(int value) {
|
||||
sum += value;
|
||||
count++;
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void merge(Bucket s) {
|
||||
sum += s.sum;
|
||||
count += s.count;
|
||||
min = Math.min(min, s.min);
|
||||
max = Math.max(max, s.max);
|
||||
}
|
||||
|
||||
public Row toRow() {
|
||||
return new Row(
|
||||
new String(name),
|
||||
Math.round((double) min) / 10.0,
|
||||
Math.round((double) sum / count) / 10.0,
|
||||
Math.round((double) max) / 10.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Quick and dirty linear-probing hash map. YOLO.
|
||||
public static final class MeasurementsMap {
|
||||
// Individual map buckets. Inlining these straight into map complicates
|
||||
// the implementation without the sensible performance improvement.
|
||||
// The map is likely sparse, so whatever footprint loss we have due to
|
||||
// Bucket headers we gain by allocating the buckets lazily. The memory
|
||||
// dereference costs are still high in both cases. The additional benefit
|
||||
// for explicit fields in Bucket is that we only need to pay for a single
|
||||
// null-check on bucket instead of multiple range-checks on inlined array.
|
||||
private final Bucket[] buckets = new Bucket[MAP_SIZE];
|
||||
|
||||
// Fast path is inlined in seqCompute. This is a slow-path that is taken
|
||||
// when something is off. We normally do not enter here.
|
||||
private void updateSlow(ByteBuffer name, int begin, int end, int hash, int temp) {
|
||||
int idx = hash & (MAP_SIZE - 1);
|
||||
|
||||
while (true) {
|
||||
Bucket cur = buckets[idx];
|
||||
if (cur == null) {
|
||||
// No bucket yet, lucky us. Lookup the name and create the bucket with it.
|
||||
// We are checking the names on hot-path. Therefore, it is convenient
|
||||
// to keep allocation for names near the buckets.
|
||||
int len = end - begin;
|
||||
byte[] copy = new byte[len];
|
||||
name.get(begin, copy, 0, len);
|
||||
|
||||
// Also pick up any tail to simplify future matches.
|
||||
long tail1 = (len < 8) ? 0 : name.getLong(begin + len - 8);
|
||||
long tail2 = (len < 16) ? 0 : name.getLong(begin + len - 16);
|
||||
|
||||
buckets[idx] = new Bucket(copy, tail1, tail2, hash, temp);
|
||||
return;
|
||||
}
|
||||
else if ((cur.hash == hash) && nameMatches(cur, name, begin, end)) {
|
||||
// Same as bucket fastpath. Check for collision by checking the full hash
|
||||
// first (since the index is truncated by map size), and then the exact name.
|
||||
cur.merge(temp);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
// No dice. Keep searching.
|
||||
idx = (idx + 1) & (MAP_SIZE - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same as update(), really, but for merging maps. See the comments there.
|
||||
public void merge(MeasurementsMap otherMap) {
|
||||
for (Bucket other : otherMap.buckets) {
|
||||
if (other == null)
|
||||
continue;
|
||||
int idx = other.hash & (MAP_SIZE - 1);
|
||||
while (true) {
|
||||
Bucket cur = buckets[idx];
|
||||
if (cur == null) {
|
||||
buckets[idx] = other;
|
||||
break;
|
||||
}
|
||||
else if ((cur.hash == other.hash) && Arrays.equals(cur.name, other.name)) {
|
||||
cur.merge(other);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
idx = (idx + 1) & (MAP_SIZE - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert from internal representation to the rows.
|
||||
// This does several major things: filters away null-s, instantates full Strings,
|
||||
// and computes stats.
|
||||
public int fill(Row[] rows) {
|
||||
int idx = 0;
|
||||
for (Bucket bucket : buckets) {
|
||||
if (bucket == null)
|
||||
continue;
|
||||
rows[idx++] = bucket.toRow();
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
}
|
||||
|
||||
// The heavy-weight, where most of the magic happens. This is not a usual
|
||||
// RecursiveAction, but rather a CountedCompleter in order to be more robust
|
||||
// in presence of I/O stalls and other scheduling irregularities.
|
||||
public static final class ParsingTask extends CountedCompleter<Void> {
|
||||
private final MappedByteBuffer mappedBuf;
|
||||
private final ByteBuffer buf;
|
||||
|
||||
public ParsingTask(CountedCompleter<Void> p, MappedByteBuffer mappedBuf) {
|
||||
super(p);
|
||||
this.mappedBuf = mappedBuf;
|
||||
this.buf = mappedBuf;
|
||||
}
|
||||
|
||||
public ParsingTask(CountedCompleter<Void> p, ByteBuffer buf) {
|
||||
super(p);
|
||||
this.mappedBuf = null;
|
||||
this.buf = buf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compute() {
|
||||
try {
|
||||
internalCompute();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Meh, YOLO.
|
||||
e.printStackTrace();
|
||||
throw new IllegalStateException("Internal error", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(CountedCompleter<?> caller) {
|
||||
if (DIRECT_UNMMAPS && (mappedBuf != null)) {
|
||||
RELEASABLE_BUFFERS.offer(mappedBuf);
|
||||
}
|
||||
}
|
||||
|
||||
private void internalCompute() throws Exception {
|
||||
int len = buf.limit();
|
||||
if (len > UNIT_SLICE_SIZE) {
|
||||
// Split in half.
|
||||
int mid = len / 2;
|
||||
|
||||
// Figure out the boundary that does not split the line.
|
||||
int w = mid + MAX_LINE_LENGTH;
|
||||
while (buf.get(w - 1) != '\n') {
|
||||
w--;
|
||||
}
|
||||
mid = w;
|
||||
|
||||
// Fork out! The stack depth would be shallow enough for us to
|
||||
// execute one of the computations directly.
|
||||
// FJP API: Tell there is a pending task.
|
||||
setPendingCount(1);
|
||||
new ParsingTask(this, buf.slice(0, mid)).fork();
|
||||
|
||||
// The stack depth would be shallow enough for us to
|
||||
// execute one of the computations directly.
|
||||
new ParsingTask(this, buf.slice(mid, len - mid)).compute();
|
||||
}
|
||||
else {
|
||||
// The call to seqCompute would normally be non-inlined.
|
||||
// Do setup stuff here to save inlining budget.
|
||||
MeasurementsMap map = MAPS.get();
|
||||
|
||||
// Force the order we need for bit extraction to work. This fits
|
||||
// most of the hardware very well without introducing platform
|
||||
// dependencies.
|
||||
buf.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Go!
|
||||
seqCompute(map, buf, len);
|
||||
|
||||
// FJP API: Notify that this task have completed.
|
||||
tryComplete();
|
||||
}
|
||||
}
|
||||
|
||||
private void seqCompute(MeasurementsMap map, ByteBuffer origSlice, int length) throws IOException {
|
||||
Bucket[] buckets = map.buckets;
|
||||
|
||||
// Slice up our slice! Pecular note here: this instantiates a full new buffer
|
||||
// object, which allows compiler to trust its fields more thoroughly.
|
||||
ByteBuffer slice = origSlice.slice();
|
||||
|
||||
// Do the same endianness as the original slice.
|
||||
slice.order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
// Touch the buffer once to let the common checks to fire once for this slice.
|
||||
slice.get(0);
|
||||
|
||||
int idx = 0;
|
||||
while (idx < length) {
|
||||
// Parse out the name, computing the hash on the fly.
|
||||
// Reading with ints allows us to guarantee that read would always
|
||||
// be in bounds, since the temperature+EOL is at least 4 bytes
|
||||
// long themselves. This implementation prefers simplicity over
|
||||
// advanced tricks like SWAR.
|
||||
int nameBegin = idx;
|
||||
int nameHash = 0;
|
||||
|
||||
outer: while (true) {
|
||||
int intName = slice.getInt(idx);
|
||||
for (int c = 0; c < 4; c++) {
|
||||
int b = (intName >> (c << 3)) & 0xFF;
|
||||
if (b == ';') {
|
||||
idx += c + 1;
|
||||
break outer;
|
||||
}
|
||||
nameHash ^= b * 82805;
|
||||
}
|
||||
idx += 4;
|
||||
}
|
||||
int nameEnd = idx - 1;
|
||||
|
||||
// Parse out the temperature. The rules specify temperatures
|
||||
// are within -99.9..99.9. We implicitly look ahead for
|
||||
// negative sign and carry the negative multiplier, if found.
|
||||
// After that, we just need to reconstruct the temperature from
|
||||
// two or three digits. The aggregation code expects temperatures
|
||||
// at 10x scale.
|
||||
|
||||
int intTemp = slice.getInt(idx);
|
||||
|
||||
int neg = 1;
|
||||
if ((intTemp & 0xFF) == '-') {
|
||||
// Unlucky, there is a sign. Record it, shift one byte and read
|
||||
// the remaining digit again. Surprisingly, doing a second read
|
||||
// is not worse than reading into long and trying to do bit
|
||||
// shifts on it.
|
||||
neg = -1;
|
||||
intTemp >>>= 8;
|
||||
intTemp |= slice.get(idx + 4) << 24;
|
||||
idx++;
|
||||
}
|
||||
|
||||
// Since the sign is consumed, we are only left with two cases:
|
||||
int temp = 0;
|
||||
if ((intTemp >>> 24) == '\n') {
|
||||
// EOL-digitL-point-digitH
|
||||
temp = (((intTemp & 0xFF)) - '0') * 10 +
|
||||
((intTemp >> 16) & 0xFF) - '0';
|
||||
idx += 4;
|
||||
}
|
||||
else {
|
||||
// digitL-point-digitH-digitHH
|
||||
temp = (((intTemp & 0xFF)) - '0') * 100 +
|
||||
(((intTemp >> 8) & 0xFF) - '0') * 10 +
|
||||
(((intTemp >>> 24)) - '0');
|
||||
idx += 5;
|
||||
}
|
||||
temp *= neg;
|
||||
|
||||
// Time to update!
|
||||
Bucket bucket = buckets[nameHash & (MAP_SIZE - 1)];
|
||||
if ((bucket != null) && (nameHash == bucket.hash) && nameMatches(bucket, slice, nameBegin, nameEnd)) {
|
||||
// Lucky fast path, existing bucket hit. Most of the time we complete here.
|
||||
bucket.merge(temp);
|
||||
}
|
||||
else {
|
||||
// Unlucky, slow path. The method would not be inlined, it is useful
|
||||
// to give it the original slice, so that we keep current hot slice
|
||||
// metadata provably unmodified.
|
||||
map.updateSlow(origSlice, nameBegin, nameEnd, nameHash, temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fork out the initial tasks. We would normally just fork out one large
|
||||
// task and let it split, but unfortunately buffer API does not allow us
|
||||
// "long" start-s and length-s. So we have to chunk at least by mmap-ed
|
||||
// size first. It is a CountedCompleter for the same reason ParsingTask is.
|
||||
// This also gives us a very nice opportunity to complete the work on
|
||||
// a given mmap slice, while there is still other work to do. This allows
|
||||
// us to unmap slices on the go.
|
||||
public static final class RootTask extends CountedCompleter<Void> {
|
||||
public RootTask(CountedCompleter<Void> parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void compute() {
|
||||
try {
|
||||
internalCompute();
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Meh, YOLO.
|
||||
e.printStackTrace();
|
||||
throw new IllegalStateException("Internal error", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void internalCompute() throws Exception {
|
||||
ByteBuffer buf = ByteBuffer.allocateDirect(MAX_LINE_LENGTH);
|
||||
FileChannel fc = FileChannel.open(Path.of(FILE), StandardOpenOption.READ);
|
||||
|
||||
long start = 0;
|
||||
long size = fc.size();
|
||||
while (start < size) {
|
||||
long end = Math.min(size, start + MMAP_CHUNK_SIZE);
|
||||
|
||||
// Read a little chunk into a little buffer.
|
||||
long minEnd = Math.max(0, end - MAX_LINE_LENGTH);
|
||||
buf.rewind();
|
||||
fc.read(buf, minEnd);
|
||||
|
||||
// Figure out the boundary that does not split the line.
|
||||
int w = MAX_LINE_LENGTH;
|
||||
while (buf.get(w - 1) != '\n') {
|
||||
w--;
|
||||
}
|
||||
end = minEnd + w;
|
||||
|
||||
// Fork out the large slice
|
||||
long len = end - start;
|
||||
MappedByteBuffer slice = fc.map(FileChannel.MapMode.READ_ONLY, start, len);
|
||||
start += len;
|
||||
|
||||
// FJP API: Announce we have a pending task before forking.
|
||||
addToPendingCount(1);
|
||||
|
||||
// ...and fork it
|
||||
new ParsingTask(this, slice).fork();
|
||||
}
|
||||
|
||||
// All mappings are up, can close the channel now.
|
||||
fc.close();
|
||||
|
||||
// FJP API: We have finished, try to complete the whole task tree.
|
||||
propagateCompletion();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(CountedCompleter<?> caller) {
|
||||
try {
|
||||
RELEASABLE_BUFFERS.put(RELEASABLE_BUFFER_POISON_PILL);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Invocation =========================
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
// This little line carries the whole world
|
||||
new RootTask(null).fork();
|
||||
|
||||
// While the root task is working, prepare what we need for the
|
||||
// end of the run. Go and try to report something to prepare the
|
||||
// reporting code for execution.
|
||||
MeasurementsMap map = new MeasurementsMap();
|
||||
Row[] rows = new Row[MAP_SIZE];
|
||||
StringBuilder sb = new StringBuilder(16384);
|
||||
|
||||
report(map, rows, sb);
|
||||
sb.setLength(0);
|
||||
|
||||
// Nothing else is left to do preparation-wise. Now see if we can clean up
|
||||
// buffers that tasks do not need anymore. The root task would communicate
|
||||
// that it is done by giving us a poison pill.
|
||||
ByteBuffer buf;
|
||||
while ((buf = RELEASABLE_BUFFERS.take()) != RELEASABLE_BUFFER_POISON_PILL) {
|
||||
DirectUnmaps.invokeCleaner(buf);
|
||||
}
|
||||
|
||||
// All done. Merge results from thread-local maps...
|
||||
for (MeasurementsMap m : ALL_MAPS) {
|
||||
map.merge(m);
|
||||
}
|
||||
|
||||
// ...and truly report them
|
||||
System.out.println(report(map, rows, sb));
|
||||
}
|
||||
|
||||
private static String report(MeasurementsMap map, Row[] rows, StringBuilder sb) {
|
||||
int rowCount = map.fill(rows);
|
||||
Arrays.sort(rows, 0, rowCount);
|
||||
|
||||
sb.append("{");
|
||||
boolean first = true;
|
||||
for (int c = 0; c < rowCount; c++) {
|
||||
if (c != 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
rows[c].printTo(sb);
|
||||
}
|
||||
sb.append("}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// ========================= Reporting =========================
|
||||
|
||||
private static final class Row implements Comparable<Row> {
|
||||
private final String name;
|
||||
private final double min;
|
||||
private final double max;
|
||||
private final double avg;
|
||||
|
||||
public Row(String name, double min, double avg, double max) {
|
||||
this.name = name;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.avg = avg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Row o) {
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
public void printTo(StringBuilder sb) {
|
||||
sb.append(name);
|
||||
sb.append("=");
|
||||
sb.append(min);
|
||||
sb.append("/");
|
||||
sb.append(avg);
|
||||
sb.append("/");
|
||||
sb.append(max);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================= Utils =========================
|
||||
|
||||
// Tries to figure out if calling Cleaner directly on the DirectByteBuffer
|
||||
// is possible. If this fails, we still go on.
|
||||
public static class DirectUnmaps {
|
||||
private static final Method METHOD_GET_CLEANER;
|
||||
private static final Method METHOD_CLEANER_CLEAN;
|
||||
|
||||
static Method getCleaner() {
|
||||
try {
|
||||
ByteBuffer dbb = ByteBuffer.allocateDirect(1);
|
||||
Method m = dbb.getClass().getMethod("cleaner");
|
||||
m.setAccessible(true);
|
||||
return m;
|
||||
}
|
||||
catch (NoSuchMethodException | InaccessibleObjectException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Method getCleanerClean(Method methodGetCleaner) {
|
||||
try {
|
||||
ByteBuffer dbb = ByteBuffer.allocateDirect(1);
|
||||
Object cleaner = methodGetCleaner.invoke(dbb);
|
||||
Method m = cleaner.getClass().getMethod("clean");
|
||||
m.setAccessible(true);
|
||||
m.invoke(cleaner);
|
||||
return m;
|
||||
}
|
||||
catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InaccessibleObjectException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
METHOD_GET_CLEANER = getCleaner();
|
||||
METHOD_CLEANER_CLEAN = (METHOD_GET_CLEANER != null) ? getCleanerClean(METHOD_GET_CLEANER) : null;
|
||||
}
|
||||
|
||||
public static void invokeCleaner(ByteBuffer bb) {
|
||||
if (METHOD_GET_CLEANER == null || METHOD_CLEANER_CLEAN == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
METHOD_CLEANER_CLEAN.invoke(METHOD_GET_CLEANER.invoke(bb));
|
||||
}
|
||||
catch (InvocationTargetException | IllegalAccessException e) {
|
||||
throw new IllegalStateException("Cannot happen at this point", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user