From 2fb849502f57ec29e9913907872183af06a57b3e Mon Sep 17 00:00:00 2001 From: Joshua Hartman <jhartman@linkedin.com> Date: Sun, 5 Dec 2010 21:20:15 -0800 Subject: [PATCH] Replacing the native lzf compression code with the ning open-source compress-lzf library. (Apache 2.0 liscense) --- Makefile | 18 +- run | 1 + src/java/spark/compress/lzf/LZF.java | 27 --- .../spark/compress/lzf/LZFInputStream.java | 180 ------------------ .../spark/compress/lzf/LZFOutputStream.java | 85 --------- src/native/Makefile | 30 --- src/native/spark_compress_lzf_LZF.c | 90 --------- src/scala/spark/Broadcast.scala | 2 +- .../compress-lzf-0.6.0/compress-lzf-0.6.0.jar | Bin 0 -> 14497 bytes 9 files changed, 8 insertions(+), 425 deletions(-) delete mode 100644 src/java/spark/compress/lzf/LZF.java delete mode 100644 src/java/spark/compress/lzf/LZFInputStream.java delete mode 100644 src/java/spark/compress/lzf/LZFOutputStream.java delete mode 100644 src/native/Makefile delete mode 100644 src/native/spark_compress_lzf_LZF.c create mode 100644 third_party/compress-lzf-0.6.0/compress-lzf-0.6.0.jar diff --git a/Makefile b/Makefile index 15ab516d1f..762d752fda 100644 --- a/Makefile +++ b/Makefile @@ -15,12 +15,13 @@ JARS += third_party/jetty-7.1.6.v20100715/servlet-api-2.5.jar JARS += third_party/apache-log4j-1.2.16/log4j-1.2.16.jar JARS += third_party/slf4j-1.6.1/slf4j-api-1.6.1.jar JARS += third_party/slf4j-1.6.1/slf4j-log4j12-1.6.1.jar +JARS += third_party/compress-lzf-0.6.0/compress-lzf-0.6.0.jar + CLASSPATH = $(subst $(SPACE),:,$(JARS)) SCALA_SOURCES = src/examples/*.scala src/scala/spark/*.scala src/scala/spark/repl/*.scala SCALA_SOURCES += src/test/spark/*.scala src/test/spark/repl/*.scala -JAVA_SOURCES = $(wildcard src/java/spark/compress/lzf/*.java) ifeq ($(USE_FSC),1) COMPILER_NAME = fsc @@ -36,25 +37,19 @@ endif CONF_FILES = conf/spark-env.sh conf/log4j.properties conf/java-opts -all: scala java conf-files +all: scala conf-files build/classes: mkdir -p build/classes -scala: build/classes java +scala: build/classes $(COMPILER) -d build/classes -classpath build/classes:$(CLASSPATH) $(SCALA_SOURCES) -java: $(JAVA_SOURCES) build/classes - javac -d build/classes $(JAVA_SOURCES) - -native: java - $(MAKE) -C src/native - jar: build/spark.jar build/spark-dep.jar dep-jar: build/spark-dep.jar -build/spark.jar: scala java +build/spark.jar: scala jar cf build/spark.jar -C build/classes spark build/spark-dep.jar: @@ -73,7 +68,6 @@ test: all default: all clean: - $(MAKE) -C src/native clean rm -rf build -.phony: default all clean scala java native jar dep-jar conf-files +.phony: default all clean scala jar dep-jar conf-files diff --git a/run b/run index d6f7d920c5..5c8943c91b 100755 --- a/run +++ b/run @@ -48,6 +48,7 @@ CLASSPATH+=:$FWDIR/third_party/jetty-7.1.6.v20100715/servlet-api-2.5.jar CLASSPATH+=:$FWDIR/third_party/apache-log4j-1.2.16/log4j-1.2.16.jar CLASSPATH+=:$FWDIR/third_party/slf4j-1.6.1/slf4j-api-1.6.1.jar CLASSPATH+=:$FWDIR/third_party/slf4j-1.6.1/slf4j-log4j12-1.6.1.jar +CLASSPATH+=:$FWDIR/third_party/compress-lzf-0.6.0/compress-lzf-0.6.0.jar for jar in $FWDIR/third_party/hadoop-0.20.0/lib/*.jar; do CLASSPATH+=:$jar done diff --git a/src/java/spark/compress/lzf/LZF.java b/src/java/spark/compress/lzf/LZF.java deleted file mode 100644 index 294a0494ec..0000000000 --- a/src/java/spark/compress/lzf/LZF.java +++ /dev/null @@ -1,27 +0,0 @@ -package spark.compress.lzf; - -public class LZF { - private static boolean loaded; - - static { - try { - System.loadLibrary("spark_native"); - loaded = true; - } catch(Throwable t) { - System.out.println("Failed to load native LZF library: " + t.toString()); - loaded = false; - } - } - - public static boolean isLoaded() { - return loaded; - } - - public static native int compress( - byte[] in, int inOff, int inLen, - byte[] out, int outOff, int outLen); - - public static native int decompress( - byte[] in, int inOff, int inLen, - byte[] out, int outOff, int outLen); -} diff --git a/src/java/spark/compress/lzf/LZFInputStream.java b/src/java/spark/compress/lzf/LZFInputStream.java deleted file mode 100644 index 16bc687489..0000000000 --- a/src/java/spark/compress/lzf/LZFInputStream.java +++ /dev/null @@ -1,180 +0,0 @@ -package spark.compress.lzf; - -import java.io.EOFException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; - -public class LZFInputStream extends FilterInputStream { - private static final int MAX_BLOCKSIZE = 1024 * 64 - 1; - private static final int MAX_HDR_SIZE = 7; - - private byte[] inBuf; // Holds data to decompress (including header) - private byte[] outBuf; // Holds decompressed data to output - private int outPos; // Current position in outBuf - private int outSize; // Total amount of data in outBuf - - private boolean closed; - private boolean reachedEof; - - private byte[] singleByte = new byte[1]; - - public LZFInputStream(InputStream in) { - super(in); - if (in == null) - throw new NullPointerException(); - inBuf = new byte[MAX_BLOCKSIZE + MAX_HDR_SIZE]; - outBuf = new byte[MAX_BLOCKSIZE + MAX_HDR_SIZE]; - outPos = 0; - outSize = 0; - } - - private void ensureOpen() throws IOException { - if (closed) throw new IOException("Stream closed"); - } - - @Override - public int read() throws IOException { - ensureOpen(); - int count = read(singleByte, 0, 1); - return (count == -1 ? -1 : singleByte[0] & 0xFF); - } - - @Override - public int read(byte[] b, int off, int len) throws IOException { - ensureOpen(); - if ((off | len | (off + len) | (b.length - (off + len))) < 0) - throw new IndexOutOfBoundsException(); - - int totalRead = 0; - - // Start with the current block in outBuf, and read and decompress any - // further blocks necessary. Instead of trying to decompress directly to b - // when b is large, we always use outBuf as an intermediate holding space - // in case GetPrimitiveArrayCritical decides to copy arrays instead of - // pinning them, which would cause b to be copied repeatedly into C-land. - while (len > 0) { - if (outPos == outSize) { - readNextBlock(); - if (reachedEof) - return totalRead == 0 ? -1 : totalRead; - } - int amtToCopy = Math.min(outSize - outPos, len); - System.arraycopy(outBuf, outPos, b, off, amtToCopy); - off += amtToCopy; - len -= amtToCopy; - outPos += amtToCopy; - totalRead += amtToCopy; - } - - return totalRead; - } - - // Read len bytes from this.in to a buffer, stopping only if EOF is reached - private int readFully(byte[] b, int off, int len) throws IOException { - int totalRead = 0; - while (len > 0) { - int amt = in.read(b, off, len); - if (amt == -1) - break; - off += amt; - len -= amt; - totalRead += amt; - } - return totalRead; - } - - // Read the next block from the underlying InputStream into outBuf, - // setting outPos and outSize, or set reachedEof if the stream ends. - private void readNextBlock() throws IOException { - // Read first 5 bytes of header - int count = readFully(inBuf, 0, 5); - if (count == 0) { - reachedEof = true; - return; - } else if (count < 5) { - throw new EOFException("Truncated LZF block header"); - } - - // Check magic bytes - if (inBuf[0] != 'Z' || inBuf[1] != 'V') - throw new IOException("Wrong magic bytes in LZF block header"); - - // Read the block - if (inBuf[2] == 0) { - // Uncompressed block - read directly to outBuf - int size = ((inBuf[3] & 0xFF) << 8) | (inBuf[4] & 0xFF); - if (readFully(outBuf, 0, size) != size) - throw new EOFException("EOF inside LZF block"); - outPos = 0; - outSize = size; - } else if (inBuf[2] == 1) { - // Compressed block - read to inBuf and decompress - if (readFully(inBuf, 5, 2) != 2) - throw new EOFException("Truncated LZF block header"); - int csize = ((inBuf[3] & 0xFF) << 8) | (inBuf[4] & 0xFF); - int usize = ((inBuf[5] & 0xFF) << 8) | (inBuf[6] & 0xFF); - if (readFully(inBuf, 7, csize) != csize) - throw new EOFException("Truncated LZF block"); - if (LZF.decompress(inBuf, 7, csize, outBuf, 0, usize) != usize) - throw new IOException("Corrupt LZF data stream"); - outPos = 0; - outSize = usize; - } else { - throw new IOException("Unknown block type in LZF block header"); - } - } - - /** - * Returns 0 after EOF has been reached, otherwise always return 1. - * - * Programs should not count on this method to return the actual number - * of bytes that could be read without blocking. - */ - @Override - public int available() throws IOException { - ensureOpen(); - return reachedEof ? 0 : 1; - } - - // TODO: Skip complete chunks without decompressing them? - @Override - public long skip(long n) throws IOException { - ensureOpen(); - if (n < 0) - throw new IllegalArgumentException("negative skip length"); - byte[] buf = new byte[512]; - long skipped = 0; - while (skipped < n) { - int len = (int) Math.min(n - skipped, buf.length); - len = read(buf, 0, len); - if (len == -1) { - reachedEof = true; - break; - } - skipped += len; - } - return skipped; - } - - @Override - public void close() throws IOException { - if (!closed) { - in.close(); - closed = true; - } - } - - @Override - public boolean markSupported() { - return false; - } - - @Override - public void mark(int readLimit) {} - - @Override - public void reset() throws IOException { - throw new IOException("mark/reset not supported"); - } -} diff --git a/src/java/spark/compress/lzf/LZFOutputStream.java b/src/java/spark/compress/lzf/LZFOutputStream.java deleted file mode 100644 index 5f65e95d2a..0000000000 --- a/src/java/spark/compress/lzf/LZFOutputStream.java +++ /dev/null @@ -1,85 +0,0 @@ -package spark.compress.lzf; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -public class LZFOutputStream extends FilterOutputStream { - private static final int BLOCKSIZE = 1024 * 64 - 1; - private static final int MAX_HDR_SIZE = 7; - - private byte[] inBuf; // Holds input data to be compressed - private byte[] outBuf; // Holds compressed data to be written - private int inPos; // Current position in inBuf - - public LZFOutputStream(OutputStream out) { - super(out); - inBuf = new byte[BLOCKSIZE + MAX_HDR_SIZE]; - outBuf = new byte[BLOCKSIZE + MAX_HDR_SIZE]; - inPos = MAX_HDR_SIZE; - } - - @Override - public void write(int b) throws IOException { - inBuf[inPos++] = (byte) b; - if (inPos == inBuf.length) - compressAndSendBlock(); - } - - @Override - public void write(byte[] b, int off, int len) throws IOException { - if ((off | len | (off + len) | (b.length - (off + len))) < 0) - throw new IndexOutOfBoundsException(); - - // If we're given a large array, copy it piece by piece into inBuf and - // write one BLOCKSIZE at a time. This is done to prevent the JNI code - // from copying the whole array repeatedly if GetPrimitiveArrayCritical - // decides to copy instead of pinning. - while (inPos + len >= inBuf.length) { - int amtToCopy = inBuf.length - inPos; - System.arraycopy(b, off, inBuf, inPos, amtToCopy); - inPos += amtToCopy; - compressAndSendBlock(); - off += amtToCopy; - len -= amtToCopy; - } - - // Copy the remaining (incomplete) block into inBuf - System.arraycopy(b, off, inBuf, inPos, len); - inPos += len; - } - - @Override - public void flush() throws IOException { - if (inPos > MAX_HDR_SIZE) - compressAndSendBlock(); - out.flush(); - } - - // Send the data in inBuf, and reset inPos to start writing a new block. - private void compressAndSendBlock() throws IOException { - int us = inPos - MAX_HDR_SIZE; - int maxcs = us > 4 ? us - 4 : us; - int cs = LZF.compress(inBuf, MAX_HDR_SIZE, us, outBuf, MAX_HDR_SIZE, maxcs); - if (cs != 0) { - // Compression made the data smaller; use type 1 header - outBuf[0] = 'Z'; - outBuf[1] = 'V'; - outBuf[2] = 1; - outBuf[3] = (byte) (cs >> 8); - outBuf[4] = (byte) (cs & 0xFF); - outBuf[5] = (byte) (us >> 8); - outBuf[6] = (byte) (us & 0xFF); - out.write(outBuf, 0, 7 + cs); - } else { - // Compression didn't help; use type 0 header and uncompressed data - inBuf[2] = 'Z'; - inBuf[3] = 'V'; - inBuf[4] = 0; - inBuf[5] = (byte) (us >> 8); - inBuf[6] = (byte) (us & 0xFF); - out.write(inBuf, 2, 5 + us); - } - inPos = MAX_HDR_SIZE; - } -} diff --git a/src/native/Makefile b/src/native/Makefile deleted file mode 100644 index 6236e26f3d..0000000000 --- a/src/native/Makefile +++ /dev/null @@ -1,30 +0,0 @@ -CC = gcc -#JAVA_HOME = /usr/lib/jvm/java-6-sun -OS_NAME = linux - -CFLAGS = -fPIC -O3 -funroll-all-loops - -SPARK = ../.. - -LZF = $(SPARK)/third_party/liblzf-3.5 - -LIB = libspark_native.so - -all: $(LIB) - -spark_compress_lzf_LZF.h: $(SPARK)/build/classes/spark/compress/lzf/LZF.class -ifeq ($(JAVA_HOME),) - $(error JAVA_HOME is not set) -else - $(JAVA_HOME)/bin/javah -classpath $(SPARK)/build/classes spark.compress.lzf.LZF -endif - -$(LIB): spark_compress_lzf_LZF.h spark_compress_lzf_LZF.c - $(CC) $(CFLAGS) -shared -o $@ spark_compress_lzf_LZF.c \ - -I $(JAVA_HOME)/include -I $(JAVA_HOME)/include/$(OS_NAME) \ - -I $(LZF) $(LZF)/lzf_c.c $(LZF)/lzf_d.c - -clean: - rm -f spark_compress_lzf_LZF.h $(LIB) - -.PHONY: all clean diff --git a/src/native/spark_compress_lzf_LZF.c b/src/native/spark_compress_lzf_LZF.c deleted file mode 100644 index c2a59def3e..0000000000 --- a/src/native/spark_compress_lzf_LZF.c +++ /dev/null @@ -1,90 +0,0 @@ -#include "spark_compress_lzf_LZF.h" -#include <lzf.h> - - -/* Helper function to throw an exception */ -static void throwException(JNIEnv *env, const char* className) { - jclass cls = (*env)->FindClass(env, className); - if (cls != 0) /* If cls is null, an exception was already thrown */ - (*env)->ThrowNew(env, cls, ""); -} - - -/* - * Since LZF.compress() and LZF.decompress() have the same signatures - * and differ only in which lzf_ function they call, implement both in a - * single function and pass it a pointer to the correct lzf_ function. - */ -static jint callCompressionFunction - (unsigned int (*func)(const void *const, unsigned int, void *, unsigned int), - JNIEnv *env, jclass cls, jbyteArray inArray, jint inOff, jint inLen, - jbyteArray outArray, jint outOff, jint outLen) -{ - jint inCap; - jint outCap; - jbyte *inData = 0; - jbyte *outData = 0; - jint ret; - jint s; - - if (!inArray || !outArray) { - throwException(env, "java/lang/NullPointerException"); - goto cleanup; - } - - inCap = (*env)->GetArrayLength(env, inArray); - outCap = (*env)->GetArrayLength(env, outArray); - - // Check if any of the offset/length pairs is invalid; we do this by OR'ing - // things we don't want to be negative and seeing if the result is negative - s = inOff | inLen | (inOff + inLen) | (inCap - (inOff + inLen)) | - outOff | outLen | (outOff + outLen) | (outCap - (outOff + outLen)); - if (s < 0) { - throwException(env, "java/lang/IndexOutOfBoundsException"); - goto cleanup; - } - - inData = (*env)->GetPrimitiveArrayCritical(env, inArray, 0); - outData = (*env)->GetPrimitiveArrayCritical(env, outArray, 0); - - if (!inData || !outData) { - // Out of memory - JVM will throw OutOfMemoryError - goto cleanup; - } - - ret = func(inData + inOff, inLen, outData + outOff, outLen); - -cleanup: - if (inData) - (*env)->ReleasePrimitiveArrayCritical(env, inArray, inData, 0); - if (outData) - (*env)->ReleasePrimitiveArrayCritical(env, outArray, outData, 0); - - return ret; -} - -/* - * Class: spark_compress_lzf_LZF - * Method: compress - * Signature: ([B[B)I - */ -JNIEXPORT jint JNICALL Java_spark_compress_lzf_LZF_compress - (JNIEnv *env, jclass cls, jbyteArray inArray, jint inOff, jint inLen, - jbyteArray outArray, jint outOff, jint outLen) -{ - return callCompressionFunction(lzf_compress, env, cls, - inArray, inOff, inLen, outArray,outOff, outLen); -} - -/* - * Class: spark_compress_lzf_LZF - * Method: decompress - * Signature: ([B[B)I - */ -JNIEXPORT jint JNICALL Java_spark_compress_lzf_LZF_decompress - (JNIEnv *env, jclass cls, jbyteArray inArray, jint inOff, jint inLen, - jbyteArray outArray, jint outOff, jint outLen) -{ - return callCompressionFunction(lzf_decompress, env, cls, - inArray, inOff, inLen, outArray,outOff, outLen); -} diff --git a/src/scala/spark/Broadcast.scala b/src/scala/spark/Broadcast.scala index 5089dca82e..2fdd960bdc 100644 --- a/src/scala/spark/Broadcast.scala +++ b/src/scala/spark/Broadcast.scala @@ -14,7 +14,7 @@ import scala.collection.mutable.Map import org.apache.hadoop.conf.Configuration import org.apache.hadoop.fs.{FileSystem, Path, RawLocalFileSystem} -import spark.compress.lzf.{LZFInputStream, LZFOutputStream} +import com.ning.compress.lzf.{LZFInputStream, LZFOutputStream} @serializable trait BroadcastRecipe { diff --git a/third_party/compress-lzf-0.6.0/compress-lzf-0.6.0.jar b/third_party/compress-lzf-0.6.0/compress-lzf-0.6.0.jar new file mode 100644 index 0000000000000000000000000000000000000000..6cb5c4c92b9e21300c5203b8e4f4727028a68ad3 GIT binary patch literal 14497 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 KR0{s_s{bF-p8@p% literal 0 HcmV?d00001 -- GitLab