diff --git a/demo/node/rntuple_selector.js b/demo/node/rntuple_selector.js new file mode 100644 index 000000000..40bb42845 --- /dev/null +++ b/demo/node/rntuple_selector.js @@ -0,0 +1,34 @@ +import { rntupleProcess } from '../../modules/rntuple.mjs'; +import { TSelector, openFile} from 'jsroot'; + +const selector = new TSelector(); +selector.sum = 0; +selector.count = 0; + +selector.Begin = function() { + console.log('Begin processing'); +}; + +selector.Process = function() { + console.log('Entry : ', this.tgtobj); + this.sum += this.tgtobj.myDouble; + this.count++; +}; + +selector.Terminate = function() { + if (this.count === 0) + console.error('No entries processed'); + else + console.log(`Mean = ${(this.sum / this.count).toFixed(4)} from ${this.count} entries`); +}; + +if (typeof window === 'undefined') { + openFile('./simple.root') + .then(file => file.readObject('myNtuple')) + .then(rntuple => { + if (!rntuple) throw new Error('myNtuple not found'); + return rntupleProcess(rntuple, selector); + }) + .then(() => console.log('RNTuple::Process finished')) + .catch(err => console.error(err)); +} diff --git a/modules/rntuple.mjs b/modules/rntuple.mjs index 2e08d9355..1dfc0c1f8 100644 --- a/modules/rntuple.mjs +++ b/modules/rntuple.mjs @@ -551,30 +551,10 @@ async function readHeaderFooter(tuple) { if (!firstColumn) throw new Error('No column descriptor found'); - const field = tuple.builder.fieldDescriptors?.[firstColumn.fieldId]; - - // Returns the size in bytes of one value based on its type - function getElementSize(typeName) { - switch (typeName) { - case 'double': return 8; - case 'float': return 4; - case 'int32_t': - case 'uint32_t': return 4; - case 'int64_t': - case 'uint64_t': return 8; - case 'int16_t': - case 'uint16_t': return 2; - case 'bool': - case 'uint8_t': - case 'int8_t': return 1; - default: - throw new Error(`Unknown type for uncompressed page size: ${typeName}`); - } - } - + const field = tuple.builder.fieldDescriptors?.[firstColumn.fieldId], // Deserialize the Page List Envelope - const group = tuple.builder.clusterGroups?.[0]; + group = tuple.builder.clusterGroups?.[0]; if (!group || !group.pageListLocator) throw new Error('No valid cluster group or page list locator found'); @@ -600,7 +580,7 @@ async function readHeaderFooter(tuple) { const pageOffset = Number(firstPage.locator.offset), pageSize = Number(firstPage.locator.size), - elementSize = getElementSize(field?.typeName ?? ''), + elementSize = firstColumn.bitsOnStorage / 8, numElements = Number(firstPage.numElements), uncompressedPageSize = elementSize * numElements; @@ -628,6 +608,56 @@ async function readHeaderFooter(tuple) { }); } +// Read and process the next data cluster from the RNTuple +function readNextCluster(rntuple, selector) { + const builder = rntuple.builder, + clusterSummary = builder.clusterSummaries[selector.currentCluster], + pages = builder.pageLocations[selector.currentCluster][0].pages; + + selector.currentCluster++; + +// Build flat array of [offset, size, offset, size, ...] to read pages + const dataToRead = pages.flatMap(p => [Number(p.locator.offset), Number(p.locator.size)]); + +return rntuple.$file.readBuffer(dataToRead).then(blobsRaw => { + const blobs = Array.isArray(blobsRaw) ? blobsRaw : [blobsRaw], + unzipPromises = blobs.map((blob, idx) => { + const numElements = Number(pages[idx].numElements); + return R__unzip(blob, 8 * numElements); + }); + + // Wait for all pages to be decompressed + return Promise.all(unzipPromises).then(unzipBlobs => { + const totalSize = unzipBlobs.reduce((sum, b) => sum + b.byteLength, 0), + flat = new Uint8Array(totalSize); + + let offset = 0; + for (const blob of unzipBlobs) { + flat.set(new Uint8Array(blob.buffer || blob), offset); + offset += blob.byteLength; + } + + // Create reader and deserialize doubles from the buffer + const reader = new RBufferReader(flat.buffer); + for (let i = 0; i < clusterSummary.numEntries; ++i) { + selector.tgtobj.myDouble = reader.readF64(); + selector.Process(); + } + + selector.Terminate(); + }); +}); +} + +// TODO args can later be used to filter fields, limit entries, etc. +// Create reader and deserialize doubles from the buffer +function rntupleProcess(rntuple, selector, args) { + return readHeaderFooter(rntuple).then(() => { + selector.Begin(); + selector.currentCluster = 0; + return readNextCluster(rntuple, selector, args); + }); +} /** @summary Create hierarchy of ROOT::RNTuple object * @desc Used by hierarchy painter to explore sub-elements @@ -653,4 +683,4 @@ async function tupleHierarchy(tuple_node, tuple) { }); } -export { tupleHierarchy, readHeaderFooter, RBufferReader }; +export { tupleHierarchy, readHeaderFooter, RBufferReader, rntupleProcess };