Added methods and @Intern annotation to stream pooled strings (a
la String.intern) as we do class names, by sending the value the first time and a short code thereafter (with special handling for datagrams, where me must continue sending the mapping until we know they've been received). This is mostly useful for the lengthy config names and parameter names of Project X, but it could conceivably also be used for things like NamedEvents, which stream the same object field names many times in the course of a stream. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@5797 542714f4-19e9-0310-aa3c-eee0fc999fb1
This commit is contained in:
@@ -90,6 +90,11 @@ public abstract class FieldMarshaller
|
||||
ftype = Object.class;
|
||||
}
|
||||
|
||||
// use the intern marshaller for pooled strings
|
||||
if (ftype == String.class && field.isAnnotationPresent(Intern.class)) {
|
||||
return _internMarshaller;
|
||||
}
|
||||
|
||||
// if we have an exact match, use that
|
||||
FieldMarshaller fm = _marshallers.get(ftype);
|
||||
|
||||
@@ -356,11 +361,28 @@ public abstract class FieldMarshaller
|
||||
_marshallers.put(BasicStreamers.BSTREAMER_TYPES[ii],
|
||||
new StreamerMarshaller(BasicStreamers.BSTREAMER_INSTANCES[ii]));
|
||||
}
|
||||
|
||||
// create the field marshaller for pooled strings
|
||||
_internMarshaller = new FieldMarshaller() {
|
||||
@Override
|
||||
public void readField (Field field, Object target, ObjectInputStream in)
|
||||
throws Exception {
|
||||
field.set(target, in.readIntern());
|
||||
}
|
||||
@Override
|
||||
public void writeField (Field field, Object source, ObjectOutputStream out)
|
||||
throws Exception {
|
||||
out.writeIntern((String)field.get(source));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Contains a mapping from field type to field marshaller instance for that type. */
|
||||
protected static HashMap<Class<?>, FieldMarshaller> _marshallers;
|
||||
|
||||
/** The field marshaller for pooled strings. */
|
||||
protected static FieldMarshaller _internMarshaller;
|
||||
|
||||
/** Defines the signature to a custom field reader method. */
|
||||
protected static final Class<?>[] READER_ARGS = { ObjectInputStream.class };
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// $Id$
|
||||
//
|
||||
// Narya library - tools for developing networked games
|
||||
// Copyright (C) 2002-2008 Three Rings Design, Inc., All Rights Reserved
|
||||
// http://www.threerings.net/code/narya/
|
||||
//
|
||||
// This library is free software; you can redistribute it and/or modify it
|
||||
// under the terms of the GNU Lesser General Public License as published
|
||||
// by the Free Software Foundation; either version 2.1 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
// Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public
|
||||
// License along with this library; if not, write to the Free Software
|
||||
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
package com.threerings.io;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Flags a {@link String} field in a streamable object as being a member of the global string pool
|
||||
* accessed using {@link String#intern}. Each unique value will be streamed only once, with
|
||||
* further instances replaced by a compact reference.
|
||||
*/
|
||||
@Target({ ElementType.FIELD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Intern
|
||||
{
|
||||
}
|
||||
@@ -100,6 +100,64 @@ public class ObjectInputStream extends DataInputStream
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a pooled string value from the input stream.
|
||||
*/
|
||||
public String readIntern ()
|
||||
throws IOException
|
||||
{
|
||||
// create our intern map if necessary
|
||||
if (_internmap == null) {
|
||||
_internmap = new ArrayList<String>();
|
||||
// insert a zeroth element
|
||||
_internmap.add(null);
|
||||
}
|
||||
|
||||
// read in the intern code for this instance
|
||||
short code = readShort();
|
||||
|
||||
// a zero code indicates a null value
|
||||
if (code == 0) {
|
||||
return null;
|
||||
|
||||
// if the code is negative, that means that we've never seen if before and value follows
|
||||
} else if (code < 0) {
|
||||
// first swap the code into positive-land
|
||||
code *= -1;
|
||||
|
||||
// read in the value
|
||||
String value = readUTF().intern();
|
||||
|
||||
// create the mapping and return the value
|
||||
mapIntern(code, value);
|
||||
return value;
|
||||
|
||||
} else {
|
||||
String value = (code < _internmap.size()) ? _internmap.get(code) : null;
|
||||
|
||||
// sanity check
|
||||
if (value == null) {
|
||||
// this will help with debugging
|
||||
log.warning("Internal stream error, no intern value", "code", code,
|
||||
"ois", this, new Exception());
|
||||
log.warning("ObjectInputStream mappings", "map", _internmap);
|
||||
String errmsg = "Read intern code for which we have no registered value " +
|
||||
"metadata [code=" + code + "]";
|
||||
throw new RuntimeException(errmsg);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the intern mapping for the specified code and value.
|
||||
*/
|
||||
protected void mapIntern (short code, String value)
|
||||
throws IOException
|
||||
{
|
||||
_internmap.add(code, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a class mapping from the stream.
|
||||
*
|
||||
@@ -253,6 +311,9 @@ public class ObjectInputStream extends DataInputStream
|
||||
* them. */
|
||||
protected ArrayList<ClassMapping> _classmap;
|
||||
|
||||
/** Maps numeric codes to pooled strings. */
|
||||
protected ArrayList<String> _internmap;
|
||||
|
||||
/** The object currently being read from the stream. */
|
||||
protected Object _current;
|
||||
|
||||
|
||||
@@ -77,6 +77,83 @@ public class ObjectOutputStream extends DataOutputStream
|
||||
writeBareObject(object, cmap.streamer, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a pooled string value to the output stream.
|
||||
*/
|
||||
public void writeIntern (String value)
|
||||
throws IOException
|
||||
{
|
||||
// if the value to be written is null, simply write a zero
|
||||
if (value == null) {
|
||||
writeShort(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// create our intern map if necessary
|
||||
if (_internmap == null) {
|
||||
_internmap = new HashMap<String, Short>();
|
||||
}
|
||||
|
||||
// look up the intern mapping record
|
||||
Short code = _internmap.get(value);
|
||||
|
||||
// create a mapping for the value if we've not got one
|
||||
if (code == null) {
|
||||
if (ObjectInputStream.STREAM_DEBUG) {
|
||||
log.info(hashCode() + ": Creating intern mapping", "code", _nextInternCode,
|
||||
"value", value);
|
||||
}
|
||||
code = createInternMapping(_nextInternCode++);
|
||||
_internmap.put(value.intern(), code);
|
||||
|
||||
// make sure we didn't blow past our maximum intern count
|
||||
if (_nextInternCode <= 0) {
|
||||
throw new RuntimeException("Too many unique interns written to ObjectOutputStream");
|
||||
}
|
||||
writeNewInternMapping(code, value);
|
||||
|
||||
} else {
|
||||
writeExistingInternMapping(code, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and returns a new intern mapping.
|
||||
*/
|
||||
protected Short createInternMapping (short code)
|
||||
{
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a new intern mapping to the stream.
|
||||
*/
|
||||
protected void writeNewInternMapping (short code, String value)
|
||||
throws IOException
|
||||
{
|
||||
// writing a negative code indicates that the value will follow
|
||||
writeInternMapping(-code, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes an existing intern mapping to the stream.
|
||||
*/
|
||||
protected void writeExistingInternMapping (short code, String value)
|
||||
throws IOException
|
||||
{
|
||||
writeShort(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out the mapping for an intern.
|
||||
*/
|
||||
protected void writeInternMapping (int code, String value)
|
||||
throws IOException
|
||||
{
|
||||
writeShort(code);
|
||||
writeUTF(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves or creates the class mapping for the supplied class, writes it out to the stream,
|
||||
* and returns a reference to it.
|
||||
@@ -97,17 +174,17 @@ public class ObjectOutputStream extends DataOutputStream
|
||||
// create a streamer instance and assign a code to this class
|
||||
Streamer streamer = Streamer.getStreamer(sclass);
|
||||
// we specifically do not inline the getStreamer() call into the ClassMapping
|
||||
// constructor because we want to be sure not to call _nextCode++ if getStreamer()
|
||||
// constructor because we want to be sure not to call _nextClassCode++ if getStreamer()
|
||||
// throws an exception
|
||||
if (ObjectInputStream.STREAM_DEBUG) {
|
||||
log.info(hashCode() + ": Creating class mapping", "code", _nextCode,
|
||||
log.info(hashCode() + ": Creating class mapping", "code", _nextClassCode,
|
||||
"class", sclass.getName());
|
||||
}
|
||||
cmap = createClassMapping(_nextCode++, sclass, streamer);
|
||||
cmap = createClassMapping(_nextClassCode++, sclass, streamer);
|
||||
_classmap.put(sclass, cmap);
|
||||
|
||||
// make sure we didn't blow past our maximum class count
|
||||
if (_nextCode <= 0) {
|
||||
if (_nextClassCode <= 0) {
|
||||
throw new RuntimeException("Too many unique classes written to ObjectOutputStream");
|
||||
}
|
||||
writeNewClassMapping(cmap);
|
||||
@@ -215,8 +292,14 @@ public class ObjectOutputStream extends DataOutputStream
|
||||
* them. */
|
||||
protected Map<Class<?>, ClassMapping> _classmap;
|
||||
|
||||
/** Used to map pooled strings to numeric codes. */
|
||||
protected Map<String, Short> _internmap;
|
||||
|
||||
/** A counter used to assign codes to streamed classes. */
|
||||
protected short _nextCode = 1;
|
||||
protected short _nextClassCode = 1;
|
||||
|
||||
/** A counter used to assign codes to pooled strings. */
|
||||
protected short _nextInternCode = 1;
|
||||
|
||||
/** The object currently being written to the stream. */
|
||||
protected Object _current;
|
||||
|
||||
@@ -43,4 +43,25 @@ public class UnreliableObjectInputStream extends ObjectInputStream
|
||||
_classmap.set(code, cmap);
|
||||
return cmap;
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void mapIntern (short code, String value)
|
||||
{
|
||||
// see if we already have a mapping
|
||||
String ovalue = (code < _internmap.size()) ? _internmap.get(code) : null;
|
||||
if (ovalue != null) {
|
||||
// sanity check
|
||||
if (!ovalue.equals(value)) {
|
||||
throw new RuntimeException(
|
||||
"Received mapping for intern that conflicts with existing mapping " +
|
||||
"[code=" + code + ", ovalue=" + ovalue + ", nvalue=" + value + "]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// insert null entries for missing mappings
|
||||
for (int ii = 0, nn = (code + 1) - _internmap.size(); ii < nn; ii++) {
|
||||
_internmap.add(null);
|
||||
}
|
||||
_internmap.set(code, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,27 @@ public class UnreliableObjectOutputStream extends ObjectOutputStream
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the receiver has received the mappings for a group of classes, and thus that from
|
||||
* now on, only the class codes need be sent.
|
||||
* Sets the reference to the set that will hold the pooled strings for which mappings have been
|
||||
* written.
|
||||
*/
|
||||
public void noteMappingsReceived (Collection<Class<?>> sclasses)
|
||||
public void setMappedInterns (Set<String> mappedInterns)
|
||||
{
|
||||
_mappedInterns = mappedInterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the set of pooled strings for which mappings have been written.
|
||||
*/
|
||||
public Set<String> getMappedInterns ()
|
||||
{
|
||||
return _mappedInterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the receiver has received the mappings for a group of classes and thus that from
|
||||
* now on, only the codes need be sent.
|
||||
*/
|
||||
public void noteClassMappingsReceived (Collection<Class<?>> sclasses)
|
||||
{
|
||||
// sanity check
|
||||
if (_classmap == null) {
|
||||
@@ -63,6 +80,74 @@ public class UnreliableObjectOutputStream extends ObjectOutputStream
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notes that the receiver has received the mappings for a group of interns and thus that from
|
||||
* now on, only the codes need be sent.
|
||||
*/
|
||||
public void noteInternMappingsReceived (Collection<String> sinterns)
|
||||
{
|
||||
// sanity check
|
||||
if (_internmap == null) {
|
||||
throw new RuntimeException("Missing intern map");
|
||||
}
|
||||
|
||||
// make each intern's code positive to signify that we no longer need to send metadata
|
||||
for (String sintern : sinterns) {
|
||||
Short code = _internmap.get(sintern);
|
||||
if (code == null) {
|
||||
throw new RuntimeException("No intern mapping for " + sintern);
|
||||
}
|
||||
short scode = code;
|
||||
if (scode < 0) {
|
||||
_internmap.put(sintern, (short)-code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected Short createInternMapping (short code)
|
||||
{
|
||||
// the negative intern code indicates that we must rewrite the metadata for the first
|
||||
// instance in each go-round; when we are notified that the other side has cached the
|
||||
// mapping, we can simply write the (positive) code
|
||||
return (short)-code;
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void writeNewInternMapping (short code, String value)
|
||||
throws IOException
|
||||
{
|
||||
writeInternMapping(code, value);
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void writeExistingInternMapping (short code, String value)
|
||||
throws IOException
|
||||
{
|
||||
// if the other side has received the mapping, we need only send the reference
|
||||
if (code > 0) {
|
||||
writeShort(code);
|
||||
|
||||
// likewise if we've written the value once this go-round
|
||||
} else if (_mappedInterns.contains(value)) {
|
||||
writeShort(-code);
|
||||
|
||||
// otherwise, we must retransmit the mapping
|
||||
} else {
|
||||
writeInternMapping(code, value);
|
||||
}
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected void writeInternMapping (int code, String value)
|
||||
throws IOException
|
||||
{
|
||||
super.writeInternMapping(code, value);
|
||||
|
||||
// note that we've written the mapping
|
||||
_mappedInterns.add(value);
|
||||
}
|
||||
|
||||
@Override // documentation inherited
|
||||
protected ClassMapping createClassMapping (short code, Class<?> sclass, Streamer streamer)
|
||||
{
|
||||
@@ -109,4 +194,7 @@ public class UnreliableObjectOutputStream extends ObjectOutputStream
|
||||
|
||||
/** The set of classes for which we have written mappings. */
|
||||
protected Set<Class<?>> _mappedClasses = new HashSet<Class<?>>();
|
||||
|
||||
/** The set of pooled strings for which we have written mappings. */
|
||||
protected Set<String> _mappedInterns = new HashSet<String>();
|
||||
}
|
||||
|
||||
@@ -58,9 +58,11 @@ public class DatagramSequencer
|
||||
_uout.writeInt(++_lastNumber);
|
||||
_uout.writeInt(_lastReceived);
|
||||
|
||||
// make sure the mapped class set is clear
|
||||
// make sure the mapped sets are clear
|
||||
Set<Class<?>> mappedClasses = _uout.getMappedClasses();
|
||||
mappedClasses.clear();
|
||||
Set<String> mappedInterns = _uout.getMappedInterns();
|
||||
mappedInterns.clear();
|
||||
|
||||
// write the object
|
||||
_uout.writeObject(datagram);
|
||||
@@ -72,8 +74,15 @@ public class DatagramSequencer
|
||||
_uout.setMappedClasses(new HashSet<Class<?>>());
|
||||
}
|
||||
|
||||
// likewise with the intern mappings
|
||||
if (mappedInterns.isEmpty()) {
|
||||
mappedInterns = null;
|
||||
} else {
|
||||
_uout.setMappedInterns(new HashSet<String>());
|
||||
}
|
||||
|
||||
// record the transmission
|
||||
_sendrecs.add(new SendRecord(_lastNumber, mappedClasses));
|
||||
_sendrecs.add(new SendRecord(_lastNumber, mappedClasses, mappedInterns));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,8 +110,13 @@ public class DatagramSequencer
|
||||
break;
|
||||
}
|
||||
remove++;
|
||||
if (sendrec.number == received && sendrec.mappedClasses != null) {
|
||||
_uout.noteMappingsReceived(sendrec.mappedClasses);
|
||||
if (sendrec.number == received) {
|
||||
if (sendrec.mappedClasses != null) {
|
||||
_uout.noteClassMappingsReceived(sendrec.mappedClasses);
|
||||
}
|
||||
if (sendrec.mappedInterns != null) {
|
||||
_uout.noteInternMappingsReceived(sendrec.mappedInterns);
|
||||
}
|
||||
}
|
||||
}
|
||||
_sendrecs.subList(0, remove).clear();
|
||||
@@ -125,10 +139,15 @@ public class DatagramSequencer
|
||||
* <code>null</code> for none). */
|
||||
public Set<Class<?>> mappedClasses;
|
||||
|
||||
public SendRecord (int number, Set<Class<?>> mappedClasses)
|
||||
/** The set of interns for which mappings were included in the datagram (or
|
||||
* <code>null</code> for none). */
|
||||
public Set<String> mappedInterns;
|
||||
|
||||
public SendRecord (int number, Set<Class<?>> mappedClasses, Set<String> mappedInterns)
|
||||
{
|
||||
this.number = number;
|
||||
this.mappedClasses = mappedClasses;
|
||||
this.mappedInterns = mappedInterns;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user