@foxglove/ulog
Version:
PX4 ULog file reader
197 lines • 7.55 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChunkedReader = void 0;
const CHUNK_SIZE = 256 * 1024;
/**
* ChunkedReader provides functions to read typed data from a Filelike.
*
* It amortizes the cost loading data from the Filelike by reading chunks (pages) of data into
* memory when reading typed data.
*/
class ChunkedReader {
chunkSize;
#file;
#chunk;
#view;
/** Position in the file where we read the next chunk */
#fileCursor = 0;
/** Position in the current chunk */
#chunkCursor = 0;
#textDecoder = new TextDecoder();
constructor(filelike, chunkSize = CHUNK_SIZE) {
this.#file = filelike;
this.chunkSize = chunkSize;
}
/**
* @deprecated You should open the Filelike yourself before using ChunkedReader
*/
async open() {
return await this.#file.open();
}
view() {
return this.#view;
}
position() {
return this.#fileCursor - (this.#chunk?.byteLength ?? 0) + this.#chunkCursor;
}
size() {
return this.#file.size();
}
remaining() {
return this.size() - (this.#fileCursor - (this.#chunk?.byteLength ?? 0) + this.#chunkCursor);
}
seek(relativeByteOffset) {
const byteOffset = this.position() + relativeByteOffset;
if (byteOffset < 0 || byteOffset > this.size()) {
throw new Error(`Cannot seek to ${byteOffset}`);
}
// If we have a chunk it is more performant to attempt re-using the chunk. So we try to figure
// out if our seekTo puts us within the chunk and if so adjust the chunkCursor.
if (this.#chunk) {
// This is where the chunk starts in the file
const chunkStart = this.#fileCursor - this.#chunk.byteLength;
if (byteOffset >= chunkStart && byteOffset < this.#fileCursor) {
this.#chunkCursor = byteOffset - chunkStart;
return;
}
}
this.#fileCursor = byteOffset;
this.#chunkCursor = 0;
this.#chunk = undefined;
this.#view = undefined;
}
seekTo(byteOffset) {
if (byteOffset < 0 || byteOffset > this.size()) {
throw new Error(`Cannot seek to ${byteOffset}`);
}
// If we have a chunk it is more performant to attempt re-using the chunk. So we try to figure
// out if our seekTo puts us within the chunk and if so adjust the chunkCursor.
if (this.#chunk) {
// This is where the chunk starts in the file
const chunkStart = this.#fileCursor - this.#chunk.byteLength;
if (byteOffset >= chunkStart && byteOffset < this.#fileCursor) {
this.#chunkCursor = byteOffset - chunkStart;
return;
}
}
this.#fileCursor = byteOffset;
this.#chunkCursor = 0;
this.#chunk = undefined;
this.#view = undefined;
}
async skip(count) {
const byteOffset = this.#chunkCursor + count;
if (count < 0 || byteOffset < 0 || byteOffset > this.size()) {
throw new Error(`Cannot skip ${count} bytes`);
}
await this.#fetch(count);
this.#chunkCursor += count;
}
async peekUint8(offset) {
const view = await this.#fetch(offset + 1);
return view.getUint8(this.#chunkCursor + offset);
}
async readBytes(count) {
const view = await this.#fetch(count);
const data = new Uint8Array(view.buffer, view.byteOffset + this.#chunkCursor, count);
this.#chunkCursor += count;
return data;
}
async readUint8() {
const view = await this.#fetch(1);
return view.getUint8(this.#chunkCursor++);
}
async readInt16() {
const view = await this.#fetch(2);
const data = view.getInt16(this.#chunkCursor, true);
this.#chunkCursor += 2;
return data;
}
async readUint16() {
const view = await this.#fetch(2);
const data = view.getUint16(this.#chunkCursor, true);
this.#chunkCursor += 2;
return data;
}
async readInt32() {
const view = await this.#fetch(4);
const data = view.getInt32(this.#chunkCursor, true);
this.#chunkCursor += 4;
return data;
}
async readUint32() {
const view = await this.#fetch(4);
const data = view.getUint32(this.#chunkCursor, true);
this.#chunkCursor += 4;
return data;
}
async readFloat32() {
const view = await this.#fetch(4);
const data = view.getFloat32(this.#chunkCursor, true);
this.#chunkCursor += 4;
return data;
}
async readFloat64() {
const view = await this.#fetch(8);
const data = view.getFloat64(this.#chunkCursor, true);
this.#chunkCursor += 8;
return data;
}
async readInt64() {
const view = await this.#fetch(8);
const data = view.getBigInt64(this.#chunkCursor, true);
this.#chunkCursor += 8;
return data;
}
async readUint64() {
const view = await this.#fetch(8);
const data = view.getBigUint64(this.#chunkCursor, true);
this.#chunkCursor += 8;
return data;
}
async readString(length) {
const view = await this.#fetch(length);
const data = this.#textDecoder.decode(view.buffer.slice(view.byteOffset + this.#chunkCursor, view.byteOffset + this.#chunkCursor + length));
this.#chunkCursor += length;
return data;
}
async #fetch(bytesRequired) {
if (bytesRequired > this.remaining()) {
throw new Error(`Cannot read ${bytesRequired} bytes from ${this.size()} byte source, ${this.remaining()} bytes remaining`);
}
if (!this.#chunk || this.#chunkCursor === this.#chunk.byteLength) {
const fileRemaining = this.size() - this.#fileCursor;
this.#chunk = await this.#file.read(this.#fileCursor, clamp(this.chunkSize, bytesRequired, fileRemaining));
this.#view = new DataView(this.#chunk.buffer, this.#chunk.byteOffset, this.#chunk.byteLength);
this.#chunkCursor = 0;
this.#fileCursor += this.#chunk.byteLength;
}
let bytesAvailable = this.#chunk.byteLength - this.#chunkCursor;
const bytesNeeded = bytesRequired - bytesAvailable;
if (bytesAvailable < bytesRequired) {
const fileRemaining = this.size() - this.#fileCursor;
const curChunk = this.#chunk;
const nextChunk = await this.#file.read(this.#fileCursor, clamp(this.chunkSize, bytesNeeded, fileRemaining));
this.#chunk = concat(curChunk.slice(this.#chunkCursor), nextChunk);
this.#view = new DataView(this.#chunk.buffer, this.#chunk.byteOffset, this.#chunk.byteLength);
this.#chunkCursor = 0;
this.#fileCursor += nextChunk.byteLength;
bytesAvailable = this.#chunk.byteLength - this.#chunkCursor;
if (bytesAvailable < bytesRequired) {
throw new Error(`Requested ${bytesRequired} bytes but ${bytesAvailable} bytes available`);
}
}
return this.#view;
}
}
exports.ChunkedReader = ChunkedReader;
function concat(a, b) {
const c = new Uint8Array(a.byteLength + b.byteLength);
c.set(a);
c.set(b, a.byteLength);
return c;
}
function clamp(value, min, max) {
return Math.max(min, Math.min(max, value));
}
//# sourceMappingURL=ChunkedReader.js.map