Browse Source

Merge pull request #49 from NASA-AMMOS/priority-queue-sort

Priority queue sort
Garrett Johnson 5 năm trước cách đây
mục cha
commit
4549fda3b5

+ 16 - 0
README.md

@@ -199,6 +199,14 @@ maxJobs = 6 : number
 
 
 The maximum number of jobs to be processing at once.
 The maximum number of jobs to be processing at once.
 
 
+### .unloadPriorityCallback
+
+```js
+unloadPriorityCallback = null : ( item ) => Number
+```
+
+Function to derive the unload priority of the given item. Higher priority values get unloaded first.
+
 ## LRUCache
 ## LRUCache
 
 
 Utility class for the TilesRenderer to keep track of currently used items so rendered items will not be unloaded.
 Utility class for the TilesRenderer to keep track of currently used items so rendered items will not be unloaded.
@@ -227,6 +235,14 @@ unloadPercent = 0.05 : number
 
 
 The maximum percentage of [minSize](#minSize) to unload during a given frame.
 The maximum percentage of [minSize](#minSize) to unload during a given frame.
 
 
+### .priorityCallback
+
+```js
+priorityCallback = null : ( item ) => Number
+```
+
+Function to derive the job priority of the given item. Higher priority values get processed first.
+
 # LICENSE
 # LICENSE
 
 
 The software is available under the [Apache V2.0 license](../LICENSE.txt).
 The software is available under the [Apache V2.0 license](../LICENSE.txt).

+ 6 - 4
src/base/TilesRendererBase.js

@@ -10,7 +10,7 @@ import { UNLOADED, LOADING, PARSING, LOADED, FAILED } from './constants.js';
 // TODO: Make sure active state works as expected
 // TODO: Make sure active state works as expected
 
 
 // Function for sorting the evicted LRU items. We should evict the shallowest depth first.
 // Function for sorting the evicted LRU items. We should evict the shallowest depth first.
-const lruSort = ( a, b ) => a.__depth - b.__depth;
+const priorityCallback = tile => 1 / tile.__depth;
 
 
 export class TilesRendererBase {
 export class TilesRendererBase {
 
 
@@ -44,13 +44,15 @@ export class TilesRendererBase {
 		this.fetchOptions = {};
 		this.fetchOptions = {};
 
 
 		const lruCache = new LRUCache();
 		const lruCache = new LRUCache();
-		lruCache.sortCallback = lruSort;
+		lruCache.unloadPriorityCallback = priorityCallback;
 
 
 		const downloadQueue = new PriorityQueue();
 		const downloadQueue = new PriorityQueue();
 		downloadQueue.maxJobs = 4;
 		downloadQueue.maxJobs = 4;
+		downloadQueue.priorityCallback = priorityCallback;
 
 
 		const parseQueue = new PriorityQueue();
 		const parseQueue = new PriorityQueue();
 		parseQueue.maxJobs = 1;
 		parseQueue.maxJobs = 1;
+		parseQueue.priorityCallback = priorityCallback;
 
 
 		this.lruCache = lruCache;
 		this.lruCache = lruCache;
 		this.downloadQueue = downloadQueue;
 		this.downloadQueue = downloadQueue;
@@ -329,7 +331,7 @@ export class TilesRendererBase {
 		stats.downloading ++;
 		stats.downloading ++;
 		tile.__loadAbort = controller;
 		tile.__loadAbort = controller;
 		tile.__loadingState = LOADING;
 		tile.__loadingState = LOADING;
-		downloadQueue.add( tile, priority, tile => {
+		downloadQueue.add( tile, tile => {
 
 
 			if ( tile.__loadIndex !== loadIndex ) {
 			if ( tile.__loadIndex !== loadIndex ) {
 
 
@@ -373,7 +375,7 @@ export class TilesRendererBase {
 				tile.__loadAbort = null;
 				tile.__loadAbort = null;
 				tile.__loadingState = PARSING;
 				tile.__loadingState = PARSING;
 
 
-				return parseQueue.add( buffer, priority, buffer => {
+				return parseQueue.add( buffer, buffer => {
 
 
 					// if it has been unloaded then the tile has been disposed
 					// if it has been unloaded then the tile has been disposed
 					if ( tile.__loadIndex !== loadIndex ) {
 					if ( tile.__loadIndex !== loadIndex ) {

+ 6 - 4
src/utilities/LRUCache.js

@@ -8,6 +8,7 @@ function enqueueMicrotask( callback ) {
 	Promise.resolve().then( callback );
 	Promise.resolve().then( callback );
 
 
 }
 }
+
 class LRUCache {
 class LRUCache {
 
 
 	constructor() {
 	constructor() {
@@ -22,7 +23,7 @@ class LRUCache {
 		this.itemList = [];
 		this.itemList = [];
 		this.callbacks = new Map();
 		this.callbacks = new Map();
 
 
-		this.sortCallback = null;
+		this.unloadPriorityCallback = null;
 
 
 	}
 	}
 
 
@@ -119,11 +120,11 @@ class LRUCache {
 		const callbacks = this.callbacks;
 		const callbacks = this.callbacks;
 		const unused = itemList.length - usedSet.size;
 		const unused = itemList.length - usedSet.size;
 		const excess = itemList.length - targetSize;
 		const excess = itemList.length - targetSize;
-		const prioritySortCb = this.sortCallback;
+		const unloadPriorityCallback = this.unloadPriorityCallback;
 
 
 		if ( excess > 0 && unused > 0 ) {
 		if ( excess > 0 && unused > 0 ) {
 
 
-			if ( prioritySortCb ) {
+			if ( unloadPriorityCallback ) {
 
 
 				// used items should be at the end of the array
 				// used items should be at the end of the array
 				itemList.sort( ( a, b ) => {
 				itemList.sort( ( a, b ) => {
@@ -138,7 +139,8 @@ class LRUCache {
 					} else if ( ! usedA && ! usedB ) {
 					} else if ( ! usedA && ! usedB ) {
 
 
 						// Use the sort function otherwise
 						// Use the sort function otherwise
-						return prioritySortCb( a, b );
+						// higher priority should be further to the left
+						return unloadPriorityCallback( b ) - unloadPriorityCallback( a );
 
 
 					} else {
 					} else {
 
 

+ 46 - 25
src/utilities/PriorityQueue.js

@@ -1,3 +1,10 @@
+// Fires at the end of the frame and before the next one
+function enqueueMicrotask( callback ) {
+
+	Promise.resolve().then( callback );
+
+}
+
 class PriorityQueue {
 class PriorityQueue {
 
 
 	constructor() {
 	constructor() {
@@ -6,31 +13,41 @@ class PriorityQueue {
 		this.maxJobs = 6;
 		this.maxJobs = 6;
 
 
 		this.items = [];
 		this.items = [];
+		this.callbacks = new Map();
 		this.currJobs = 0;
 		this.currJobs = 0;
 		this.scheduled = false;
 		this.scheduled = false;
 
 
+		this.priorityCallback = () => {
+
+			throw new Error( 'PriorityQueue: PriorityCallback function not defined.' );
+
+		};
+
 	}
 	}
 
 
-	add( item, priority, callback ) {
+	sort() {
 
 
-		return new Promise( ( resolve, reject ) => {
+		const priorityCallback = this.priorityCallback;
+		const items = this.items;
+		items.sort( ( a, b ) => {
 
 
-			const prCallback = ( ...args ) => callback( ...args ).then( resolve ).catch( reject );
-			const items = this.items;
-			for ( let i = 0, l = items.length; i < l; i ++ ) {
+			return priorityCallback( a ) - priorityCallback( b );
 
 
-				const thisItem = items[ i ];
-				if ( thisItem.priority > priority ) {
+		} );
 
 
-					items.splice( i, 0, { priority, item, callback: prCallback } );
-					this.scheduleJobRun();
-					return;
+	}
+
+	add( item, callback ) {
+
+		return new Promise( ( resolve, reject ) => {
 
 
-				}
+			const prCallback = ( ...args ) => callback( ...args ).then( resolve ).catch( reject );
+			const items = this.items;
+			const callbacks = this.callbacks;
 
 
-			}
+			items.push( item );
+			callbacks.set( item, prCallback );
 
 
-			items.push( { priority, item, callback: prCallback } );
 			this.scheduleJobRun();
 			this.scheduleJobRun();
 
 
 		} );
 		} );
@@ -40,15 +57,13 @@ class PriorityQueue {
 	remove( item ) {
 	remove( item ) {
 
 
 		const items = this.items;
 		const items = this.items;
-		for ( let i = 0, l = items.length; i < l; i ++ ) {
+		const callbacks = this.callbacks;
 
 
-			const thisItem = items[ i ];
-			if ( thisItem.item === item ) {
+		const index = items.indexOf( item );
+		if ( index !== - 1 ) {
 
 
-				items.splice( i, 1 );
-				break;
-
-			}
+			items.splice( index, 1 );
+			callbacks.delete( item );
 
 
 		}
 		}
 
 
@@ -56,13 +71,18 @@ class PriorityQueue {
 
 
 	tryRunJobs() {
 	tryRunJobs() {
 
 
+		this.sort();
+
 		const items = this.items;
 		const items = this.items;
+		const callbacks = this.callbacks;
 		const maxJobs = this.maxJobs;
 		const maxJobs = this.maxJobs;
-		while ( maxJobs > this.currJobs && items.length > 0 ) {
+		let currJobs = this.currJobs;
+		while ( maxJobs > currJobs && items.length > 0 ) {
 
 
-			this.currJobs ++;
-			const { item, priority, callback } = items.pop();
-			callback( item, priority )
+			currJobs ++;
+			const item = items.pop();
+			const callback = callbacks.get( item );
+			callback( item )
 				.then( () => {
 				.then( () => {
 
 
 					this.currJobs --;
 					this.currJobs --;
@@ -77,6 +97,7 @@ class PriorityQueue {
 				} );
 				} );
 
 
 		}
 		}
+		this.currJobs = currJobs;
 
 
 	}
 	}
 
 
@@ -84,7 +105,7 @@ class PriorityQueue {
 
 
 		if ( ! this.scheduled ) {
 		if ( ! this.scheduled ) {
 
 
-			Promise.resolve().then( () => {
+			enqueueMicrotask( () => {
 
 
 				this.tryRunJobs();
 				this.tryRunJobs();
 				this.scheduled = false;
 				this.scheduled = false;

+ 1 - 1
test/LRUCache.test.js

@@ -61,7 +61,7 @@ describe( 'LRUCache', () => {
 	it( 'should sort before unloading', () => {
 	it( 'should sort before unloading', () => {
 
 
 		const cache = new LRUCache();
 		const cache = new LRUCache();
-		cache.sortCallback = ( a, b ) => b.priority - a.priority;
+		cache.unloadPriorityCallback = item => item.priority;
 		cache.minSize = 0;
 		cache.minSize = 0;
 		cache.maxSize = 10;
 		cache.maxSize = 10;
 		cache.unloadPercent = 1;
 		cache.unloadPercent = 1;

+ 54 - 35
test/PriorityQueue.test.js

@@ -9,13 +9,14 @@ describe( 'PriorityQueue', () => {
 
 
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
 		queue.maxJobs = 6;
 		queue.maxJobs = 6;
-		queue.add( {}, 6, () => new Promise( () => {} ) );
-		queue.add( {}, 3, () => new Promise( () => {} ) );
-		queue.add( {}, 4, () => new Promise( () => {} ) );
-		queue.add( {}, 0, () => new Promise( () => {} ) );
-		queue.add( {}, 8, () => new Promise( () => {} ) );
-		queue.add( {}, 2, () => new Promise( () => {} ) );
-		queue.add( {}, 1, () => new Promise( () => {} ) );
+		queue.priorityCallback = item => item.priority;
+		queue.add( { priority : 6 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 3 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 4 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 0 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 8 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 2 }, () => new Promise( () => {} ) );
+		queue.add( { priority : 1 }, () => new Promise( () => {} ) );
 
 
 		await nextFrame();
 		await nextFrame();
 
 
@@ -24,46 +25,60 @@ describe( 'PriorityQueue', () => {
 
 
 	} );
 	} );
 
 
-	it( 'should add the jobs in the correct order.', () => {
+	it( 'should run jobs in the correct order.', async () => {
+
+		const result = [];
+		const cb = item => new Promise( resolve => {
+
+			result.push( item.priority );
+			resolve();
+
+		} );
 
 
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
-		queue.add( {}, 6, () => new Promise( () => {} ) );
-		queue.add( {}, 3, () => new Promise( () => {} ) );
-		queue.add( {}, 4, () => new Promise( () => {} ) );
-		queue.add( {}, 0, () => new Promise( () => {} ) );
-		queue.add( {}, 8, () => new Promise( () => {} ) );
-		queue.add( {}, 2, () => new Promise( () => {} ) );
-		queue.add( {}, 1, () => new Promise( () => {} ) );
+		queue.maxJobs = 1;
+		queue.priorityCallback = item => item.priority;
+		queue.add( { priority : 6 }, cb );
+		queue.add( { priority : 3 }, cb );
+		queue.add( { priority : 4 }, cb );
+		queue.add( { priority : 0 }, cb );
+		queue.add( { priority : 8 }, cb );
+		queue.add( { priority : 2 }, cb );
+		queue.add( { priority : 1 }, cb );
 
 
-		expect( queue.items.map( item => item.priority ) ).toEqual( [ 0, 1, 2, 3, 4, 6, 8 ] );
+		await nextTick();
+
+		expect( result ).toEqual( [ 8, 6, 4, 3, 2, 1, 0 ] );
 
 
 	} );
 	} );
 
 
 	it( 'should remove an item from the queue correctly.', () => {
 	it( 'should remove an item from the queue correctly.', () => {
 
 
-		const A = {};
-		const B = {};
-		const C = {};
-		const D = {};
+		const A = { priority : 0 };
+		const B = { priority : 1 };
+		const C = { priority : 2 };
+		const D = { priority : 3 };
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
-		queue.add( A, 0, () => new Promise( () => {} ) );
-		queue.add( B, 1, () => new Promise( () => {} ) );
-		queue.add( C, 2, () => new Promise( () => {} ) );
-		queue.add( D, 3, () => new Promise( () => {} ) );
+		queue.priorityCallback = item => item.priority;
+		queue.add( A, () => new Promise( () => {} ) );
+		queue.add( B, () => new Promise( () => {} ) );
+		queue.add( C, () => new Promise( () => {} ) );
+		queue.add( D, () => new Promise( () => {} ) );
+		queue.sort();
 
 
-		expect( queue.items.map( item => item.item ) ).toEqual( [ A, B, C, D ] );
+		expect( queue.items ).toEqual( [ A, B, C, D ] );
 
 
 		queue.remove( C );
 		queue.remove( C );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ A, B, D ] );
+		expect( queue.items ).toEqual( [ A, B, D ] );
 
 
 		queue.remove( A );
 		queue.remove( A );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ B, D ] );
+		expect( queue.items ).toEqual( [ B, D ] );
 
 
 		queue.remove( B );
 		queue.remove( B );
-		expect( queue.items.map( item => item.item ) ).toEqual( [ D ] );
+		expect( queue.items ).toEqual( [ D ] );
 
 
 		queue.remove( D );
 		queue.remove( D );
-		expect( queue.items.map( item => item.item ) ).toEqual( [] );
+		expect( queue.items ).toEqual( [] );
 
 
 	} );
 	} );
 
 
@@ -73,15 +88,16 @@ describe( 'PriorityQueue', () => {
 		let resolveFunc = null;
 		let resolveFunc = null;
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
 		queue.maxJobs = 1;
 		queue.maxJobs = 1;
+		queue.priorityCallback = item => item.priority;
 
 
-		queue.add( {}, 1, () => new Promise( resolve => {
+		queue.add( { priority : 1 }, () => new Promise( resolve => {
 
 
 			resolveFunc = resolve;
 			resolveFunc = resolve;
 			called ++;
 			called ++;
 
 
 		} ) );
 		} ) );
 
 
-		queue.add( {}, 0, () => new Promise( () => {
+		queue.add( { priority : 0 }, () => new Promise( () => {
 
 
 			called ++;
 			called ++;
 
 
@@ -105,12 +121,13 @@ describe( 'PriorityQueue', () => {
 
 
 	it( 'should fire the callback with the item and priority.', async () => {
 	it( 'should fire the callback with the item and priority.', async () => {
 
 
-		const A = {};
+		const A = { priority : 100 };
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
-		queue.add( A, 100, ( item, priority ) => new Promise( () => {
+		queue.priorityCallback = item => item.priority;
+
+		queue.add( A, item => new Promise( () => {
 
 
 			expect( item ).toEqual( A );
 			expect( item ).toEqual( A );
-			expect( priority ).toEqual( 100 );
 
 
 		} ) );
 		} ) );
 
 
@@ -121,8 +138,10 @@ describe( 'PriorityQueue', () => {
 	it( 'should return a promise that resolves from the add function.',  async () => {
 	it( 'should return a promise that resolves from the add function.',  async () => {
 
 
 		const queue = new PriorityQueue();
 		const queue = new PriorityQueue();
+		queue.priorityCallback = item => item.priority;
+
 		let result = null;
 		let result = null;
-		queue.add( {}, 0, item => Promise.resolve( 1000 ) ).then( res => result = res );
+		queue.add( { priority : 0 }, item => Promise.resolve( 1000 ) ).then( res => result = res );
 
 
 		expect( result ).toEqual( null );
 		expect( result ).toEqual( null );