Warning, /firebird/packages/root2dex/src/partial-read.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The other half of the contract: converting one event must read a small
0003 * fraction of the file, not all of it. Event files are routinely multi-GB, and
0004 * the whole point of going through JSROOT is that the browser never sees more
0005 * than the baskets covering the requested entry.
0006 *
0007 * The test counts bytes through a ByteRangeReader - the same seam a future
0008 * XRootD proxy plugs into.
0009 */
0010
0011 import { describe, expect, it } from 'vitest';
0012 import { existsSync, statSync } from 'node:fs';
0013 import { open, type FileHandle } from 'node:fs/promises';
0014 import { resolve } from 'node:path';
0015 import { createByteRangeSource, type ByteRangeReader } from './byte-range-source';
0016 import { Root2DexConverter } from './convert';
0017 import { PodioEventFile } from './podio-file';
0018
0019 const ROOT_DATA = resolve(__dirname, '../../../pyrobird/tests/unit_tests/data');
0020 const EIC_FILE = resolve(ROOT_DATA, 'reco_2024-09_craterlake_2evt.edm4eic.root');
0021
0022 /** A ByteRangeReader over a local file that records what it was asked for. */
0023 class CountingFileReader implements ByteRangeReader {
0024 readonly name: string;
0025 readonly size: number;
0026 bytesRead = 0;
0027 requests = 0;
0028
0029 private constructor(
0030 private readonly handle: FileHandle,
0031 path: string,
0032 size: number,
0033 ) {
0034 this.name = path;
0035 this.size = size;
0036 }
0037
0038 static async open(path: string): Promise<CountingFileReader> {
0039 return new CountingFileReader(await open(path, 'r'), path, statSync(path).size);
0040 }
0041
0042 async readRange(position: number, length: number): Promise<ArrayBuffer> {
0043 this.requests++;
0044 this.bytesRead += length;
0045 const buffer = new Uint8Array(length);
0046 await this.handle.read(buffer, 0, length, position);
0047 return buffer.buffer;
0048 }
0049
0050 async close(): Promise<void> {
0051 await this.handle.close();
0052 }
0053 }
0054
0055 describe('partial reading', () => {
0056 it.runIf(existsSync(EIC_FILE))(
0057 'reads a fraction of the file to convert one event',
0058 async () => {
0059 const reader = await CountingFileReader.open(EIC_FILE);
0060 try {
0061 const converter = await Root2DexConverter.open(createByteRangeSource(reader));
0062 const afterOpen = reader.bytesRead;
0063
0064 const event = await converter.convertEntry(0);
0065 const afterConvert = reader.bytesRead;
0066
0067 expect(event.pieces.length).toBeGreaterThan(0);
0068 // Opening reads the key directory, the streamer info and the TTree
0069 // metadata - podio trees are branch-heavy, but still a small part of
0070 // the file, and the cost is paid once per file, not per event
0071 expect(afterOpen).toBeLessThan(reader.size * 0.5);
0072 // Converting one event reads only the baskets of the branches that
0073 // event needs
0074 expect(afterConvert - afterOpen).toBeLessThan(reader.size * 0.25);
0075 expect(reader.requests).toBeGreaterThan(0);
0076 } finally {
0077 await reader.close();
0078 }
0079 },
0080 );
0081
0082 it.runIf(existsSync(EIC_FILE))('converts a second event without reopening', async () => {
0083 const reader = await CountingFileReader.open(EIC_FILE);
0084 try {
0085 const converter = await Root2DexConverter.open(createByteRangeSource(reader));
0086 await converter.convertEntry(0);
0087 const afterFirst = reader.bytesRead;
0088 await converter.convertEntry(1);
0089 const secondEventBytes = reader.bytesRead - afterFirst;
0090
0091 // The second event costs at most what the first one did: the tree
0092 // metadata is already in hand, and both entries of this two-event file
0093 // often share the very same baskets
0094 expect(secondEventBytes).toBeLessThanOrEqual(afterFirst);
0095 } finally {
0096 await reader.close();
0097 }
0098 });
0099
0100 it.runIf(existsSync(EIC_FILE))('rejects entries outside the tree', async () => {
0101 const file = await PodioEventFile.open(EIC_FILE);
0102 await expect(file.readEntry(['CentralTrackSegments.points_begin'], 99)).rejects.toThrow(
0103 /outside of the tree/,
0104 );
0105 });
0106 });