From 07e53140ef1138635d20d61097eb5f4b9a769c3d Mon Sep 17 00:00:00 2001
From: Terry Truong <terry06890@gmail.com>
Date: Tue, 3 Jan 2023 23:42:36 +1100
Subject: Throttle server queries

Store previous queried range to avoid repeated duplicate queries.
In onResize(), account for edge case of 'await' taking a long time.
---
 src/App.vue | 201 +++++++++++++++++++++++++++++++++---------------------------
 1 file changed, 110 insertions(+), 91 deletions(-)

(limited to 'src/App.vue')

diff --git a/src/App.vue b/src/App.vue
index de127f6..b65e0dc 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -147,101 +147,119 @@ const EVENT_REQ_LIMIT = 300;
 const MAX_EVENTS_PER_UNIT = 4; // Should equal MAX_DISPLAYED_PER_UNIT in backend gen_disp_data.py
 let queriedRanges: DateRangeTree[] = SCALES.map(() => new DateRangeTree());
 	// For each scale, holds date ranges for which data has already been queried fromm the server
-let pendingReq = false; // Used to serialise event-req handling
+const SERVER_QUERY_TIMEOUT = 1000 // Used to throttle server queries
+let lastEvtDispHdlrTime = 0;
+let afterEvtDispHdlr = 0; // Used to trigger handler after ending a run of event-display events
+let lastQueriedRange: [HistDate, HistDate] | null = null;
 async function onEventDisplay(
 		timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){
-	while (pendingReq){
-		await timeout(100);
-	}
-	pendingReq = true;
-	// Skip if range has been queried, and enough of its events have been obtained
-	if (queriedRanges[scaleIdx].contains([firstDate, lastDate])){
-		// Get number of events in range, server-side
-		let fullCount = 0;
-		let date = firstDate.clone();
-		let eventCounts: Map<number, number> = new Map(); // For calculating number of events, client-side
-		while (date.isEarlier(lastDate)){
-			let unit = dateToUnit(date, SCALES[scaleIdx]);
-			if (unitCountMaps.value[scaleIdx].has(unit)){
-				fullCount += Math.min(MAX_EVENTS_PER_UNIT, unitCountMaps.value[scaleIdx].get(unit)!);
+	async function handleEvent(
+			timelineId: number, eventIds: number[], firstDate: HistDate, lastDate: HistDate, scaleIdx: number){
+		// Skip if range has been queried, and enough of its events have been obtained
+		if (queriedRanges[scaleIdx].contains([firstDate, lastDate])){
+			// Get number of events in range, server-side
+			let fullCount = 0;
+			let date = firstDate.clone();
+			let eventCounts: Map<number, number> = new Map(); // For calculating number of events, client-side
+			while (date.isEarlier(lastDate)){
+				let unit = dateToUnit(date, SCALES[scaleIdx]);
+				if (unitCountMaps.value[scaleIdx].has(unit)){
+					fullCount += Math.min(MAX_EVENTS_PER_UNIT, unitCountMaps.value[scaleIdx].get(unit)!);
+				}
+				eventCounts.set(unit, 0);
+				stepDate(date, SCALES[scaleIdx], {inplace: true});
 			}
-			eventCounts.set(unit, 0);
-			stepDate(date, SCALES[scaleIdx], {inplace: true});
-		}
-		if (fullCount > 0){
-			// Get number of events, client-side
-			let eventCount = 0;
-			let itr = eventTree.value.lowerBound(new HistEvent(0, '', firstDate))
-			while (itr.data() != null){
-				let event = itr.data()!;
-				itr.next();
-				if (!event.start.isEarlier(lastDate)){
-					break;
+			if (fullCount > 0){
+				// Get number of events, client-side
+				let eventCount = 0;
+				let itr = eventTree.value.lowerBound(new HistEvent(0, '', firstDate))
+				while (itr.data() != null){
+					let event = itr.data()!;
+					itr.next();
+					if (!event.start.isEarlier(lastDate)){
+						break;
+					}
+					let unit = dateToUnit(event.start, SCALES[scaleIdx]);
+					if (eventCounts.has(unit)){
+						eventCounts.set(unit, eventCounts.get(unit)! + 1);
+					}
 				}
-				let unit = dateToUnit(event.start, SCALES[scaleIdx]);
-				if (eventCounts.has(unit)){
-					eventCounts.set(unit, eventCounts.get(unit)! + 1);
+				for (let [, count] of eventCounts.entries()){
+					eventCount += Math.min(MAX_EVENTS_PER_UNIT, count);
+				}
+				// If we have enough events
+				if (eventCount >= fullCount || eventCount >= EVENT_REQ_LIMIT){
+					return;
 				}
 			}
-			for (let [, count] of eventCounts.entries()){
-				eventCount += Math.min(MAX_EVENTS_PER_UNIT, count);
+		}
+		// Get events from server
+		if (lastQueriedRange != null && lastQueriedRange[0].equals(firstDate) && lastQueriedRange[1].equals(lastDate)){
+			console.log(`INFO: Skipping redundant server request from ${firstDate} to ${lastDate}`);
+			return;
+		}
+		lastQueriedRange = [firstDate, lastDate]
+		let urlParams = new URLSearchParams({
+			type: 'events',
+			range: `${firstDate}.${lastDate}`,
+			scale: String(SCALES[scaleIdx]),
+			limit: String(EVENT_REQ_LIMIT),
+		});
+		let responseObj: EventResponseJson = await queryServer(urlParams);
+		if (responseObj == null){
+			return;
+		}
+		queriedRanges[scaleIdx].add([firstDate, lastDate]);
+		// Collect events
+		let eventAdded = false;
+		for (let eventObj of responseObj.events){
+			let event = jsonToHistEvent(eventObj);
+			let success = eventTree.value.insert(event);
+			if (success){
+				eventAdded = true;
+				idToEvent.set(event.id, event);
 			}
-			// If we have enough events
-			if (eventCount >= fullCount || eventCount >= EVENT_REQ_LIMIT){
-				pendingReq = false;
-				return;
+		}
+		// Collect unit counts
+		const unitCounts = responseObj.unitCounts;
+		if (unitCounts == null){
+			console.log('WARNING: Exceeded unit-count limit for server query');
+		} else {
+			for (let [unitStr, count] of Object.entries(unitCounts)){
+				let unit = parseInt(unitStr)
+				if (isNaN(unit)){
+					console.log('WARNING: Invalid non-integer unit value in server response');
+					break;
+				}
+				unitCountMaps.value[scaleIdx].set(unit, count)
 			}
 		}
-	}
-	// Get events from server
-	let urlParams = new URLSearchParams({
-		type: 'events',
-		range: `${firstDate}.${lastDate}`,
-		scale: String(SCALES[scaleIdx]),
-		limit: String(EVENT_REQ_LIMIT),
-	});
-	let responseObj: EventResponseJson = await queryServer(urlParams);
-	if (responseObj == null){
-		pendingReq = false;
-		return;
-	}
-	queriedRanges[scaleIdx].add([firstDate, lastDate]);
-	// Collect events
-	let eventAdded = false;
-	for (let eventObj of responseObj.events){
-		let event = jsonToHistEvent(eventObj);
-		let success = eventTree.value.insert(event);
-		if (success){
-			eventAdded = true;
-			idToEvent.set(event.id, event);
+		// Notify components if new events were added
+		if (eventAdded){
+			eventTree.value = rbtree_shallow_copy(eventTree.value); // Note: triggerRef(eventTree) does not work here
 		}
-	}
-	// Collect unit counts
-	const unitCounts = responseObj.unitCounts;
-	if (unitCounts == null){
-		console.log('WARNING: Exceeded unit-count limit for server query');
-	} else {
-		for (let [unitStr, count] of Object.entries(unitCounts)){
-			let unit = parseInt(unitStr)
-			if (isNaN(unit)){
-				console.log('WARNING: Invalid non-integer unit value in server response');
-				break;
-			}
-			unitCountMaps.value[scaleIdx].set(unit, count)
+		// Check memory limit
+		displayedEvents.set(timelineId, eventIds);
+		if (eventTree.value.size > EXCESS_EVENTS_THRESHOLD){
+			console.log(`INFO: Calling reduceEvents() upon reaching ${eventTree.value.size} events`);
+			reduceEvents();
+			queriedRanges.forEach((t: DateRangeTree) => t.clear());
 		}
 	}
-	// Notify components if new events were added
-	if (eventAdded){
-		eventTree.value = rbtree_shallow_copy(eventTree.value); // Note: triggerRef(eventTree) does not work here
-	}
-	// Check memory limit
-	displayedEvents.set(timelineId, eventIds);
-	if (eventTree.value.size > EXCESS_EVENTS_THRESHOLD){
-		console.log(`INFO: Calling reduceEvents() upon reaching ${eventTree.value.size} events`);
-		reduceEvents();
-		queriedRanges.forEach((t: DateRangeTree) => t.clear());
+	clearTimeout(afterEvtDispHdlr);
+	let currentTime = new Date().getTime();
+	if (currentTime - lastEvtDispHdlrTime > SERVER_QUERY_TIMEOUT){
+		lastEvtDispHdlrTime = currentTime;
+		await handleEvent(timelineId, eventIds, firstDate, lastDate, scaleIdx);
+		lastEvtDispHdlrTime = new Date().getTime();
+	} else {
+		// Set up handler to execute after ending a run of event-display events
+		afterEvtDispHdlr = window.setTimeout(async () => {
+			afterEvtDispHdlr = 0;
+			await handleEvent(timelineId, eventIds, firstDate, lastDate, scaleIdx);
+			lastEvtDispHdlrTime = new Date().getTime();
+		}, SERVER_QUERY_TIMEOUT);
 	}
-	pendingReq = false;
 }
 
 // For resize handling
@@ -249,22 +267,23 @@ let lastResizeHdlrTime = 0; // Used to throttle resize handling
 let afterResizeHdlr = 0; // Used to trigger handler after ending a run of resize events
 async function onResize(){
 	// Handle event if not recently done
-	let handleResize = async () => {
+	async function handleResize(){
 		updateAreaDims();
-	};
+	}
+	clearTimeout(afterResizeHdlr);
 	let currentTime = new Date().getTime();
 	if (currentTime - lastResizeHdlrTime > 200){
 		lastResizeHdlrTime = currentTime;
 		await handleResize();
 		lastResizeHdlrTime = new Date().getTime();
+	} else {
+		// Set up handler to execute after ending a run of resize events
+		afterResizeHdlr = window.setTimeout(async () => {
+			afterResizeHdlr = 0;
+			await handleResize();
+			lastResizeHdlrTime = new Date().getTime();
+		}, 200); // If too small, touch-device detection when swapping to/from mobile-mode gets unreliable
 	}
-	// Setup a handler to execute after ending a run of resize events
-	clearTimeout(afterResizeHdlr);
-	afterResizeHdlr = window.setTimeout(async () => {
-		afterResizeHdlr = 0;
-		await handleResize();
-		lastResizeHdlrTime = new Date().getTime();
-	}, 200); // If too small, touch-device detection when swapping to/from mobile-mode gets unreliable
 }
 onMounted(() => window.addEventListener('resize', onResize));
 onUnmounted(() => window.removeEventListener('resize', onResize));
-- 
cgit v1.2.3

