Back to home page

EIC code displayed by LXR

 
 

    


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 });