Skip to content

Commit 2fd05ce

Browse files
committed
multiple read heads
1 parent 30a4d35 commit 2fd05ce

File tree

4 files changed

+100
-52
lines changed

4 files changed

+100
-52
lines changed

sql.js/src/api.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
11161116
return null;
11171117
}
11181118
errmsg = sqlite3_errmsg(this.db);
1119-
throw new Error("SQLite: " + errmsg);
1119+
throw new Error("SQLite: " + (errmsg || "Code " + returnCode));
11201120
};
11211121

11221122
/** Returns the number of changed rows (modified, inserted or deleted)
@@ -1153,7 +1153,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
11531153
try {
11541154
result = func.apply(null, args);
11551155
} catch (error) {
1156-
sqlite3_result_error(cx, error, -1);
1156+
sqlite3_result_error(cx, "JS threw: " + error, -1);
11571157
return;
11581158
}
11591159
Module.set_return_value(cx, result);
@@ -1232,6 +1232,7 @@ Module["onRuntimeInitialized"] = function onRuntimeInitialized() {
12321232
}
12331233
break;
12341234
default:
1235+
console.warn("unknown sqlite result type: ", typeof result, result);
12351236
sqlite3_result_null(cx);
12361237
}
12371238
}

src/lazyFile.ts

Lines changed: 92 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ export type RangeMapper = (
1010
) => { url: string; fromByte: number; toByte: number };
1111

1212
export 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
};
1824
export 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)
2734
export 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;

src/sqlite.worker.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,13 @@ async function init(wasmfile: string) {
3636
}
3737

3838
export function toObjects<T>(res: QueryExecResult[]): T[] {
39-
const r = res[0];
40-
if (!r) return [];
41-
return r.values.map((v) => {
39+
return res.flatMap(r => r.values.map((v) => {
4240
const o: any = {};
4341
for (let i = 0; i < r.columns.length; i++) {
4442
o[r.columns[i]] = v[i];
4543
}
4644
return o as T;
47-
});
45+
}));
4846
}
4947

5048
export type SplitFileConfig =
@@ -181,6 +179,8 @@ const mod = {
181179
config.serverMode === "chunked"
182180
? config.databaseLengthBytes
183181
: undefined,
182+
logPageReads: true,
183+
maxReadHeads: 3
184184
});
185185
lazyFiles.set(filename, lazyFile);
186186
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
// "incremental": true, /* Enable incremental compilation */
77
"target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
88
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
9-
"lib": [], /* Specify library files to be included in the compilation. */
9+
"lib": ["es2020"], /* Specify library files to be included in the compilation. */
1010
// "allowJs": true, /* Allow javascript files to be compiled. */
1111
// "checkJs": true, /* Report errors in .js files. */
1212
"jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */

0 commit comments

Comments
 (0)