diff options
Diffstat (limited to 'src/main/java')
| -rw-r--r-- | src/main/java/org/mamago/logging/Formattable.java | 13 | ||||
| -rw-r--r-- | src/main/java/org/mamago/logging/Header.java | 82 | ||||
| -rw-r--r-- | src/main/java/org/mamago/logging/Logger.java | 153 | ||||
| -rw-r--r-- | src/main/java/org/mamago/logging/ThrowableFormatterFlyweight.java | 23 | ||||
| -rw-r--r-- | src/main/java/org/mamago/util/Bits.java | 16 | ||||
| -rw-r--r-- | src/main/java/org/mamago/util/Cast.java | 16 |
6 files changed, 208 insertions, 95 deletions
diff --git a/src/main/java/org/mamago/logging/Formattable.java b/src/main/java/org/mamago/logging/Formattable.java new file mode 100644 index 0000000..2711ea1 --- /dev/null +++ b/src/main/java/org/mamago/logging/Formattable.java @@ -0,0 +1,13 @@ +package org.mamago.logging; + +import java.nio.ByteBuffer; + +public interface Formattable { + /** + * Format an argument to the logger buffer. + * + * @param buffer ByteBuffer to format to. + * @return length of bytes written to buffer. + */ + int formatTo(ByteBuffer buffer); +} diff --git a/src/main/java/org/mamago/logging/Header.java b/src/main/java/org/mamago/logging/Header.java new file mode 100644 index 0000000..392b328 --- /dev/null +++ b/src/main/java/org/mamago/logging/Header.java @@ -0,0 +1,82 @@ +package org.mamago.logging; + +import org.mamago.util.Bits; + +import java.nio.ByteBuffer; + +import static java.lang.Math.min; +import static org.mamago.util.Cast.asByte; +import static org.mamago.util.Cast.asShort; + +/** + * Header fields are as follows: + * <table> + * <tr><th>Bytes</th><th>Field</th></tr> + * <tr><td>8</td><td>Header size</td></tr> + * <tr><td>8</td><td>Timestamp</td></tr> + * <tr><td>8</td><td>Log level</td></tr> + * <tr><td>8</td><td>Thread Id (tid)</td></tr> + * <tr><td>130</td><td>Thread name (includes length field - 2 bytes)</td></tr> + * <tr><td>130</td><td>Logger name (includes length field - 2 bytes)</td></tr> + * <tr><td>?</td><td>Padding to align to long size</td></tr> + * <tr><td>8</td><td>Body size</td></tr> + * </table> + */ +class Header { + static final int NAME_SIZE_LIMIT = 128; + + static final int SIZE_OFFSET = 0; + static final int TS_OFFSET = SIZE_OFFSET + 8; + static final int LEVEL_OFFSET = TS_OFFSET + 8; + static final int TID_OFFSET = LEVEL_OFFSET + 8; + static final int TNAME_OFFSET = TID_OFFSET + 8; + static final int LOGGER_NAME_OFFSET = TNAME_OFFSET + NAME_SIZE_LIMIT + 2; + static final int PADDING_OFFSET = LOGGER_NAME_OFFSET + NAME_SIZE_LIMIT + 2; + static final int BODY_SIZE_OFFSET = Bits.roundUpToLongSize(PADDING_OFFSET); + static final int HEADER_SIZE = BODY_SIZE_OFFSET + 8; + + private final ByteBuffer buffer; + + Header(CharSequence loggerName, ByteBuffer buffer) { + this.buffer = buffer; + // Write header length at start + buffer.putLong(0, HEADER_SIZE); + put(LOGGER_NAME_OFFSET, loggerName); + } + + Header timestamp(long ts) { + buffer.putLong(TS_OFFSET, ts); + return this; + } + + Header level(Logger.Level level) { + buffer.putLong(LEVEL_OFFSET, asByte(level.ordinal())); + return this; + } + + Header threadId(long tid) { + buffer.putLong(TID_OFFSET, tid); + return this; + } + + Header threadName(CharSequence tName) { + put(TNAME_OFFSET, tName); + return this; + } + + // Does not return this reference, as this is the terminator for building the header buffer + void bodySize(long size) { + buffer.putLong(BODY_SIZE_OFFSET, size); + } + + private void put(int offset, CharSequence csq) { + // @Optimisation[MA]: CharSequence length as a short, as we limit header char sequence length + // (we don't expect long thread or logger names). + int length = min(csq.length(), NAME_SIZE_LIMIT); + buffer.putShort(offset++, asShort(length)); + offset++; + for (int i = 0; i < length; i++) { + buffer.put(offset++, (byte) csq.charAt(i)); + } + } +} diff --git a/src/main/java/org/mamago/logging/Logger.java b/src/main/java/org/mamago/logging/Logger.java index 6610da8..0c6d06a 100644 --- a/src/main/java/org/mamago/logging/Logger.java +++ b/src/main/java/org/mamago/logging/Logger.java @@ -1,16 +1,22 @@ package org.mamago.logging; -import sun.misc.HexDumpEncoder; - import java.nio.ByteBuffer; -import static java.lang.Math.min; +import static org.mamago.util.Cast.asByte; +import static org.mamago.util.Cast.asShort; +// @Speedup[MA]: Increased performance can be gained by providing our own buffer for CharSequence types that gives +// direct access to the byte array for use with System.arrayCopy(). +//@NotThreadSafe @Fixme[MA] public class Logger { public enum Level {TRACE, DEBUG, INFO, WARN, ERROR} - private enum TypeMarker {BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, CHAR_SEQUENCE, FORMATTABLE} + enum TypeMarker {BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, CHAR_SEQUENCE, FORMATTABLE} private final LogBuilder builder; + // @Fixme[MA]: Extract to a ring buffer registry, or similar. Also allow custom ring buffer to be passed in to + // Logger for complete flexible configuration. + final ByteBuffer logRingBuffer = ByteBuffer.allocateDirect(4 * 1024 * 1024); // 4 MiB + // Factory methods ------------------------------------ @@ -22,47 +28,44 @@ public class Logger { return new Logger(name); } + // Instance methods ----------------------------------- private Logger(CharSequence name) { - builder = new LogBuilder(name.toString()); + builder = new LogBuilder(name.toString(), logRingBuffer); } public LogBuilder log(Level level, CharSequence formatSpec) { return builder.reset(level, formatSpec); } + // Helper methods -------------------------------------- private static String getCallerClass() { return Thread.currentThread().getStackTrace()[3].getClassName(); } - private static byte b(int i) { - // @Fixme[MA]: check whether casting give out-of-bounds check, otherwise add in for safety - return (byte) i; - } // Helper classes -------------------------------------- - public interface Formattable { - void formatTo(ByteBuffer b); - } - public static class LogBuilder implements Appendable { - // @Fixme[MA]: buffer needs to be auto-expandable - private final ByteBuffer headerBuf = ByteBuffer.allocate(256); + private static final short DUMMY = 0; + + // @Fixme[MA]: handle log message overflowing message limit + private final ByteBuffer buffer = ByteBuffer.allocate(32 * 1024); // 32 KiB limit per log message + private final ThrowableFormatterFlyweight throwableFormatterFlyweight = new ThrowableFormatterFlyweight(); + private final ByteBuffer logRingBuffer; private final Header header; - // @Fixme[MA]: buffer needs to be auto-expandable - private final ByteBuffer buffer = ByteBuffer.allocate(256); private Level level; - private LogBuilder(CharSequence loggerName) { + private LogBuilder(CharSequence loggerName, ByteBuffer logRingBuffer) { + this.logRingBuffer = logRingBuffer; // @Optimisation[MA]: Write logger name to the header buffer once. Should never have to be written again, // as LogBuilders are not shareable between loggers. In the event of seeing corruption of header buffer // data in the wild can remove this mini-optimisation and just write logger name every time in log() call. // (Though the real question would be: how if the buffer getting corrupted?) - header = new Header(loggerName); + header = new Header(loggerName, buffer); } @Override @@ -73,141 +76,101 @@ public class Logger { @Override public LogBuilder append(CharSequence csq, int start, int end) { - buffer.put(b(TypeMarker.CHAR_SEQUENCE.ordinal())); + buffer.put(asByte(TypeMarker.CHAR_SEQUENCE.ordinal())); buffer.putInt(end - start); // length for (int i = start; i < end; i++) { - append(csq.charAt(i)); + buffer.put(asByte(csq.charAt(i))); } return this; } @Override public LogBuilder append(char c) { - buffer.put(b(TypeMarker.CHAR.ordinal())); - buffer.put(b(c)); + buffer.put(asByte(TypeMarker.CHAR.ordinal())); + buffer.put(asByte(c)); return this; } - @Deprecated // @Fixme[MA]: really want an annotation to create a compiler warning. + @Deprecated // @Fixme[MA]: really want my own annotation to create a compiler warning. public LogBuilder append(String s) { append((CharSequence) s); return this; } public LogBuilder append(Formattable f) { - buffer.put(b(TypeMarker.FORMATTABLE.ordinal())); - // @Fixme[MA]: Implement this! - throw new RuntimeException("Not yet implemented"); -// return this; + buffer.put(asByte(TypeMarker.FORMATTABLE.ordinal())); + int lengthPos = buffer.position(); + // @Optimisation[MA]: would we ever add an argument whose length is longer than max short? + // @Fixme[MA]: Handle short length overflow by setting the high bit to indicate to the log writer that + // a value was truncated. The log writer can then log an appropriate warning. + buffer.putShort(DUMMY); + int length = f.formatTo(buffer); + // Set the position, as can't depend on formatter implementations to do so + buffer.position(lengthPos + length); + // Update the formatter length field + buffer.putShort(lengthPos, asShort(length)); + return this; } public LogBuilder append(byte b) { - buffer.put(b(TypeMarker.BYTE.ordinal())); + buffer.put(asByte(TypeMarker.BYTE.ordinal())); buffer.put(b); return this; } public LogBuilder append(short s) { - buffer.put(b(TypeMarker.SHORT.ordinal())); + buffer.put(asByte(TypeMarker.SHORT.ordinal())); buffer.putShort(s); return this; } public LogBuilder append(int i) { - buffer.put(b(TypeMarker.INT.ordinal())); + buffer.put(asByte(TypeMarker.INT.ordinal())); buffer.putInt(i); return this; } public LogBuilder append(long l) { - buffer.put(b(TypeMarker.LONG.ordinal())); + buffer.put(asByte(TypeMarker.LONG.ordinal())); buffer.putLong(l); return this; } public LogBuilder append(float f) { - buffer.put(b(TypeMarker.FLOAT.ordinal())); + buffer.put(asByte(TypeMarker.FLOAT.ordinal())); buffer.putFloat(f); return this; } public LogBuilder append(double d) { - buffer.put(b(TypeMarker.DOUBLE.ordinal())); + buffer.put(asByte(TypeMarker.DOUBLE.ordinal())); buffer.putDouble(d); return this; } - // @Fixme[MA]: should generate a compiler warning if a log statement is missing the .log() call + public LogBuilder append(Throwable t) { + append(throwableFormatterFlyweight.wrap(t)); + throwableFormatterFlyweight.unwrap(); + return this; + } + + // @Fixme[MA]: should generate a compiler warning if a client log statement is missing the .log() call public void log() { Thread ct = Thread.currentThread(); header.timestamp(System.currentTimeMillis()) // @Fixme[MA] Use high precision wall clock .level(level) - .threadId(ct.getId()).threadName(ct.getName()); -// logRingBuffer.write(headerBuf, buffer); - HexDumpEncoder encoder = new HexDumpEncoder(); - System.out.println("Header:"); - headerBuf.position(0).limit(headerBuf.get(0)); - System.out.println(encoder.encodeBuffer(headerBuf)); - System.out.println("Args:"); - System.out.println(encoder.encodeBuffer((ByteBuffer) buffer.flip())); + .threadId(ct.getId()) + .threadName(ct.getName()) + .bodySize(buffer.position() - Header.HEADER_SIZE); + buffer.flip(); + logRingBuffer.put(buffer); } public LogBuilder reset(Level level, CharSequence formatSpec) { this.level = level; - buffer.clear(); + buffer.clear().position(Header.HEADER_SIZE); append(formatSpec); return this; } - - // Helper classes ---------------------------------- - - // @Speedup[MA]: this implementation just packs everything tight in the header. - // It is possible that alignment optimisations may make this faster. - private class Header { - private final int tsOffset; - private final int levelOffset; - private final int tidOffset; - private final int tNameOffset; - - Header(CharSequence loggerName) { - tsOffset = put(1, loggerName); - levelOffset = tsOffset + 8; - tidOffset = levelOffset + 1; - tNameOffset = tidOffset + 8; - } - - Header timestamp(long ts) { - System.out.println("ts = " + ts); - headerBuf.putLong(tsOffset, ts); - return this; - } - - Header level(Level level) { - headerBuf.put(levelOffset, b(level.ordinal())); - return this; - } - - Header threadId(long tid) { - headerBuf.putLong(tidOffset, tid); - return this; - } - - // Does not return this reference, as this is the terminator for building the header buffer - void threadName(CharSequence tName) { - // Write header size to first byte - headerBuf.put(0, b(put(tNameOffset, tName))); - } - - private int put(int offset, CharSequence csq) { - // @Optimisation[MA]: CharSequence length as a byte, as we chop header char sequences short (we don't - // expect long thread or logger names). - int length = min(csq.length(), 50); - headerBuf.put(offset++, b(length)); - for (int i = 0; i < length; i++) { - headerBuf.put(offset++, (byte) csq.charAt(i)); - } - return offset; - } - } } } diff --git a/src/main/java/org/mamago/logging/ThrowableFormatterFlyweight.java b/src/main/java/org/mamago/logging/ThrowableFormatterFlyweight.java new file mode 100644 index 0000000..6761c8c --- /dev/null +++ b/src/main/java/org/mamago/logging/ThrowableFormatterFlyweight.java @@ -0,0 +1,23 @@ +package org.mamago.logging; + +import java.nio.ByteBuffer; + +class ThrowableFormatterFlyweight implements Formattable { + private Throwable throwable; + + ThrowableFormatterFlyweight wrap(Throwable t) { + throwable = t; + return this; + } + + void unwrap() { + throwable = null; + } + + @Override + public int formatTo(ByteBuffer b) { + int startPos = b.position(); + // @Fixme[MA]: Implement this! + return b.position() - startPos; + } +} diff --git a/src/main/java/org/mamago/util/Bits.java b/src/main/java/org/mamago/util/Bits.java new file mode 100644 index 0000000..4672ea9 --- /dev/null +++ b/src/main/java/org/mamago/util/Bits.java @@ -0,0 +1,16 @@ +package org.mamago.util; + +public class Bits { + // Prevent instantiation + private Bits() {} + + public static int roundUpToLongSize(int size) { + return roundUpToSize(size, 8); + } + + public static int roundUpToSize(int size, int width) { + // Formula: + // (size + w) / w * w; // Adding w rounds up to the next whole width + return ((size + width) / width) * width; + } +} diff --git a/src/main/java/org/mamago/util/Cast.java b/src/main/java/org/mamago/util/Cast.java new file mode 100644 index 0000000..4ed3d47 --- /dev/null +++ b/src/main/java/org/mamago/util/Cast.java @@ -0,0 +1,16 @@ +package org.mamago.util; + +public class Cast { + // Prevent instantiation + private Cast() {} + + public static byte asByte(int i) { + // @Fixme[MA]: check whether casting give out-of-bounds check, otherwise add in for safety + return (byte) i; + } + + public static short asShort(int i) { + // @Fixme[MA]: check whether casting give out-of-bounds check, otherwise add in for safety + return (byte) i; + } +} |
