gecko-dev/dom/system/gonk/worker_buf.js

624 lines
20 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* This object contains helpers buffering incoming data & deconstructing it
* into parcels as well as buffering outgoing data & constructing parcels.
* For that it maintains two buffers and corresponding uint8 views, indexes.
*
* The incoming buffer is a circular buffer where we store incoming data.
* As soon as a complete parcel is received, it is processed right away, so
* the buffer only needs to be large enough to hold one parcel.
*
* The outgoing buffer is to prepare outgoing parcels. The index is reset
* every time a parcel is sent.
*/
let Buf = {
INT32_MAX: 2147483647,
UINT8_SIZE: 1,
UINT16_SIZE: 2,
UINT32_SIZE: 4,
PARCEL_SIZE_SIZE: 4,
PDU_HEX_OCTET_SIZE: 4,
incomingBufferLength: 1024,
incomingBuffer: null,
incomingBytes: null,
incomingWriteIndex: 0,
incomingReadIndex: 0,
readIncoming: 0,
readAvailable: 0,
currentParcelSize: 0,
outgoingBufferLength: 1024,
outgoingBuffer: null,
outgoingBytes: null,
outgoingIndex: 0,
outgoingBufferCalSizeQueue: null,
_init: function() {
this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength);
this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength);
this.incomingBytes = new Uint8Array(this.incomingBuffer);
this.outgoingBytes = new Uint8Array(this.outgoingBuffer);
// Track where incoming data is read from and written to.
this.incomingWriteIndex = 0;
this.incomingReadIndex = 0;
// Leave room for the parcel size for outgoing parcels.
this.outgoingIndex = this.PARCEL_SIZE_SIZE;
// How many bytes we've read for this parcel so far.
this.readIncoming = 0;
// How many bytes available as parcel data.
this.readAvailable = 0;
// Size of the incoming parcel. If this is zero, we're expecting a new
// parcel.
this.currentParcelSize = 0;
// Queue for storing outgoing override points
this.outgoingBufferCalSizeQueue = [];
},
/**
* Mark current outgoingIndex as start point for calculation length of data
* written to outgoingBuffer.
* Mark can be nested for here uses queue to remember marks.
*
* @param writeFunction
* Function to write data length into outgoingBuffer, this function is
* also used to allocate buffer for data length.
* Raw data size(in Uint8) is provided as parameter calling writeFunction.
* If raw data size is not in proper unit for writing, user can adjust
* the length value in writeFunction before writing.
**/
startCalOutgoingSize: function(writeFunction) {
let sizeInfo = {index: this.outgoingIndex,
write: writeFunction};
// Allocate buffer for data lemgtj.
writeFunction.call(0);
// Get size of data length buffer for it is not counted into data size.
sizeInfo.size = this.outgoingIndex - sizeInfo.index;
// Enqueue size calculation information.
this.outgoingBufferCalSizeQueue.push(sizeInfo);
},
/**
* Calculate data length since last mark, and write it into mark position.
**/
stopCalOutgoingSize: function() {
let sizeInfo = this.outgoingBufferCalSizeQueue.pop();
// Remember current outgoingIndex.
let currentOutgoingIndex = this.outgoingIndex;
// Calculate data length, in uint8.
let writeSize = this.outgoingIndex - sizeInfo.index - sizeInfo.size;
// Write data length to mark, use same function for allocating buffer to make
// sure there is no buffer overloading.
this.outgoingIndex = sizeInfo.index;
sizeInfo.write(writeSize);
// Restore outgoingIndex.
this.outgoingIndex = currentOutgoingIndex;
},
/**
* Grow the incoming buffer.
*
* @param min_size
* Minimum new size. The actual new size will be the the smallest
* power of 2 that's larger than this number.
*/
growIncomingBuffer: function(min_size) {
if (DEBUG) {
debug("Current buffer of " + this.incomingBufferLength +
" can't handle incoming " + min_size + " bytes.");
}
let oldBytes = this.incomingBytes;
this.incomingBufferLength =
2 << Math.floor(Math.log(min_size)/Math.log(2));
if (DEBUG) debug("New incoming buffer size: " + this.incomingBufferLength);
this.incomingBuffer = new ArrayBuffer(this.incomingBufferLength);
this.incomingBytes = new Uint8Array(this.incomingBuffer);
if (this.incomingReadIndex <= this.incomingWriteIndex) {
// Read and write index are in natural order, so we can just copy
// the old buffer over to the bigger one without having to worry
// about the indexes.
this.incomingBytes.set(oldBytes, 0);
} else {
// The write index has wrapped around but the read index hasn't yet.
// Write whatever the read index has left to read until it would
// circle around to the beginning of the new buffer, and the rest
// behind that.
let head = oldBytes.subarray(this.incomingReadIndex);
let tail = oldBytes.subarray(0, this.incomingReadIndex);
this.incomingBytes.set(head, 0);
this.incomingBytes.set(tail, head.length);
this.incomingReadIndex = 0;
this.incomingWriteIndex += head.length;
}
if (DEBUG) {
debug("New incoming buffer size is " + this.incomingBufferLength);
}
},
/**
* Grow the outgoing buffer.
*
* @param min_size
* Minimum new size. The actual new size will be the the smallest
* power of 2 that's larger than this number.
*/
growOutgoingBuffer: function(min_size) {
if (DEBUG) {
debug("Current buffer of " + this.outgoingBufferLength +
" is too small.");
}
let oldBytes = this.outgoingBytes;
this.outgoingBufferLength =
2 << Math.floor(Math.log(min_size)/Math.log(2));
this.outgoingBuffer = new ArrayBuffer(this.outgoingBufferLength);
this.outgoingBytes = new Uint8Array(this.outgoingBuffer);
this.outgoingBytes.set(oldBytes, 0);
if (DEBUG) {
debug("New outgoing buffer size is " + this.outgoingBufferLength);
}
},
/**
* Functions for reading data from the incoming buffer.
*
* These are all little endian, apart from readParcelSize();
*/
/**
* Ensure position specified is readable.
*
* @param index
* Data position in incoming parcel, valid from 0 to
* currentParcelSize.
*/
ensureIncomingAvailable: function(index) {
if (index >= this.currentParcelSize) {
throw new Error("Trying to read data beyond the parcel end!");
} else if (index < 0) {
throw new Error("Trying to read data before the parcel begin!");
}
},
/**
* Seek in current incoming parcel.
*
* @param offset
* Seek offset in relative to current position.
*/
seekIncoming: function(offset) {
// Translate to 0..currentParcelSize
let cur = this.currentParcelSize - this.readAvailable;
let newIndex = cur + offset;
this.ensureIncomingAvailable(newIndex);
// ... incomingReadIndex -->|
// 0 new cur currentParcelSize
// |================|=======|====================|
// |<-- cur -->|<- readAvailable ->|
// |<-- newIndex -->|<-- new readAvailable -->|
this.readAvailable = this.currentParcelSize - newIndex;
// Translate back:
if (this.incomingReadIndex < cur) {
// The incomingReadIndex is wrapped.
newIndex += this.incomingBufferLength;
}
newIndex += (this.incomingReadIndex - cur);
newIndex %= this.incomingBufferLength;
this.incomingReadIndex = newIndex;
},
readUint8Unchecked: function() {
let value = this.incomingBytes[this.incomingReadIndex];
this.incomingReadIndex = (this.incomingReadIndex + 1) %
this.incomingBufferLength;
return value;
},
readUint8: function() {
// Translate to 0..currentParcelSize
let cur = this.currentParcelSize - this.readAvailable;
this.ensureIncomingAvailable(cur);
this.readAvailable--;
return this.readUint8Unchecked();
},
readUint8Array: function(length) {
// Translate to 0..currentParcelSize
let last = this.currentParcelSize - this.readAvailable;
last += (length - 1);
this.ensureIncomingAvailable(last);
let array = new Uint8Array(length);
for (let i = 0; i < length; i++) {
array[i] = this.readUint8Unchecked();
}
this.readAvailable -= length;
return array;
},
readUint16: function() {
return this.readUint8() | this.readUint8() << 8;
},
readInt32: function() {
return this.readUint8() | this.readUint8() << 8 |
this.readUint8() << 16 | this.readUint8() << 24;
},
readInt64: function() {
// Avoid using bitwise operators as the operands of all bitwise operators
// are converted to signed 32-bit integers.
return this.readUint8() +
this.readUint8() * Math.pow(2, 8) +
this.readUint8() * Math.pow(2, 16) +
this.readUint8() * Math.pow(2, 24) +
this.readUint8() * Math.pow(2, 32) +
this.readUint8() * Math.pow(2, 40) +
this.readUint8() * Math.pow(2, 48) +
this.readUint8() * Math.pow(2, 56);
},
readInt32List: function() {
let length = this.readInt32();
let ints = [];
for (let i = 0; i < length; i++) {
ints.push(this.readInt32());
}
return ints;
},
readString: function() {
let string_len = this.readInt32();
if (string_len < 0 || string_len >= this.INT32_MAX) {
return null;
}
let s = "";
for (let i = 0; i < string_len; i++) {
s += String.fromCharCode(this.readUint16());
}
// Strings are \0\0 delimited, but that isn't part of the length. And
// if the string length is even, the delimiter is two characters wide.
// It's insane, I know.
this.readStringDelimiter(string_len);
return s;
},
readStringList: function() {
let num_strings = this.readInt32();
let strings = [];
for (let i = 0; i < num_strings; i++) {
strings.push(this.readString());
}
return strings;
},
readStringDelimiter: function(length) {
let delimiter = this.readUint16();
if (!(length & 1)) {
delimiter |= this.readUint16();
}
if (DEBUG) {
if (delimiter !== 0) {
debug("Something's wrong, found string delimiter: " + delimiter);
}
}
},
readParcelSize: function() {
return this.readUint8Unchecked() << 24 |
this.readUint8Unchecked() << 16 |
this.readUint8Unchecked() << 8 |
this.readUint8Unchecked();
},
/**
* Functions for writing data to the outgoing buffer.
*/
/**
* Ensure position specified is writable.
*
* @param index
* Data position in outgoing parcel, valid from 0 to
* outgoingBufferLength.
*/
ensureOutgoingAvailable: function(index) {
if (index >= this.outgoingBufferLength) {
this.growOutgoingBuffer(index + 1);
}
},
writeUint8: function(value) {
this.ensureOutgoingAvailable(this.outgoingIndex);
this.outgoingBytes[this.outgoingIndex] = value;
this.outgoingIndex++;
},
writeUint16: function(value) {
this.writeUint8(value & 0xff);
this.writeUint8((value >> 8) & 0xff);
},
writeInt32: function(value) {
this.writeUint8(value & 0xff);
this.writeUint8((value >> 8) & 0xff);
this.writeUint8((value >> 16) & 0xff);
this.writeUint8((value >> 24) & 0xff);
},
writeString: function(value) {
if (value == null) {
this.writeInt32(-1);
return;
}
this.writeInt32(value.length);
for (let i = 0; i < value.length; i++) {
this.writeUint16(value.charCodeAt(i));
}
// Strings are \0\0 delimited, but that isn't part of the length. And
// if the string length is even, the delimiter is two characters wide.
// It's insane, I know.
this.writeStringDelimiter(value.length);
},
writeStringList: function(strings) {
this.writeInt32(strings.length);
for (let i = 0; i < strings.length; i++) {
this.writeString(strings[i]);
}
},
writeStringDelimiter: function(length) {
this.writeUint16(0);
if (!(length & 1)) {
this.writeUint16(0);
}
},
writeParcelSize: function(value) {
/**
* Parcel size will always be the first thing in the parcel byte
* array, but the last thing written. Store the current index off
* to a temporary to be reset after we write the size.
*/
let currentIndex = this.outgoingIndex;
this.outgoingIndex = 0;
this.writeUint8((value >> 24) & 0xff);
this.writeUint8((value >> 16) & 0xff);
this.writeUint8((value >> 8) & 0xff);
this.writeUint8(value & 0xff);
this.outgoingIndex = currentIndex;
},
copyIncomingToOutgoing: function(length) {
if (!length || (length < 0)) {
return;
}
let translatedReadIndexEnd =
this.currentParcelSize - this.readAvailable + length - 1;
this.ensureIncomingAvailable(translatedReadIndexEnd);
let translatedWriteIndexEnd = this.outgoingIndex + length - 1;
this.ensureOutgoingAvailable(translatedWriteIndexEnd);
let newIncomingReadIndex = this.incomingReadIndex + length;
if (newIncomingReadIndex < this.incomingBufferLength) {
// Reading won't cause wrapping, go ahead with builtin copy.
this.outgoingBytes
.set(this.incomingBytes.subarray(this.incomingReadIndex,
newIncomingReadIndex),
this.outgoingIndex);
} else {
// Not so lucky.
newIncomingReadIndex %= this.incomingBufferLength;
this.outgoingBytes
.set(this.incomingBytes.subarray(this.incomingReadIndex,
this.incomingBufferLength),
this.outgoingIndex);
if (newIncomingReadIndex) {
let firstPartLength = this.incomingBufferLength - this.incomingReadIndex;
this.outgoingBytes.set(this.incomingBytes.subarray(0, newIncomingReadIndex),
this.outgoingIndex + firstPartLength);
}
}
this.incomingReadIndex = newIncomingReadIndex;
this.readAvailable -= length;
this.outgoingIndex += length;
},
/**
* Parcel management
*/
/**
* Write incoming data to the circular buffer.
*
* @param incoming
* Uint8Array containing the incoming data.
*/
writeToIncoming: function(incoming) {
// We don't have to worry about the head catching the tail since
// we process any backlog in parcels immediately, before writing
// new data to the buffer. So the only edge case we need to handle
// is when the incoming data is larger than the buffer size.
let minMustAvailableSize = incoming.length + this.readIncoming;
if (minMustAvailableSize > this.incomingBufferLength) {
this.growIncomingBuffer(minMustAvailableSize);
}
// We can let the typed arrays do the copying if the incoming data won't
// wrap around the edges of the circular buffer.
let remaining = this.incomingBufferLength - this.incomingWriteIndex;
if (remaining >= incoming.length) {
this.incomingBytes.set(incoming, this.incomingWriteIndex);
} else {
// The incoming data would wrap around it.
let head = incoming.subarray(0, remaining);
let tail = incoming.subarray(remaining);
this.incomingBytes.set(head, this.incomingWriteIndex);
this.incomingBytes.set(tail, 0);
}
this.incomingWriteIndex = (this.incomingWriteIndex + incoming.length) %
this.incomingBufferLength;
},
/**
* Process incoming data.
*
* @param incoming
* Uint8Array containing the incoming data.
*/
processIncoming: function(incoming) {
if (DEBUG) {
debug("Received " + incoming.length + " bytes.");
debug("Already read " + this.readIncoming);
}
this.writeToIncoming(incoming);
this.readIncoming += incoming.length;
while (true) {
if (!this.currentParcelSize) {
// We're expecting a new parcel.
if (this.readIncoming < this.PARCEL_SIZE_SIZE) {
// We don't know how big the next parcel is going to be, need more
// data.
if (DEBUG) debug("Next parcel size unknown, going to sleep.");
return;
}
this.currentParcelSize = this.readParcelSize();
if (DEBUG) {
debug("New incoming parcel of size " + this.currentParcelSize);
}
// The size itself is not included in the size.
this.readIncoming -= this.PARCEL_SIZE_SIZE;
}
if (this.readIncoming < this.currentParcelSize) {
// We haven't read enough yet in order to be able to process a parcel.
if (DEBUG) debug("Read " + this.readIncoming + ", but parcel size is "
+ this.currentParcelSize + ". Going to sleep.");
return;
}
// Alright, we have enough data to process at least one whole parcel.
// Let's do that.
let expectedAfterIndex = (this.incomingReadIndex + this.currentParcelSize)
% this.incomingBufferLength;
if (DEBUG) {
let parcel;
if (expectedAfterIndex < this.incomingReadIndex) {
let head = this.incomingBytes.subarray(this.incomingReadIndex);
let tail = this.incomingBytes.subarray(0, expectedAfterIndex);
parcel = Array.slice(head).concat(Array.slice(tail));
} else {
parcel = Array.slice(this.incomingBytes.subarray(
this.incomingReadIndex, expectedAfterIndex));
}
debug("Parcel (size " + this.currentParcelSize + "): " + parcel);
}
if (DEBUG) debug("We have at least one complete parcel.");
try {
this.readAvailable = this.currentParcelSize;
this.processParcel();
} catch (ex) {
if (DEBUG) debug("Parcel handling threw " + ex + "\n" + ex.stack);
}
// Ensure that the whole parcel was consumed.
if (this.incomingReadIndex != expectedAfterIndex) {
if (DEBUG) {
debug("Parcel handler didn't consume whole parcel, " +
Math.abs(expectedAfterIndex - this.incomingReadIndex) +
" bytes left over");
}
this.incomingReadIndex = expectedAfterIndex;
}
this.readIncoming -= this.currentParcelSize;
this.readAvailable = 0;
this.currentParcelSize = 0;
}
},
/**
* Communicate with the IPC thread.
*/
sendParcel: function() {
// Compute the size of the parcel and write it to the front of the parcel
// where we left room for it. Note that he parcel size does not include
// the size itself.
let parcelSize = this.outgoingIndex - this.PARCEL_SIZE_SIZE;
this.writeParcelSize(parcelSize);
// This assumes that postRILMessage will make a copy of the ArrayBufferView
// right away!
let parcel = this.outgoingBytes.subarray(0, this.outgoingIndex);
if (DEBUG) debug("Outgoing parcel: " + Array.slice(parcel));
this.onSendParcel(parcel);
this.outgoingIndex = this.PARCEL_SIZE_SIZE;
},
getCurrentParcelSize: function() {
return this.currentParcelSize;
},
getReadAvailable: function() {
return this.readAvailable;
}
/**
* Process one parcel.
*
* |processParcel| is an implementation provided incoming parcel processing
* function invoked when we have received a complete parcel. Implementation
* may call multiple read functions to extract data from the incoming buffer.
*/
//processParcel: function() {
// let something = this.readInt32();
// ...
//},
/**
* Write raw data out to underlying channel.
*
* |onSendParcel| is an implementation provided stream output function
* invoked when we're really going to write something out. We assume the
* data are completely copied to some output buffer in this call and may
* be destroyed when it's done.
*
* @param parcel
* An array of numeric octet data.
*/
//onSendParcel: function(parcel) {
// ...
//}
};
module.exports = { Buf: Buf };