@@ -10,10 +10,16 @@ export type RangeMapper = (
1010) => { url : string ; fromByte : number ; toByte : number } ;
1111
1212export type LazyFileConfig = {
13+ /** function to map a read request to an url with read request */
1314 rangeMapper : RangeMapper ;
1415 /** must be known beforehand if there's multiple server chunks (i.e. rangeMapper returns different urls) */
1516 fileLength ?: number ;
1617 requestChunkSize : number ;
18+ /** number of virtual read heads. default: 3 */
19+ maxReadHeads ?: number ;
20+ /** max read speed for sequential access. default: 5 MiB */
21+ maxReadSpeed ?: number ;
22+ logPageReads ?: boolean ;
1723} ;
1824export type PageReadLog = {
1925 pageno : number ;
@@ -23,67 +29,116 @@ export type PageReadLog = {
2329 prefetch : number ;
2430} ;
2531
26- // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse.
32+ type ReadHead = { startChunk : number ; speed : number } ;
33+ // Lazy chunked Uint8Array (implements get and length from Uint8Array)
2734export class LazyUint8Array {
28- serverChecked = false ;
29- chunks : Uint8Array [ ] = [ ] ; // Loaded chunks. Index is the chunk number
35+ private serverChecked = false ;
36+ private readonly chunks : Uint8Array [ ] = [ ] ; // Loaded chunks. Index is the chunk number
3037 totalFetchedBytes = 0 ;
3138 totalRequests = 0 ;
3239 readPages : PageReadLog [ ] = [ ] ;
33- _length ?: number ;
40+ private _length ?: number ;
3441
35- lastChunk = 0 ;
36- speed = 1 ;
37- _chunkSize : number ;
38- rangeMapper : RangeMapper ;
39- maxSpeed : number ;
42+ // LRU list of read heds, max length = READ_HEADS. first is most recently used
43+ private readonly readHeads : ReadHead [ ] = [ ] ;
44+ private readonly _chunkSize : number ;
45+ private readonly rangeMapper : RangeMapper ;
46+ private readonly maxSpeed : number ;
47+ private readonly maxReadHeads : number ;
48+ private readonly logPageReads : boolean ;
4049
4150 constructor ( config : LazyFileConfig ) {
4251 this . _chunkSize = config . requestChunkSize ;
43- this . maxSpeed = ( 5 * 1024 * 1024 ) / this . _chunkSize ; // max 5MiB at once
52+ this . maxSpeed = Math . round (
53+ ( config . maxReadSpeed || 5 * 1024 * 1024 ) / this . _chunkSize
54+ ) ; // max 5MiB at once
55+ this . maxReadHeads = config . maxReadHeads ?? 3 ;
4456 this . rangeMapper = config . rangeMapper ;
57+ this . logPageReads = config . logPageReads ?? false ;
4558 if ( config . fileLength ) {
4659 this . _length = config . fileLength ;
4760 }
4861 }
62+ copyInto (
63+ buffer : Uint8Array ,
64+ outOffset : number ,
65+ _length : number ,
66+ start : number
67+ ) : number {
68+ if ( start >= this . length ) return 0 ;
69+ const length = Math . min ( this . length - start , _length ) ;
70+ const end = start + length ;
71+ let i = 0 ;
72+ while ( i < length ) {
73+ // {idx: 24, chunkOffset: 24, chunkNum: 0, wantedSize: 16}
74+ const idx = start + i ;
75+ const chunkOffset = idx % this . chunkSize ;
76+ const chunkNum = ( idx / this . chunkSize ) | 0 ;
77+ const wantedSize = Math . min ( this . chunkSize , end - idx ) ;
78+ let inChunk = this . getChunk ( chunkNum ) ;
79+ if ( chunkOffset !== 0 || wantedSize !== this . chunkSize ) {
80+ inChunk = inChunk . subarray ( chunkOffset , chunkOffset + wantedSize ) ;
81+ }
82+ buffer . set ( inChunk , outOffset + i ) ;
83+ i += inChunk . length ;
84+ }
85+ return length ;
86+ }
87+
4988 get ( idx : number ) {
5089 if ( idx > this . length - 1 || idx < 0 ) {
5190 return undefined ;
5291 }
5392 var chunkOffset = idx % this . chunkSize ;
5493 var chunkNum = ( idx / this . chunkSize ) | 0 ;
55- return this . getter ( chunkNum ) [ chunkOffset ] ;
94+ return this . getChunk ( chunkNum ) [ chunkOffset ] ;
5695 }
5796 lastGet = - 1 ;
58- getter ( wantedChunkNum : number ) {
97+ /* find the best matching existing read head to get given chunk or create a new one */
98+ private moveReadHead ( wantedChunkNum : number ) : ReadHead {
99+ for ( const [ i , head ] of this . readHeads . entries ( ) ) {
100+ const fetchStartChunkNum = head . startChunk + head . speed ;
101+ const newSpeed = head . speed * 2 ;
102+ const wantedIsInNextFetchOfHead =
103+ wantedChunkNum >= fetchStartChunkNum &&
104+ wantedChunkNum < fetchStartChunkNum + newSpeed ;
105+ if ( wantedIsInNextFetchOfHead ) {
106+ head . speed = Math . min ( this . maxSpeed , newSpeed ) ;
107+ head . startChunk = fetchStartChunkNum ;
108+ if ( i !== 0 ) {
109+ // move head to front
110+ this . readHeads . splice ( i , 1 ) ;
111+ this . readHeads . unshift ( head ) ;
112+ }
113+ return head ;
114+ }
115+ }
116+ const newHead : ReadHead = {
117+ startChunk : wantedChunkNum ,
118+ speed : 1 ,
119+ } ;
120+ this . readHeads . unshift ( newHead ) ;
121+ while ( this . readHeads . length > this . maxReadHeads ) this . readHeads . pop ( ) ;
122+ return newHead ;
123+ }
124+ private getChunk ( wantedChunkNum : number ) {
59125 let wasCached = true ;
60126 if ( typeof this . chunks [ wantedChunkNum ] === "undefined" ) {
61127 wasCached = false ;
62128 // double the fetching chunk size if the wanted chunk would be within the next fetch request
63- const wouldStartChunkNum = this . lastChunk + 1 ;
64- let fetchStartChunkNum ;
65- if (
66- wantedChunkNum >= wouldStartChunkNum &&
67- wantedChunkNum < wouldStartChunkNum + this . speed * 2
68- ) {
69- fetchStartChunkNum = wouldStartChunkNum ;
70- this . speed = Math . min ( this . maxSpeed , this . speed * 2 ) ;
71- } else {
72- fetchStartChunkNum = wantedChunkNum ;
73- this . speed = 1 ;
74- }
75- const chunksToFetch = this . speed ;
76- const startByte = fetchStartChunkNum * this . chunkSize ;
77- let endByte = ( fetchStartChunkNum + chunksToFetch ) * this . chunkSize - 1 ; // including this byte
129+ const head = this . moveReadHead ( wantedChunkNum ) ;
130+
131+ const chunksToFetch = head . speed ;
132+ const startByte = head . startChunk * this . chunkSize ;
133+ let endByte = ( head . startChunk + chunksToFetch ) * this . chunkSize - 1 ; // including this byte
78134 endByte = Math . min ( endByte , this . length - 1 ) ; // if datalength-1 is selected, this is the last block
79135
80- this . lastChunk = fetchStartChunkNum + chunksToFetch - 1 ;
81136 const buf = this . doXHR ( startByte , endByte ) ;
82137 for ( let i = 0 ; i < chunksToFetch ; i ++ ) {
83- const curChunk = fetchStartChunkNum + i ;
138+ const curChunk = head . startChunk + i ;
84139 if ( i * this . chunkSize >= buf . byteLength ) break ; // past end of file
85140 const curSize =
86- ( i + i ) * this . chunkSize > buf . byteLength
141+ ( i + 1 ) * this . chunkSize > buf . byteLength
87142 ? buf . byteLength - i * this . chunkSize
88143 : this . chunkSize ;
89144 // console.log("constructing chunk", buf.byteLength, i * this.chunkSize, curSize);
@@ -96,13 +151,13 @@ export class LazyUint8Array {
96151 }
97152 if ( typeof this . chunks [ wantedChunkNum ] === "undefined" )
98153 throw new Error ( "doXHR failed (bug)!" ) ;
99- const boring = this . lastGet == wantedChunkNum ;
154+ const boring = ! this . logPageReads || this . lastGet == wantedChunkNum ;
100155 if ( ! boring ) {
101156 this . lastGet = wantedChunkNum ;
102157 this . readPages . push ( {
103158 pageno : wantedChunkNum ,
104159 wasCached,
105- prefetch : wasCached ? 0 : this . speed - 1 ,
160+ prefetch : wasCached ? 0 : this . readHeads [ 0 ] . speed - 1 ,
106161 } ) ;
107162 }
108163 return this . chunks [ wantedChunkNum ] ;
@@ -121,7 +176,8 @@ export class LazyUint8Array {
121176 var usesGzip = xhr . getResponseHeader ( "Content-Encoding" ) === "gzip" ;
122177
123178 if ( ! hasByteServing ) {
124- const msg = "server does not support byte serving (`Accept-Ranges: bytes` header missing), or your database is hosted on CORS and the server d" ;
179+ const msg =
180+ "server does not support byte serving (`Accept-Ranges: bytes` header missing), or your database is hosted on CORS and the server doesn't mark the accept-ranges header as exposed" ;
125181 console . error ( msg , "seen response headers" , xhr . getAllResponseHeaders ( ) ) ;
126182 // throw Error(msg);
127183 }
@@ -149,7 +205,7 @@ export class LazyUint8Array {
149205 }
150206 private doXHR ( absoluteFrom : number , absoluteTo : number ) {
151207 console . log (
152- `- [xhr of size ${ ( absoluteTo + 1 - absoluteFrom ) / 1024 } KiB]`
208+ `[xhr of size ${ ( absoluteTo + 1 - absoluteFrom ) / 1024 } KiB @ ${ absoluteFrom / 1024 } KiB]`
153209 ) ;
154210 this . totalFetchedBytes += absoluteTo - absoluteFrom ;
155211 this . totalRequests ++ ;
@@ -232,19 +288,10 @@ export function createLazyFile(
232288 position : number
233289 ) {
234290 FS . forceLoadFile ( node ) ;
235- console . log (
236- `[fs: ${ length / 1024 } KiB read request offset @ ${ position / 1024 } KiB `
237- ) ;
291+
238292 const contents = stream . node . contents ;
239- if ( position >= contents . length ) return 0 ;
240- const size = Math . min ( contents . length - position , length ) ;
241293
242- // TODO: optimize this to copy whole chunks at once
243- for ( let i = 0 ; i < size ; i ++ ) {
244- // LazyUint8Array from sync binary XHR
245- buffer [ offset + i ] = contents . get ( position + i ) ! ;
246- }
247- return size ;
294+ return contents . copyInto ( buffer , offset , length , position ) ;
248295 } ;
249296 node . stream_ops = stream_ops ;
250297 return node ;
0 commit comments