Skip to content

Commit a22b771

Browse files
authored
feat(widget): Batch tile requests within an animation frame (#181)
* feat(widget): Batch tile requests within an animation frame HiGlass's built-in data fetching logic is designed to optimize API calls by consolidating requests based on time, ID, and server. Our custom Jupyter data fetcher doesn't include these optimizations. This PR introduces a `consolidator` helper, which performs a much simpler form of batching. Since we assume a single server, this function only batches tile requests that occur within the same animation frame and submits them together. This helps reduce the number of comms calls and deduplicates requests efficiently. Initial testing suggests it feels quite responsive! * Add `MAX_TILES_PER_REQUEST` const * Use `Promise.withResolvers` * Remove chunkIterable for now
1 parent 12fde00 commit a22b771

File tree

2 files changed

+62
-12
lines changed

2 files changed

+62
-12
lines changed

src/higlass/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export type HGC = {
7070
tileResponseToData<T>(
7171
inputData: Record<string, T>,
7272
server: string,
73-
theseTileIds: Array<string>,
73+
tileIds: Array<string>,
7474
): Record<string, unknown>;
7575
};
7676
};

src/higlass/widget.js

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ async function registerJupyterHiGlassDataFetcher(model) {
194194
/** @type {(...args: ConstructorParameters<PluginDataFetcherConstructor>) => DataFetcher} */
195195
function DataFetcher(hgc, dataConfig, pubSub) {
196196
let config = { ...dataConfig, server: NAME };
197+
197198
return new hgc.dataFetchers.DataFetcher(config, pubSub, {
198199
async fetchTilesetInfo({ server, tilesetUid }) {
199200
assert(server === NAME, "must be a jupyter server");
@@ -202,17 +203,29 @@ async function registerJupyterHiGlassDataFetcher(model) {
202203
});
203204
return response.payload;
204205
},
205-
async fetchTiles({ tileIds }) {
206-
let response = await sendCustomMessage(tModel, {
207-
payload: { type: "tiles", tileIds },
208-
});
209-
let result = hgc.services.tileResponseToData(
210-
response.payload,
211-
NAME,
212-
tileIds,
213-
);
214-
return result;
215-
},
206+
fetchTiles: consolidator(
207+
/** @param {Array<WithResolvers<{ tileIds: Array<string> }, Record<string, any>>>} requests */
208+
async (requests) => {
209+
let tileIds = [...new Set(requests.flatMap((r) => r.data.tileIds))];
210+
let response = await sendCustomMessage(tModel, {
211+
payload: { type: "tiles", tileIds },
212+
});
213+
let tiles = hgc.services.tileResponseToData(
214+
response.payload,
215+
NAME,
216+
tileIds,
217+
);
218+
for (let request of requests) {
219+
/** @type {Record<string, unknown>} */
220+
const requestData = {};
221+
for (let id of request.data.tileIds) {
222+
let tileData = tiles[id];
223+
if (tileData) requestData[id] = tileData;
224+
}
225+
request.resolve(requestData);
226+
}
227+
},
228+
),
216229
registerTileset() {
217230
throw new Error("Not implemented");
218231
},
@@ -302,3 +315,40 @@ export default {
302315
};
303316
},
304317
};
318+
319+
/**
320+
* @template T
321+
* @template U
322+
* @typedef {{ data: T, resolve: (success: U) => void, reject: (err: unknown) => void }} WithResolvers
323+
*/
324+
325+
/**
326+
* Collects multiple calls within the same frame to processes them as a batch.
327+
*
328+
* The provided `processBatch` function receives an array of items, each with
329+
* associated resolvers for fulfilling the original request.
330+
*
331+
* @template T
332+
* @template U
333+
* @param {(batch: Array<WithResolvers<T, U>>) => void} processBatch - A function to process each batch.
334+
* @returns {(item: T) => Promise<U>} - A function to enqueue items.
335+
*/
336+
function consolidator(processBatch) {
337+
/** @type {Array<WithResolvers<T, U>>} */
338+
let pending = [];
339+
/** @type {number} */
340+
let id = 0;
341+
342+
function run() {
343+
processBatch(pending);
344+
pending = [];
345+
id = 0;
346+
}
347+
348+
return function enqueue(data) {
349+
id = id || requestAnimationFrame(() => run());
350+
let { promise, resolve, reject } = Promise.withResolvers();
351+
pending.push({ data, resolve, reject });
352+
return promise;
353+
};
354+
}

0 commit comments

Comments
 (0)